diff --git a/bin/pest b/bin/pest index 5ff4eea1..6c704ca1 100755 --- a/bin/pest +++ b/bin/pest @@ -4,8 +4,10 @@ use NunoMaduro\Collision\Provider; use Pest\Actions\ValidatesEnvironment; use Pest\Console\Command; +use Pest\Support\Container; use Pest\TestSuite; use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; (static function () { // Used when Pest is required using composer. @@ -25,8 +27,13 @@ use Symfony\Component\Console\Output\ConsoleOutput; $rootPath = getcwd(); $testSuite = TestSuite::getInstance($rootPath); + $output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, true); + + $container = Container::getInstance(); + $container->add(TestSuite::class, $testSuite); + $container->add(OutputInterface::class, $output); ValidatesEnvironment::in($testSuite); - exit((new Command($testSuite, new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, true)))->run($_SERVER['argv'])); + exit($container->get(Command::class)->run($_SERVER['argv'])); })(); diff --git a/composer.json b/composer.json index 2b7803ec..cdbfcec8 100644 --- a/composer.json +++ b/composer.json @@ -85,6 +85,9 @@ "providers": [ "Pest\\Laravel\\PestServiceProvider" ] + }, + "branch-alias": { + "dev-master": "dev-feature/add-container" } } } diff --git a/src/Console/Command.php b/src/Console/Command.php index 290dbe04..d7481372 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -62,7 +62,7 @@ final class Command extends BaseCommand /** @var HandlesArguments $plugin */ foreach ($plugins as $plugin) { - $argv = $plugin->handleArguments($this->testSuite, $argv); + $argv = $plugin->handleArguments($argv); } /* @@ -134,7 +134,7 @@ final class Command extends BaseCommand /** @var AddsOutput $plugin */ foreach ($plugins as $plugin) { - $result = $plugin->addOutput($this->testSuite, $this->output, $result); + $result = $plugin->addOutput($result); } exit($result); diff --git a/src/Contracts/Plugins/AddsOutput.php b/src/Contracts/Plugins/AddsOutput.php index 543eace9..a105e0d0 100644 --- a/src/Contracts/Plugins/AddsOutput.php +++ b/src/Contracts/Plugins/AddsOutput.php @@ -4,9 +4,6 @@ declare(strict_types=1); namespace Pest\Contracts\Plugins; -use Pest\TestSuite; -use Symfony\Component\Console\Output\OutputInterface; - /** * @internal */ @@ -15,5 +12,5 @@ interface AddsOutput /** * Allows to add custom output after the test suite was executed. */ - public function addOutput(TestSuite $testSuite, OutputInterface $output, int $testReturnCode): int; + public function addOutput(int $testReturnCode): int; } diff --git a/src/Contracts/Plugins/HandlesArguments.php b/src/Contracts/Plugins/HandlesArguments.php index b7f09068..e9c1b725 100644 --- a/src/Contracts/Plugins/HandlesArguments.php +++ b/src/Contracts/Plugins/HandlesArguments.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace Pest\Contracts\Plugins; -use Pest\TestSuite; - /** * @internal */ @@ -21,5 +19,5 @@ interface HandlesArguments * * @return array the updated list of arguments */ - public function handleArguments(TestSuite $testSuite, array $arguments): array; + public function handleArguments(array $arguments): array; } diff --git a/src/Support/Container.php b/src/Support/Container.php new file mode 100644 index 00000000..85a5f9d2 --- /dev/null +++ b/src/Support/Container.php @@ -0,0 +1,99 @@ + + */ + private $instances = []; + + /** + * Gets a new or already existing container. + */ + public static function getInstance(): self + { + if (static::$instance === null) { + static::$instance = new static(); + } + + return static::$instance; + } + + /** + * Gets a dependency from the container. + * + * @return object + */ + public function get(string $id) + { + if (array_key_exists($id, $this->instances)) { + return $this->instances[$id]; + } + + $this->instances[$id] = $this->build($id); + + return $this->instances[$id]; + } + + /** + * Adds the given instance to the container. + * + * @param mixed $instance + */ + public function add(string $id, $instance): void + { + $this->instances[$id] = $instance; + } + + /** + * Tries to build the given instance. + */ + private function build(string $id): object + { + /** @phpstan-ignore-next-line */ + $reflectionClass = new ReflectionClass($id); + + if ($reflectionClass->isInstantiable()) { + $constructor = $reflectionClass->getConstructor(); + + if ($constructor !== null) { + $params = array_map( + function (ReflectionParameter $param) use ($id) { + $candidate = null; + + if ($param->getType() !== null && $param->getType()->isBuiltin()) { + $candidate = $param->getName(); + } elseif ($param->getClass() !== null) { + $candidate = $param->getClass()->getName(); + } else { + throw ShouldNotHappen::fromMessage(sprintf('The type of `$%s` in `%s` cannot be determined.', $id, $param->getName())); + } + + return $this->get($candidate); + }, + $constructor->getParameters() + ); + + return $reflectionClass->newInstanceArgs($params); + } + } + + throw ShouldNotHappen::fromMessage(sprintf('A dependency with the name `%s` cannot be resolved.', $id)); + } +} diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 294814f2..1db9c356 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -118,6 +118,15 @@ PASS Tests\Unit\Support\Backtrace ✓ it gets file name from called file + PASS Tests\Unit\Support\Container + ✓ it exists + ✓ it gets an instance + ✓ it creates an instance and resolves parameters + ✓ it creates an instance and resolves also sub parameters + ✓ it can resolve builtin value types + ✓ it cannot resolve a parameter that requires additional dependencies + ✓ it cannot resolve a parameter without type + PASS Tests\Unit\Support\Reflection ✓ it gets file name from closure ✓ it gets property values @@ -134,5 +143,5 @@ WARN Tests\Visual\Success s visual snapshot of test suite on success - Tests: 6 skipped, 71 passed - Time: 2.87s + Tests: 6 skipped, 78 passed + Time: 3.40s diff --git a/tests/Unit/Support/Container.php b/tests/Unit/Support/Container.php new file mode 100644 index 00000000..33256885 --- /dev/null +++ b/tests/Unit/Support/Container.php @@ -0,0 +1,71 @@ +group('container'); + +beforeEach(function () { + $this->container = new Container(); +}); + +it('exists') + ->assertTrue(class_exists(Container::class)); + +it('gets an instance', function () { + $this->container->add(Container::class, $this->container); + assertSame($this->container, $this->container->get(Container::class)); +}); + +it('creates an instance and resolves parameters', function () { + $this->container->add(Container::class, $this->container); + $instance = $this->container->get(ClassWithDependency::class); + + assertInstanceOf(ClassWithDependency::class, $instance); +}); + +it('creates an instance and resolves also sub parameters', function () { + $this->container->add(Container::class, $this->container); + $instance = $this->container->get(ClassWithSubDependency::class); + + assertInstanceOf(ClassWithSubDependency::class, $instance); +}); + +it('can resolve builtin value types', function () { + $this->container->add('rootPath', getcwd()); + + $instance = $this->container->get(TestSuite::class); + assertInstanceOf(TestSuite::class, $instance); +}); + +it('cannot resolve a parameter that requires additional dependencies', function () { + $this->expectException(ShouldNotHappen::class); + $this->container->get(ClassWithDependency::class); +}); + +it('cannot resolve a parameter without type', function () { + $this->expectException(ShouldNotHappen::class); + $this->container->get(ClassWithoutTypeParameter::class); +}); + +class ClassWithDependency +{ + public function __construct(Container $container) + { + } +} + +class ClassWithSubDependency +{ + public function __construct(ClassWithDependency $param) + { + } +} + +class ClassWithoutTypeParameter +{ + public function __construct($param) + { + } +}