Search code examples
phplaravellaravel-artisan

Laravel write to console without impacting PHPUnit


I want to output to the console from an artisan command. This is deep in a long call stack so using $this->error('My warning'); is not suitable.

There is some solutions online that suggest doing.

$out = new ConsoleOutput();
$out->writeln('My warning');

This solves the problem, but when I run phpunit my command's output interferes with that of phpunit

vendor/bin/phpunit --filter=MyCmdTest
PHPUnit 9.3.10 by Sebastian Bergmann and contributors.

Warning:       Your XML configuration validates against a deprecated schema.
Suggestion:    Migrate your XML configuration using "--migrate-configuration"!

My warning
.

Time: 00:17.182, Memory: 56.50 MB

OK (1 test, 3 assertions)

What is the best way to write output to the cmd line deep within an artisan command. Without impacting the output of PHPUnit?

Edit

I'm calling the command like this.

$this->artisan('my:cmd')
    ->assertExitCode(0);

The problem with using the usual $this->error(), is i do something similar to this in the artisan command.

class MyCmd extends Command {
    public function handle(MyService $service) {
        $service->doStuff();
    }
}

class MyService {
    public function doStuff() {
        if (true) {
            // print a warning when the state is bad
        }
    }
}

The service is overly simplified as the logic inside the service(s) can be quite complex and passing a reference of the artisan command seems like the wrong approach. Also this would mean i had to pass it around to multiple services and or model logic etc.


Solution

  • Usually when thinking about separation of concerns (e.g. MVC structure) we think of a web context, keeping HTML and business logic separate. But the same thing applies to CLI code.

    Your Artisan command class is run from the command line, and should be outputting data there. Your service class is solely for running its processes and should not be outputting anything anywhere. What happens when you want to start using this service class in the context of a web interface, or piping content to a file?

    So what you should be doing is throwing an exception from your service class, and then catching it at a higher level. This allows the calling code to output the error message in a suitable format – in this case, using the methods available from the Command class.

    class MyCmd extends Command {
        public function handle(MyService $service) {
            try {
                $service->doStuff();
            } catch (\Exception $e) {
                $this->error($e->getMessage());
            }
        }
    }
    
    class MyService {
        public function doStuff() {
            if (true) {
                // THROW a warning when the state is bad
                throw new \Exception("the state is bad!");
            }
        }
    }
    

    If you'll have a variety of different errors that need to be handled differently you should consider writing your own exception classes, so your code might look like this instead:

    class MyCmd extends Command {
        public function handle(MyService $service) {
            try {
                $service->doStuff();
            } catch (MyException $e) {
                $this->error($e->getMessage());
            } catch (MyOtherException $e) {
                do_something_else($e->someProperty);
            }
        }
    }
    
    class MyService {
        public function doStuff() {
            if (true) {
                // THROW a warning when the state is bad
                throw new MyException("the state is bad!");
            }
            if (something else) {
                $e = new MyOtherException("something else happened!");
                $e->someProperty = "foo";
                throw $e;
            }
        }
    }
    
    class MyException extends \Exception {}
    class MyOtherException extends \Exception {}