Console Output Anywhere — with Symfony

I love writing (Laravel) artisan commands, basically little scripts that perform some sort of task. But what I love even more, is having those scripts output stuff to the console, so that it can tell me what’s going on! I’m going to show you how to easily enable any PHP class, script, etc to output whatever you want to the console with Symfony.

Some Background Info

If you hate reading, skip ahead to the implementation. Otherwise, we will have to start with the Symfony OutputInterface and StyleInterface. The former handles the actual act of writing to the console screen and how verbose to be. The latter handles grouping or formatting text in different meaningful ways (think titles, sections, asking questions and handling user input, etc) as well as the creation and management of progress bars.

I have to backtrack a bit and say that Symfony has great docs explaining how to use the console output, and I recommend checking them out. Despite learning a lot of cool in those references, admittedly the one thing I couldn’t comprehend was how to instantiate the actual class which handled the output (writing of text to screen). Most examples shown are written from the context of a Symfony Command class, which has the output class (or OutputInterface rather) injected in.

Things finally started clicking together when I took a look at the SymfonyStyle class. This class marries the OutputInterface and StyleInterface. In order to get an instance of this class you can choose from a few different input and output options here and here. Since I wasn’t interested in the input option I did the following:

new SymfonyStyle(new StringInput(''), new ConsoleOutput(OutputInterface::VERBOSITY_NORMAL))

Now I could finally proceed to my main goal which was giving the ability to output stuff to any class, regardless of whether you were in a Command class or not. I also didn’t want it to be annoying by always outputting extra info if I were using the artisan tinker command with Laravel; so there had to be some level of flexibility. Below is what I came up with.

Implementation

<?php
namespace App\Classes;
use App\Classes\Contracts\FlexibleConsoleOutputStyleInterface;
use App\Classes\Traits\FlexibleConsoleOutputterTrait;
use DB;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class EloquentTableTruncator implements FlexibleConsoleOutputStyleInterface
{
use FlexibleConsoleOutputterTrait;
/**
* @var Collection $models
*/
protected $models;
/**
* @param Collection|Model[]|string $models
*/
public function __construct($models)
{
$this->setModels($models);
}
/**
* @param Collection|Model[]|string $models
* @return $this
*/
public function setModels($models)
{
$this->models = collect();
if (is_array($models) || is_string($models))
$models = collect($models);
while($model = $models->shift()) {
if (is_string($model))
$model = new $model;
if ($model instanceof Model)
$this->models->put(get_class($model), $model);
}
return $this;
}
/**
* @return Collection
*/
public function empty()
{
$results = $this->models->transform(function($model) {
/* @var Model $model */
$this->comment("Truncating {$model->getTable()}…");
$success = true;
$success &= DB::statement("SET foreign_key_checks=0;");
$success &= DB::statement("TRUNCATE `{$model->getTable()}`;");
$success &= DB::statement("ALTER TABLE `{$model->getTable()}` AUTO_INCREMENT=1;");
$success &= DB::statement("SET foreign_key_checks=1;");
$this->eventStatus($success);
return (bool) $success;
});
$this->models = collect();
return $results;
}
/**
* @param Collection|Model[]|string $models
* @return Collection
*/
public static function staticEmpty($models)
{
return (new self($models))->empty();
}
}
<?php
namespace App\Classes\Contracts;
interface FlexibleConsoleOutputStyleInterface
{
/**
* Instantiate an instance of OutputInterface.
*
* @return self
*/
public function initializeOutput();
/**
* @param $string
* @param string|null $style
* @param int|string|null $verbosity
* @return void
*/
public function line($string, $style = null, $verbosity = null);
/**
* @param $string
* @param int|string|null $verbosity
* @return void
*/
public function comment($string, $verbosity = null);
/**
* @param string $string
* @return void
*/
public function success($string);
/**
* Write a string as error output.
*
* @param string $string
* @param int|string|null $verbosity
* @return void
*/
public function error($string, $verbosity = null);
/**
* @param bool $wasPassed
* @param int|string|null $verbosity
* @return mixed
*/
public function eventStatus(bool $wasPassed, $verbosity = null);
/**
* @param int $count
* @return void
*/
public function progressStart($count = 0);
/**
* @param int $count
* @return void
*/
public function progressAdvance($count = 1);
/**
* @return void
*/
public function progressFinish();
}
<?php
namespace App\Classes;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
trait FlexibleConsoleOutputterTrait
{
/**
* @var SymfonyStyle $output
*/
protected $output;
/**
* The mapping between human readable verbosity levels and Symfony's OutputInterface.
*
* @var array
*/
protected $verbosityMap = [
'v' => OutputInterface::VERBOSITY_VERBOSE,
'vv' => OutputInterface::VERBOSITY_VERY_VERBOSE,
'vvv' => OutputInterface::VERBOSITY_DEBUG,
'quiet' => OutputInterface::VERBOSITY_QUIET,
'normal' => OutputInterface::VERBOSITY_NORMAL,
];
/**
* The default verbosity of output commands.
*
* @var int
*/
protected $verbosity = OutputInterface::VERBOSITY_NORMAL;
public function initializeOutput()
{
$this->output = new SymfonyStyle(new StringInput(''), new ConsoleOutput($this->verbosity));
return $this;
}
/**
* Write a string as standard output.
*
* @param string $string
* @param string|null $style
* @param int|string|null $verbosity
* @return void
*/
public function line($string, $style = null, $verbosity = null)
{
if (! isset($this->output))
return;
$styled = $style ? "<$style>$string</$style>" : $string;
$this->output->writeln($styled, $this->parseVerbosity($verbosity));
}
/**
* Write a string as comment output.
*
* @param string $string
* @param int|string|null $verbosity
* @return void
*/
public function comment($string, $verbosity = null)
{
$this->line($string, 'comment', $verbosity);
}
/**
* @param string $string
* @param int|string|null $verbosity
* @return void
*/
public function success($string, $verbosity = null)
{
if (! isset($this->output))
return;
$this->output->success($string);
}
/**
* Write a string as error output.
*
* @param string $string
* @param int|string|null $verbosity
* @return void
*/
public function error($string, $verbosity = null)
{
$this->line($string, 'error', $verbosity);
}
/**
* @param bool $wasPassed
* @param int|string|null $verbosity
*/
public function eventStatus(bool $wasPassed, $verbosity = null)
{
if ($wasPassed) {
$this->success('Success', $verbosity);
} else {
$this->error('Fail', $verbosity);
}
}
/**
* @param int $count
* @return void
*/
public function progressStart($count = 0)
{
if (! isset($this->output))
return;
$this->output->progressStart($count);
}
/**
* @param int $count
* @return void
*/
public function progressAdvance($count = 1)
{
if (! isset($this->output))
return;
$this->output->progressAdvance($count);
}
/**
* @return void
*/
public function progressFinish()
{
if (! isset($this->output))
return;
$this->output->progressFinish();
}
/**
* Get the verbosity level in terms of Symfony's OutputInterface level.
*
* @param string|int|null $level
* @return int
*/
protected function parseVerbosity($level = null)
{
if (isset($this->verbosityMap[$level])) {
$level = $this->verbosityMap[$level];
} elseif (! is_int($level)) {
$level = $this->verbosity;
}
return $level;
}
}

