From 84c9078bb790e45902d004e734217c22652ca825 Mon Sep 17 00:00:00 2001 From: "johannes.pichler" Date: Wed, 3 Jun 2020 22:08:25 +0200 Subject: [PATCH 1/4] Add basic container implementation --- composer.json | 3 ++ src/Support/Container.php | 82 ++++++++++++++++++++++++++++++++ tests/.snapshots/success.txt | 13 ++++- tests/Unit/Support/Container.php | 71 +++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 src/Support/Container.php create mode 100644 tests/Unit/Support/Container.php diff --git a/composer.json b/composer.json index 2f2ffd62..8a1e87ae 100644 --- a/composer.json +++ b/composer.json @@ -82,6 +82,9 @@ "providers": [ "Pest\\Laravel\\PestServiceProvider" ] + }, + "branch-alias": { + "dev-master": "dev-feature/add-container" } } } diff --git a/src/Support/Container.php b/src/Support/Container.php new file mode 100644 index 00000000..bead50ed --- /dev/null +++ b/src/Support/Container.php @@ -0,0 +1,82 @@ + + */ + private $instances = []; + + /** + * 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 4509aee3..151cb1fa 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -114,6 +114,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 @@ -130,5 +139,5 @@ WARN Tests\Visual\Success s visual snapshot of test suite on success - Tests: 6 skipped, 69 passed - Time: 2.34s + Tests: 6 skipped, 76 passed + Time: 3.32s 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) + { + } +} From 9899b3c3a4c969a41b68ca4dd345dcfd5cc9cacd Mon Sep 17 00:00:00 2001 From: "johannes.pichler" Date: Fri, 5 Jun 2020 07:48:51 +0200 Subject: [PATCH 2/4] Update plugin interfaces and instantiate container --- bin/pest | 9 ++++++++- src/Console/Command.php | 4 ++-- src/Contracts/Plugins/AddsOutput.php | 5 +---- src/Contracts/Plugins/HandlesArguments.php | 4 +--- src/Support/Container.php | 17 +++++++++++++++++ 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/bin/pest b/bin/pest index 9b66a2b6..ddb68f81 100755 --- a/bin/pest +++ b/bin/pest @@ -3,8 +3,10 @@ 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. @@ -24,8 +26,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/src/Console/Command.php b/src/Console/Command.php index b4dc3c75..7c7eb9a5 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); } /* @@ -133,7 +133,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 index bead50ed..85a5f9d2 100644 --- a/src/Support/Container.php +++ b/src/Support/Container.php @@ -13,11 +13,28 @@ use ReflectionParameter; */ final class Container { + /** + * @var self + */ + private static $instance; + /** * @var array */ 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. * From b20f208b554189bb7c3eb9acc92a2de5aea68b64 Mon Sep 17 00:00:00 2001 From: "johannes.pichler" Date: Wed, 3 Jun 2020 22:08:25 +0200 Subject: [PATCH 3/4] Add basic container implementation --- composer.json | 3 ++ src/Support/Container.php | 82 ++++++++++++++++++++++++++++++++ tests/.snapshots/success.txt | 14 ++++++ tests/Unit/Support/Container.php | 71 +++++++++++++++++++++++++++ 4 files changed, 170 insertions(+) create mode 100644 src/Support/Container.php create mode 100644 tests/Unit/Support/Container.php diff --git a/composer.json b/composer.json index f832fda5..29ecbb4d 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/Support/Container.php b/src/Support/Container.php new file mode 100644 index 00000000..bead50ed --- /dev/null +++ b/src/Support/Container.php @@ -0,0 +1,82 @@ + + */ + private $instances = []; + + /** + * 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 e34ef134..ae43a75c 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,10 @@ WARN Tests\Visual\Success s visual snapshot of test suite on success +<<<<<<< HEAD Tests: 6 skipped, 71 passed Time: 2.89s +======= + Tests: 6 skipped, 76 passed + Time: 3.32s +>>>>>>> 84c9078... Add basic container implementation 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) + { + } +} From 337e75120039f405291786d54c566ccdabde8df4 Mon Sep 17 00:00:00 2001 From: "johannes.pichler" Date: Fri, 5 Jun 2020 07:48:51 +0200 Subject: [PATCH 4/4] Update plugin interfaces and instantiate container --- bin/pest | 9 ++++++++- src/Console/Command.php | 4 ++-- src/Contracts/Plugins/AddsOutput.php | 5 +---- src/Contracts/Plugins/HandlesArguments.php | 4 +--- src/Support/Container.php | 17 +++++++++++++++++ tests/.snapshots/success.txt | 9 ++------- 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/bin/pest b/bin/pest index 9b66a2b6..ddb68f81 100755 --- a/bin/pest +++ b/bin/pest @@ -3,8 +3,10 @@ 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. @@ -24,8 +26,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/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 index bead50ed..85a5f9d2 100644 --- a/src/Support/Container.php +++ b/src/Support/Container.php @@ -13,11 +13,28 @@ use ReflectionParameter; */ final class Container { + /** + * @var self + */ + private static $instance; + /** * @var array */ 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. * diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index ae43a75c..0d97fd98 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -143,10 +143,5 @@ WARN Tests\Visual\Success s visual snapshot of test suite on success -<<<<<<< HEAD - Tests: 6 skipped, 71 passed - Time: 2.89s -======= - Tests: 6 skipped, 76 passed - Time: 3.32s ->>>>>>> 84c9078... Add basic container implementation + Tests: 6 skipped, 78 passed + Time: 3.37s