Skip to content

Commit

Permalink
Feat: Add direct call through facade (#1)
Browse files Browse the repository at this point in the history
* Feat: Add direct call through facade

* Test: Add tests for StreamableArtisan

* Doc: Add example

* Doc: Add real world examples
  • Loading branch information
mpyw authored Dec 28, 2019
1 parent 56624e6 commit 2b3bd2b
Show file tree
Hide file tree
Showing 9 changed files with 600 additions and 0 deletions.
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,46 @@ class RunCommand extends Command
```

Note that you can use `yes()` as an alias of `usingInfiniteInput()`.

### Use via `StreamableArtisan` Facade

```php
<?php

use Mpyw\StreamableConsole\StreamableArtisan;

StreamableArtisan::usingInputStream("no\n")->call('example:quiz');
StreamableArtisan::usingInfiniteInput("no\n")->call('example:quiz');
```

## Real World Examples

### [barryvdh/laravel-ide-helper](https://github.com/barryvdh/laravel-ide-helper)

```php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Mpyw\StreamableConsole\Streamable;

class AllCommand extends Command
{
use Streamable;

protected $signature = 'ide-helper:all';

/**
* @return int
*/
public function handle(): int
{
$this->call('ide-helper:generate');
$this->call('ide-helper:meta');
$this->usingInputStream("no\n")->call('ide-helper:models');

return 0;
}
}
```
62 changes: 62 additions & 0 deletions src/ArtisanCallFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace Mpyw\StreamableConsole;

use function GuzzleHttp\Psr7\stream_for;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Contracts\Container\Container;

/**
* Class ArtisanCallFactory
*/
class ArtisanCallFactory
{
/**
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;

/**
* @var \Illuminate\Contracts\Console\Kernel|\Illuminate\Foundation\Console\Kernel
*/
protected $kernel;

/**
* PendingStreamableCall constructor.
*
* @param \Illuminate\Contracts\Container\Container $container
* @param \Illuminate\Contracts\Console\Kernel $kernel
*/
public function __construct(Container $container, Kernel $kernel)
{
$this->container = $container;
$this->kernel = $kernel;
}

/**
* @param null|bool|callable|float|int|\Iterator|\Psr\Http\Message\StreamInterface|resource|string $resource Entity body data
* @return \Mpyw\StreamableConsole\PendingArtisanCall
*/
public function usingInputStream($resource): PendingArtisanCall
{
return new PendingArtisanCall($this->container, $this->kernel, stream_for($resource));
}

/**
* @param string $input
* @return \Mpyw\StreamableConsole\PendingArtisanCall
*/
public function usingInfiniteInput(string $input): PendingArtisanCall
{
return (new InfiniteStreamRegistrar())->usingInfiniteInput($input, [$this, 'usingInputStream']);
}

/**
* @param string $input
* @return \Mpyw\StreamableConsole\PendingArtisanCall
*/
public function yes(string $input = "yes\n"): PendingArtisanCall
{
return $this->usingInfiniteInput($input);
}
}
10 changes: 10 additions & 0 deletions src/InteractiveInputs/ArrayInput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Mpyw\StreamableConsole\InteractiveInputs;

use Symfony\Component\Console\Input\ArrayInput as BaseArrayInput;

class ArrayInput extends BaseArrayInput
{
use Concerns\ForcesInteractions;
}
18 changes: 18 additions & 0 deletions src/InteractiveInputs/Concerns/ForcesInteractions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Mpyw\StreamableConsole\InteractiveInputs\Concerns;

/**
* Trait ForcesInteractions
*/
trait ForcesInteractions
{
/**
* Sets the input interactivity.
*
* @param bool $interactive If the input should be interactive
*/
public function setInteractive($interactive): void
{
}
}
10 changes: 10 additions & 0 deletions src/InteractiveInputs/StringInput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Mpyw\StreamableConsole\InteractiveInputs;

use Symfony\Component\Console\Input\StringInput as BaseStringInput;

class StringInput extends BaseStringInput
{
use Concerns\ForcesInteractions;
}
181 changes: 181 additions & 0 deletions src/PendingArtisanCall.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php

namespace Mpyw\StreamableConsole;

use Illuminate\Console\Application as Artisan;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Application as ArtisanContract;
use Illuminate\Contracts\Console\Kernel as KernelContract;
use Illuminate\Contracts\Container\Container;
use Illuminate\Foundation\Console\Kernel;
use Mpyw\StreamableConsole\InteractiveInputs\ArrayInput;
use Mpyw\StreamableConsole\InteractiveInputs\StringInput;
use Psr\Http\Message\StreamInterface;
use ReflectionMethod;
use ReflectionProperty;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\StreamableInputInterface;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;

/**
* Class PendingArtisanCall
*/
class PendingArtisanCall
{
/**
* @var Container
*/
protected $container;

/**
* @var Kernel|KernelContract
*/
protected $kernel;

/**
* @var null|StreamInterface
*/
protected $stream;

/**
* PendingStreamableCall constructor.
*
* @param Container $container
* @param KernelContract $kernel
* @param StreamInterface $stream
*/
public function __construct(Container $container, KernelContract $kernel, StreamInterface $stream)
{
$this->container = $container;
$this->kernel = $kernel;
$this->stream = $stream;
}

/** @noinspection PhpDocMissingThrowsInspection */

/**
* Call another console command.
*
* @param Command|string $command
* @param array $parameters
* @param null|OutputInterface $outputBuffer
* @return int
*/
public function call($command, array $parameters = [], ?OutputInterface $outputBuffer = null): int
{
$this->kernel->bootstrap();

$artisan = $this->getArtisan();

/* @var Command $command */
/* @var StreamableInputInterface $input */
[$command, $input] = $this->parseCommand($artisan, $command, $parameters);
$input->setStream($this->stream->detach());

if (!$artisan->has($command)) {
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $command));
}

/* @noinspection PhpUnhandledExceptionInspection */
return $artisan->run(
$input,
$this->setLastOutput($artisan, $outputBuffer ?: new BufferedOutput())
);
}

/**
* Get the Artisan application instance.
*
* @return Artisan|ArtisanContract
*/
public function getArtisan(): ArtisanContract
{
return $this->callMethod($this->kernel, __FUNCTION__);
}

/**
* Gets the name of the command based on input.
*
* @param ArtisanContract $artisan
* @param InputInterface $input
* @return null|string
*/
protected function getCommandName(ArtisanContract $artisan, InputInterface $input): ?string
{
return $this->callMethod($artisan, __FUNCTION__, $input);
}

/**
* @param ArtisanContract $artisan
* @param OutputInterface $output
* @return OutputInterface
*/
protected function setLastOutput(ArtisanContract $artisan, OutputInterface $output): OutputInterface
{
return $this->setProperty($artisan, 'lastOutput', $output);
}

/** @noinspection PhpDocMissingThrowsInspection */

/**
* Parse the incoming Artisan command and its input.
*
* @param ArtisanContract $artisan
* @param string $command
* @param array $parameters
* @return array
*/
protected function parseCommand(ArtisanContract $artisan, string $command, array $parameters): array
{
if (is_subclass_of($command, SymfonyCommand::class)) {
$callingClass = true;
/* @noinspection PhpUnhandledExceptionInspection */
$command = $this->container->make($command)->getName();
}

if (!isset($callingClass) && empty($parameters)) {
$command = $this->getCommandName($artisan, $input = new StringInput($command));
} else {
array_unshift($parameters, $command);
$input = new ArrayInput($parameters);
}

return [$command, $input];
}

/** @noinspection PhpDocMissingThrowsInspection */

/**
* @param mixed $object
* @param string $method
* @param array $arguments
* @return mixed
*/
protected function callMethod($object, string $method, ...$arguments)
{
/* @noinspection PhpUnhandledExceptionInspection */
$method = new ReflectionMethod($object, $method);
$method->setAccessible(true);
return $method->invokeArgs($object, $arguments);
}

/** @noinspection PhpDocMissingThrowsInspection */

/**
* @param mixed $object
* @param string $property
* @param mixed $value
* @return mixed
*/
protected function setProperty($object, string $property, $value)
{
/* @noinspection PhpUnhandledExceptionInspection */
$property = new ReflectionProperty($object, $property);
$property->setAccessible(true);
$property->setValue($object, $value);
return $value;
}
}
26 changes: 26 additions & 0 deletions src/StreamableArtisan.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Mpyw\StreamableConsole;

use Illuminate\Support\Facades\Facade;

/**
* Class StreamableArtisan
*
* @see \Mpyw\StreamableConsole\ArtisanCallFactory
*
* @method static \Mpyw\StreamableConsole\PendingArtisanCall usingInputStream(null|bool|callable|float|int|\Iterator|\Psr\Http\Message\StreamInterface|resource|string $resource) Entity body data
* @method static \Mpyw\StreamableConsole\PendingArtisanCall usingInfiniteInput(string $input)
* @method static \Mpyw\StreamableConsole\PendingArtisanCall yes(string $input = "yes\n")
* @method static int call(string $command, array $parameters, null|\Symfony\Component\Console\Output\OutputInterface $outputBuffer)
*/
class StreamableArtisan extends Facade
{
/**
* @return string
*/
public static function getFacadeAccessor(): string
{
return ArtisanCallFactory::class;
}
}
30 changes: 30 additions & 0 deletions tests/Feature/ArtisanTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Mpyw\StreamableConsole\Tests\Feature;

use Illuminate\Contracts\Console\Kernel;
use Mpyw\StreamableConsole\StreamableArtisan;
use Mpyw\StreamableConsole\Tests\Feature\Commands\QuizCommand;
use Mpyw\StreamableConsole\Tests\Feature\Commands\RepeatQuizCommand;
use Orchestra\Testbench\TestCase;

class ArtisanTest extends TestCase
{
public function testRun(): void
{
/* @var Kernel|\Illuminate\Foundation\Console\Kernel $kernel */
$kernel = $this->app->make(Kernel::class);
$kernel->registerCommand(new QuizCommand());

$this->assertSame(0, StreamableArtisan::usingInputStream("2\nno\n")->call('example:quiz'));
}

public function testRunInfinite(): void
{
/* @var Kernel|\Illuminate\Foundation\Console\Kernel $kernel */
$kernel = $this->app->make(Kernel::class);
$kernel->registerCommand(new RepeatQuizCommand());

$this->assertSame(0, StreamableArtisan::yes("2\n")->call('example:repeat-quiz'));
}
}
Loading

0 comments on commit 2b3bd2b

Please sign in to comment.