We need something which will handle the functionality of the outputter in a sort of on/off fashion. What I mean by that is, if an outputter hasn’t been initialized, any call to one of it’s functions should not result in a null pointer exception. However if it has been initialized, calls should be executed as expected. It should also be able to instantiate an OutputInterface class if necessary.

An Interface is perfect for this job because we can implement this functionality in any class we want. It provides clear documentation as to what it’s capable of doing. We can see if a class is capable of outputting text using instanceof (and most importantly we get type-hinting yo!).

The ability to easily and quickly bring this to any class is achieved via the use of a Trait. Simply include the following statement at the beginning of your class:

use FlexibleConsoleOutputter;

Aside from its code once-use anywhere functionality traits provide, this trait gives us the ability to freely use functions associated with the outputter (line, comment, progressStart) without worrying about null pointer exceptions. In other words, if the output class hasn’t been instantiated, calling these functions will not have any adverse effects because each function checks if the outputter has been set.

This class has been set up with the interface and trait I created. It allows me to use $this->line($message) as if I were inside a command class. It will not output anything unless the $output property has been set. If I want to enable output functionality I can achieve this by doing

(new EloquentTableTruncator())->iniatlizeOutput()->execute()

or by using initializeOutput() straight away in the class constructor.

And that’s it! I hope this leads to more expressive scripts being written, and never having to wonder if your class, script, or command is actually working or not.

Leave a Reply

Like what you've read? Then please make sure you've left a like or a comment. If you'd be willing to receive email notifications of new ramblings when they arrive, I'd appreciate it! If not, I'd be happy to have you back soon.

Designed with WordPress

Discover more from Eddie's Code Shop

Subscribe now to keep reading and get access to the full archive.

Continue reading