From faaf0af565dd8d71eca2b62b76566131b9a84731 Mon Sep 17 00:00:00 2001 From: mariano Date: Wed, 18 Sep 2019 01:14:13 +0200 Subject: [PATCH] Add production files. --- .gitignore | 74 ++++++ README.md | 121 +++++++++ composer.json | 33 +++ phpunit.xml | 18 ++ src/Container.php | 231 ++++++++++++++++++ src/DI.php | 28 +++ src/Exceptions/CircularReferenceException.php | 4 + src/Exceptions/ContainerException.php | 45 ++++ .../MissingRequiredParameterException.php | 4 + src/Exceptions/NotFoundException.php | 6 + src/Factory.php | 33 +++ tests/Benchmarks/.gitignore | 1 + tests/Benchmarks/README.md | 131 ++++++++++ tests/Benchmarks/benchmark1.php | 21 ++ tests/Benchmarks/benchmark2.php | 54 ++++ tests/Benchmarks/benchmark3.php | 61 +++++ tests/Benchmarks/benchmark4.php | 22 ++ tests/Benchmarks/benchmark5.php | 24 ++ tests/UnitTest/AliasTest.php | 20 ++ tests/UnitTest/CircularTest.php | 37 +++ tests/UnitTest/ClassOneTest.php | 47 ++++ tests/UnitTest/ClassThreeTest.php | 24 ++ tests/UnitTest/ClassTwoTest.php | 49 ++++ .../ClassWithOneMixedParameterTest.php | 20 ++ tests/UnitTest/DiTest.php | 20 ++ tests/UnitTest/InstantiationTest.php | 76 ++++++ tests/UnitTest/NoConstructorTest.php | 27 ++ tests/UnitTest/ParameterCallbackTest.php | 23 ++ tests/UnitTest/TestClasses/Bar.php | 11 + tests/UnitTest/TestClasses/Baz.php | 24 ++ tests/UnitTest/TestClasses/CircularA.php | 19 ++ tests/UnitTest/TestClasses/CircularB.php | 19 ++ tests/UnitTest/TestClasses/ClassOne.php | 52 ++++ tests/UnitTest/TestClasses/ClassThree.php | 40 +++ tests/UnitTest/TestClasses/ClassTwo.php | 54 ++++ .../ClassWithOneMixedParameter.php | 19 ++ .../TestClasses/ClassWithoutConstructor.php | 8 + tests/UnitTest/TestClasses/Foo.php | 27 ++ .../TestClasses/OneParamConstructor.php | 24 ++ 39 files changed, 1551 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100755 phpunit.xml create mode 100644 src/Container.php create mode 100644 src/DI.php create mode 100644 src/Exceptions/CircularReferenceException.php create mode 100644 src/Exceptions/ContainerException.php create mode 100644 src/Exceptions/MissingRequiredParameterException.php create mode 100644 src/Exceptions/NotFoundException.php create mode 100644 src/Factory.php create mode 100644 tests/Benchmarks/.gitignore create mode 100644 tests/Benchmarks/README.md create mode 100644 tests/Benchmarks/benchmark1.php create mode 100644 tests/Benchmarks/benchmark2.php create mode 100644 tests/Benchmarks/benchmark3.php create mode 100644 tests/Benchmarks/benchmark4.php create mode 100644 tests/Benchmarks/benchmark5.php create mode 100644 tests/UnitTest/AliasTest.php create mode 100644 tests/UnitTest/CircularTest.php create mode 100644 tests/UnitTest/ClassOneTest.php create mode 100644 tests/UnitTest/ClassThreeTest.php create mode 100644 tests/UnitTest/ClassTwoTest.php create mode 100644 tests/UnitTest/ClassWithOneMixedParameterTest.php create mode 100644 tests/UnitTest/DiTest.php create mode 100644 tests/UnitTest/InstantiationTest.php create mode 100644 tests/UnitTest/NoConstructorTest.php create mode 100644 tests/UnitTest/ParameterCallbackTest.php create mode 100644 tests/UnitTest/TestClasses/Bar.php create mode 100644 tests/UnitTest/TestClasses/Baz.php create mode 100644 tests/UnitTest/TestClasses/CircularA.php create mode 100644 tests/UnitTest/TestClasses/CircularB.php create mode 100644 tests/UnitTest/TestClasses/ClassOne.php create mode 100644 tests/UnitTest/TestClasses/ClassThree.php create mode 100644 tests/UnitTest/TestClasses/ClassTwo.php create mode 100644 tests/UnitTest/TestClasses/ClassWithOneMixedParameter.php create mode 100644 tests/UnitTest/TestClasses/ClassWithoutConstructor.php create mode 100644 tests/UnitTest/TestClasses/Foo.php create mode 100644 tests/UnitTest/TestClasses/OneParamConstructor.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd18617 --- /dev/null +++ b/.gitignore @@ -0,0 +1,74 @@ +# Created by .ignore support plugin (hsz.mobi) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +composer.lock +./vendor/ +.phpunit.result.cache diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff72d36 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# mdarc/DI +A simple yet powerful PSR-11 autowiring dependency injection container. + +**mdarc/DI** was conceived to be simple to configure and use. It was built +for performance. (check [benchmarks](tests/Benchmarks/README.md)) + +mdarc/DI has an very small but robust code base. It is **production ready** and can be used for small micro-services or large monolithic projects. + +##### Features: ##### +- Autowiring: Automatically instantiate and inject dependencies +- Manual configuration: When classes cannot be autowired, you can create them by yourself +- Circular reference detection: It throws a `CircularReferenceException` with enough details to fix the problem + +##### What mdarc/DI is not good for: ##### +- Autowiring via setter methods is not supported (and it will never be) +- Autowiring using phpDoc annotations is not supported (and it will never be) +- Automatically injecting dependencies on constructor parameters without type hints is not supported. You must manually configure those cases + +Installation +------------ + +### Composer ### + +Before anything else, use this to add it to your composer.json + +```shell script +$ composer require mdarc/di "^1.0" +``` + +Usage +----- +Creating a container ready to use with autowiring enabled is a matter of creating a `Container` instance: + +```php +use Mdarc\DI\Container; + +$container = new Container(); +``` + +If your classes contain **constructor parameters** that are other objects, then simply: +```php +$myClass = $container->get(\Path\To\MyClass::class); +``` +As any other DI container, `$myClass` will always get the same instance on the requested class. + +If you want to create a new object every time (instead of getting the same object instance) then use the **factory** helper: +```php +use Mdarc\DI\Container; +use Mdarc\DI\DI; + +$container = new Container([ + \Path\To\MyClass::class => DI::factory(function () { + return new \Path\To\MyClass(); + }), +]); + +$myClass = $container->get(\Path\To\MyClass::class); +``` + +For those classes that cannot be created using autowiring, then you can add their **definitions**: +```php +use Mdarc\DI\Container; + +$container = new Container([ + \Monolog\Logger::class => function (Container $c) { + $config = $c->get(\Path\To\Config::class); + $logger = new \Monolog\Logger($config->get('name')); + $logger->pushHandler(new \Monolog\Handler\StreamHandler('php://stdout', \Monolog\Logger::DEBUG)); + + return $logger; + }), +]); + +$logger = $container->get(\Monolog\Logger::class); +``` + +Defining **aliases** for binding interfaces to implementations is simple: +```php +use Mdarc\DI\Container; + +$container = new Container([ + // Binding Interface to implementation + \Psr\Log\LoggerInterface::class => \Monolog\Logger::class, + // Concrete implementation + \Monolog\Logger::class => function (Container $c) { + // build Monolog here + }), +]); + +$logger = $container->get(\Psr\Log\LoggerInterface::class); +``` + +Specifying **arguments** for classes with constructor parameters that are scalar, array or undefined type: +```php +use Mdarc\DI\Container; + +class MyClass { + public function __construct(array $config, \Psr\LoggerInterface $logger) { /*...*/ } +} + +$definitions = [ + // Binding Interface to implementation + \Psr\Log\LoggerInterface::class => \Monolog\Logger::class, + // Concrete implementation + \Monolog\Logger::class => function (Container $c) { + // build Monolog here + }, +]; + +$constructorParameters = [ + MyClass::class => [ + 'config' => ['an array', 'of relevant', 'things'] + ], +]; +$container = new Container($definitions, $constructorParameters); + +$myClass = $container->get(MyClass::class); +``` + +### License ### +mdarc/DI is licensed under the MIT License. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..95293ff --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name": "mdarc/di", + "description": "A simple yet powerful PSR-11 autowiring dependency injection container", + "type": "library", + "keywords": ["container", "di", "dependency injection", "ioc", "psr-11", "psr11", "autowiring"], + "prefer-stable": true, + "license": "MIT", + "homepage": "https://github.com/mdarc/DI", + "authors": [ + {"name": "Mariano Darc", "email": "espolete@gmail.com"} + ], + "require": { + "php": ">=7.1.0", + "psr/container": "^1.0", + "ext-reflection": "*" + }, + "require-dev": { + "phpunit/phpunit": "^7.5.16", + "jbzoo/profiler": "^1.0.5", + "pimple/pimple":"^3.2.0", + "php-di/php-di": "^6.0.0" + }, + "autoload": { + "psr-4": { + "Mdarc\\DI\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Mdarc\\DI\\Test\\UnitTest\\": "tests/UnitTest/" + } + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100755 index 0000000..8838f76 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/UnitTest + + + + + ./src + + + diff --git a/src/Container.php b/src/Container.php new file mode 100644 index 0000000..09b1204 --- /dev/null +++ b/src/Container.php @@ -0,0 +1,231 @@ +definitions = $definitions; + $this->constructorParameters = $constructorParameters; + } + + /** + * Find an entry of the container by its identifier and return it. + * + * @param string $id Identifier of the entry to look for. + * + * @return mixed Entry. + * + * @throws NotFoundExceptionInterface No entry was found for **this** identifier. + * @throws ContainerExceptionInterface Error while retrieving the entry. + */ + public function get($id) + { + // Was already built? + if (isset($this->instances[$id])) { + return $this->instances[$id]; + } + + // Let's build it + try { + $instance = $this->build($id); + } catch (ReflectionException $e) { + throw new NotFoundException("Exception building '{$id}': " . $e->getMessage(), 0, $e); + } + + if ($instance instanceof Factory) { + $newInstance = $instance->build($this); + // Decrement the circular reference counter + $this->circularReferenceMap[$id] -= 1; + + return $newInstance; + } + + $this->instances[$id] = $instance; + + return $instance; + } + + /** + * @param string $id + * + * @return mixed + * + * @throws CircularReferenceException + * @throws MissingRequiredParameterException + * @throws ReflectionException + */ + private function build(string $id) + { + // is it in the definitions? + if (isset($this->definitions[$id])) { + + // is it an alias of another class? + if (is_string($this->definitions[$id])) { + return $this->get($this->definitions[$id]); + } + + // is this a factory? then it should return a new object always + if ($this->definitions[$id] instanceof Factory) { + if (!isset($this->circularReferenceMap[$id])) { + $this->circularReferenceMap[$id] = 0; + } + if ($this->circularReferenceMap[$id] >= 1) { + throw new CircularReferenceException( + "Factory for object '{$id}' has a circular reference with itself", 0, '', $id + ); + } + // Increment the circular reference counter + $this->circularReferenceMap[$id] += 1; + return $this->definitions[$id]; + } + + // build it + return $this->definitions[$id]($this); + } + + // let's build it using reflection + $reflectionClass = new ReflectionClass($id); + $constructor = $reflectionClass->getConstructor(); + if ($constructor === null) { + return $reflectionClass->newInstance(); + } + + $params = $constructor->getParameters(); + if (empty($params)) { + return $reflectionClass->newInstance(); + } + + // Parameters validation + foreach ($params as $parameter) { + if ($parameter->isOptional()) { + continue; + } + $pname = $parameter->getName(); + if (isset($this->constructorParameters[$id][$pname])) { + continue; + } + + $ptype = $parameter->getType(); + if ($ptype === null) { // no defined type + throw new MissingRequiredParameterException( + "Constructor parameter without type '\${$pname}' in class '{$id}' cannot be guessed and must be manually defined", + 0, + $pname, + $id + ); + } + + if ($this->isInternalType($ptype->getName())) { + throw new MissingRequiredParameterException( + sprintf( + "Constructor parameter '%s \$%s' in class '%s' cannot be guessed and must be manually defined", $ptype->getName(), $pname, $id + ), + 0, + $pname, + $id + ); + } + } + + $args = []; + foreach ($params as $parameter) { + $pname = $parameter->getName(); + if ($parameter->isOptional()) { + if (!isset($this->constructorParameters[$id][$pname])) { + $args[] = $parameter->getDefaultValue(); + continue; + } + } + $ptype = $parameter->getType(); + if ($ptype === null || $this->isInternalType($ptype->getName())) { + $val = $this->constructorParameters[$id][$pname]; + if (is_callable($val)) { + $val = $val($this); + } + $args[] = $val; + continue; + } + + $dependencyClassName = $parameter->getType()->getName(); + + // Circular reference check + if (isset($this->circularReferenceMap[$dependencyClassName][$id])) { + throw new CircularReferenceException( + "Parameter '" . $ptype->getName() . " \${$pname}' in class {$id} has a circular reference with class {$dependencyClassName}", 0, $pname, $id + ); + } + $this->circularReferenceMap[$id][$dependencyClassName] = 1; + + // Getting the reference + $args[] = $this->get($dependencyClassName); + } + + return $reflectionClass->newInstanceArgs($args); + } + + /** + * @param string $typeName + * + * @return bool + */ + private function isInternalType(string $typeName): bool + { + return in_array($typeName, ['array', 'int', 'string', 'float']); + } + + /** + * Returns true if the container can return an entry for the given identifier. + * Returns false otherwise. + * + * `has($id)` returning true does not mean that `get($id)` will not throw an exception. + * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. + * + * @param string $id Identifier of the entry to look for. + * + * @return bool + */ + public function has($id) + { + // We do our best. + return true; + } + + /** + * @param string $id + * @param mixed $definition + */ + public function set(string $id, $definition) + { + $this->instances[$id] = $definition; + } +} diff --git a/src/DI.php b/src/DI.php new file mode 100644 index 0000000..58d8b27 --- /dev/null +++ b/src/DI.php @@ -0,0 +1,28 @@ +parameter = $parameter; + $this->class = $class; + } + + /** + * @return string + */ + public function getParameter(): string + { + return $this->parameter; + } + + /** + * @return string + */ + public function getClass(): string + { + return $this->class; + } +} diff --git a/src/Exceptions/MissingRequiredParameterException.php b/src/Exceptions/MissingRequiredParameterException.php new file mode 100644 index 0000000..06790be --- /dev/null +++ b/src/Exceptions/MissingRequiredParameterException.php @@ -0,0 +1,4 @@ +object = $object; + } + + /** + * @param ContainerInterface|null $container + * + * @return mixed + */ + public function build(?ContainerInterface $container) + { + $exec = $this->object; + return $exec($container); + } +} diff --git a/tests/Benchmarks/.gitignore b/tests/Benchmarks/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/tests/Benchmarks/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/tests/Benchmarks/README.md b/tests/Benchmarks/README.md new file mode 100644 index 0000000..311837e --- /dev/null +++ b/tests/Benchmarks/README.md @@ -0,0 +1,131 @@ +# mdarc/DI + +This is a set of performance comparisons between mdarc/DI, [Pimple](https://pimple.symfony.com/) and [PHP-DI](http://php-di.org). + +These tests were executed in the following environment: + +| | | +|---|---| +|OS | Ubuntu 19.04 desktop 64-bit | +|PHP | PHP 7.3.9-1+ubuntu19.04.1+deb.sury.org+1 (cli) (built: Sep 2 2019 12:54:43) ( NTS ) | +|Processor | Intel® Core™ i7-8550U CPU @ 1.80GHz × 8 | +|Memory | 15,5 GiB | + + +## Benchmark results ## + +The first benchmark is about the time it take just to create a new instance of the Container +class without any configuration. +``` +$ php benchmark1.php + +--------------- Start compare: Class Instantiation --------------- +Running tests 100000 times +PHP Overhead: time=173 ms; memory=47.95 KB; + +Testing 1/3 : pimple ... Done! +Testing 2/3 : php-di ... Done! +Testing 3/3 : mdarc/DI ... Done! + +Name of test Time Time one Time, % Memory leak Memory, % +mdarc/DI 205 ms 0.002 ms 100 0 B 100 +pimple 264 ms 0.003 ms 129 0 B 100 +php-di 1 125 ms 0.011 ms 549 132.79 KB 279 + +TOTAL: Time: 1.77 secs; Memory: 601.44 KB +-------------------- Finish compare: Class Instantiation --------- +``` + +The following benchmark is instantiation of the Container, simple configuration and fetching +one object from it (no autowiring) for the first time. +``` +$ php benchmark2.php + +--------------- Start compare: Fetch an instance of simple class without dependencies - First time --------------- +Running tests 100000 times +PHP Overhead: time=191 ms; memory=47.95 KB; + +Testing 1/4 : pimple ... Done! +Testing 2/4 : php-di ... Done! +Testing 3/4 : php-di (compilation enabled) ... Done! +Testing 4/4 : mdarc/DI ... Done! + +Name of test Time Time one Time, % Memory leak Memory, % +mdarc/DI 524 ms 0.005 ms 100 0 B 100 +pimple 682 ms 0.007 ms 130 0 B 100 +php-di (compilation enabled) 5 027 ms 0.050 ms 959 388.40 KB 817 +php-di 5 508 ms 0.055 ms 1 050 468.38 KB 986 + +TOTAL: Time: 11.95 secs; Memory: 4.59 MB +-------------------- Finish compare: Fetch an instance of simple class without dependencies - First time --------- +``` + +The following benchmark measures the time of an already configured container +and fetching one object from it (no autowiring) for the second time time. +``` +$ php benchmark3.php + +--------------- Start compare: Fetch an instance of simple class without dependencies - Second time --------------- +Running tests 1000000 times +PHP Overhead: time=1 627 ms; memory=47.85 KB; + +Testing 1/4 : pimple ... Done! +Testing 2/4 : php-di ... Done! +Testing 3/4 : php-di (compilation enabled) ... Done! +Testing 4/4 : mdarc/DI ... Done! + +Name of test Time Time one Time, % Memory leak Memory, % +mdarc/DI 2 136 ms 0.002 ms 100 0 B 100 +php-di (compilation enabled) 2 172 ms 0.002 ms 102 0 B 100 +php-di 2 345 ms 0.002 ms 110 0 B 100 +pimple 2 458 ms 0.002 ms 115 0 B -100 + +TOTAL: Time: 10.73 secs; Memory: 0 B +-------------------- Finish compare: Fetch an instance of simple class without dependencies - Second time --------- +``` + +The following benchmark measures the time that PHP-DI and mdarc/DI +take for fetching an object for the first time using autowiring (Reflection) +``` +$ php benchmark4.php + +--------------- Start compare: Autowiring - Fetch an instance of simple class without dependencies - First time --------------- +Running tests 100000 times +PHP Overhead: time=181 ms; memory=47.95 KB; + +Testing 1/2 : php-di (development mode) ... Done! +Testing 2/2 : mdarc/DI ... Done! + +Name of test Time Time one Time, % Memory leak Memory, % +mdarc/DI 500 ms 0.005 ms 100 0 B 100 +php-di (development mode) 3 689 ms 0.037 ms 738 247.67 KB 521 + +TOTAL: Time: 4.37 secs; Memory: 642.68 KB +-------------------- Finish compare: Autowiring - Fetch an instance of simple class without dependencies - First time --------- +``` + +The following benchmark measures the time that mdarc/DI +takes for fetching an object for the first time by using a definition vs. autowiring (Reflection). + +``` +$ php benchmark5.php + +--------------- Start compare: Defined class vs. Autowiring --------------- +Running tests 100000 times +PHP Overhead: time=172 ms; memory=47.95 KB; + +Testing 1/2 : mdarc/DI (class defined in constructor) ... Done! +Testing 2/2 : mdarc/DI (autowiring - aka Reflection) ... Done! + +Name of test Time Time one Time, % Memory leak Memory, % +mdarc/DI (autowiring - aka Reflection) 451 ms 0.005 ms 100 0 B 100 +mdarc/DI (class defined in constructor) 488 ms 0.005 ms 108 0 B -100 + +TOTAL: Time: 1.11 secs; Memory: 168.45 KB +-------------------- Finish compare: Defined class vs. Autowiring --------- +``` +*Interestingly enough in this previous test we see that using Reflection is a bit faster than not using it at all.* +*This peculiarity is due to optimizations in the PHP 7 runtime.* + +#### Are these tests wrongly setup or not reflecting the correct numbers? #### +Please, help us by contributing a **pull request** diff --git a/tests/Benchmarks/benchmark1.php b/tests/Benchmarks/benchmark1.php new file mode 100644 index 0000000..45135a4 --- /dev/null +++ b/tests/Benchmarks/benchmark1.php @@ -0,0 +1,21 @@ + function () { + $pimple = new \Pimple\Container([]); + }, + 'php-di' => function () { + $builder = new ContainerBuilder(); + $phpDi = $builder->build(); + }, + 'mdarc/DI' => function () { + $container = new Container(); + }, +], ['count' => 100000, 'name' => 'Class Instantiation']); diff --git a/tests/Benchmarks/benchmark2.php b/tests/Benchmarks/benchmark2.php new file mode 100644 index 0000000..3528b6c --- /dev/null +++ b/tests/Benchmarks/benchmark2.php @@ -0,0 +1,54 @@ + function () { + $pimple = new \Pimple\Container([ + ClassWithoutConstructor::class => function ($c) { + return new ClassWithoutConstructor(); + } + ]); + $classWithoutConstructor = $pimple[ClassWithoutConstructor::class]; + }, + 'php-di' => function () { + $builder = new ContainerBuilder(); + $builder->addDefinitions([ + ClassWithoutConstructor::class => function (ContainerInterface $c) { + return new ClassWithoutConstructor(); + } + ]); + $phpDi = $builder->build(); + $classWithoutConstructor = $phpDi->get(ClassWithoutConstructor::class); + }, + 'php-di (compilation enabled)' => function () { + $builder = new ContainerBuilder(); + $builder->addDefinitions([ + ClassWithoutConstructor::class => function (ContainerInterface $c) { + return new ClassWithoutConstructor(); + } + ]); + $builder->enableCompilation(__DIR__ . '/tmp'); + $phpDi = $builder->build(); + $classWithoutConstructor = $phpDi->get(ClassWithoutConstructor::class); + }, + 'mdarc/DI' => function () { + $container = new Container([ + ClassWithoutConstructor::class => function (ContainerInterface $c) { + return new ClassWithoutConstructor(); + } + ]); + $classWithoutConstructor = $container->get(ClassWithoutConstructor::class); + }, +], ['count' => 100000, 'name' => 'Fetch an instance of simple class without dependencies - First time']); diff --git a/tests/Benchmarks/benchmark3.php b/tests/Benchmarks/benchmark3.php new file mode 100644 index 0000000..ea17954 --- /dev/null +++ b/tests/Benchmarks/benchmark3.php @@ -0,0 +1,61 @@ + function ($c) { + return new ClassWithoutConstructor(); + } +]); +$classWithoutConstructor = $pimple[ClassWithoutConstructor::class]; +// +$builder = new ContainerBuilder(); +$builder->addDefinitions([ + ClassWithoutConstructor::class => function (ContainerInterface $c) { + return new ClassWithoutConstructor(); + } +]); +$phpDi = $builder->build(); +$classWithoutConstructor = $phpDi->get(ClassWithoutConstructor::class); +// +if (file_exists(__DIR__ . '/tmp/CompiledContainer.php')) { + @unlink(__DIR__ . '/tmp/CompiledContainer.php'); + @rmdir(__DIR__ . '/tmp'); +} +$builder = new ContainerBuilder(); +$builder->addDefinitions([ + ClassWithoutConstructor::class => function (ContainerInterface $c) { + return new ClassWithoutConstructor(); + } +]); +$builder->enableCompilation(__DIR__ . '/tmp'); +$phpDiCompiled = $builder->build(); +$classWithoutConstructor = $phpDiCompiled->get(ClassWithoutConstructor::class); +// +$container = new Container([ + ClassWithoutConstructor::class => function (ContainerInterface $c) { + return new ClassWithoutConstructor(); + } +]); +$classWithoutConstructor = $container->get(ClassWithoutConstructor::class); + +Benchmark::compare([ + 'pimple' => function () use ($pimple) { + $classWithoutConstructor = $pimple[ClassWithoutConstructor::class]; + }, + 'php-di' => function () use ($phpDi) { + $classWithoutConstructor = $phpDi->get(ClassWithoutConstructor::class); + }, + 'php-di (compilation enabled)' => function () use ($phpDiCompiled) { + $classWithoutConstructor = $phpDiCompiled->get(ClassWithoutConstructor::class); + }, + 'mdarc/DI' => function () use ($container) { + $classWithoutConstructor = $container->get(ClassWithoutConstructor::class); + }, +], ['count' => 1000000, 'name' => 'Fetch an instance of simple class without dependencies - Second time']); diff --git a/tests/Benchmarks/benchmark4.php b/tests/Benchmarks/benchmark4.php new file mode 100644 index 0000000..184460d --- /dev/null +++ b/tests/Benchmarks/benchmark4.php @@ -0,0 +1,22 @@ + function () { + $builder = new ContainerBuilder(); + $builder->addDefinitions(); + $phpDi = $builder->build(); + $classWithoutConstructor = $phpDi->get(ClassWithoutConstructor::class); + }, + 'mdarc/DI' => function () { + $container = new Container(); + $classWithoutConstructor = $container->get(ClassWithoutConstructor::class); + }, +], ['count' => 100000, 'name' => 'Autowiring - Fetch an instance of simple class without dependencies - First time']); diff --git a/tests/Benchmarks/benchmark5.php b/tests/Benchmarks/benchmark5.php new file mode 100644 index 0000000..4376045 --- /dev/null +++ b/tests/Benchmarks/benchmark5.php @@ -0,0 +1,24 @@ + function () { + $DiContainer = new Container([ + ClassWithoutConstructor::class => function (ContainerInterface $c) { + return new ClassWithoutConstructor(); + } + ]); + $classWithoutConstructor = $DiContainer->get(ClassWithoutConstructor::class); + }, + 'mdarc/DI (autowiring - aka Reflection)' => function () { + $DiContainer = new Container(); + $classWithoutConstructor = $DiContainer->get(ClassWithoutConstructor::class); + }, +], ['count' => 100000, 'name' => 'Defined class vs. Autowiring']); diff --git a/tests/UnitTest/AliasTest.php b/tests/UnitTest/AliasTest.php new file mode 100644 index 0000000..946bfac --- /dev/null +++ b/tests/UnitTest/AliasTest.php @@ -0,0 +1,20 @@ + Foo::class + ]; + $container = new Container($definitions); + + /** @var Foo $object */ + $object = $container->get('pepe'); + $this->assertInstanceOf(Foo::class, $object); + } +} diff --git a/tests/UnitTest/CircularTest.php b/tests/UnitTest/CircularTest.php new file mode 100644 index 0000000..92dc1db --- /dev/null +++ b/tests/UnitTest/CircularTest.php @@ -0,0 +1,37 @@ +expectException(CircularReferenceException::class); + $this->expectExceptionMessage("Parameter 'Mdarc\DI\Test\UnitTest\TestClasses\CircularA \$circularA' in class Mdarc\DI\Test\UnitTest\TestClasses\CircularB has a circular reference with class Mdarc\DI\Test\UnitTest\TestClasses\CircularA"); + + $container = new Container(); + $container->get(CircularA::class); + } + + public function test_CircularReferencesInFactoryThrowException() + { + $this->expectException(CircularReferenceException::class); + $this->expectExceptionMessage("Factory for object 'Mdarc\DI\Test\UnitTest\TestClasses\Baz' has a circular reference with itself"); + + $container = new Container([ + Baz::class => DI::factory(function(ContainerInterface $container) { + $baz = $container->get(Baz::class); + // Not doing anything with $baz. Just for the test. + return new Baz(); + }) + ]); + + $container->get(Baz::class); + } +} diff --git a/tests/UnitTest/ClassOneTest.php b/tests/UnitTest/ClassOneTest.php new file mode 100644 index 0000000..710c05b --- /dev/null +++ b/tests/UnitTest/ClassOneTest.php @@ -0,0 +1,47 @@ + [ + 'param1' => 1, + 'param2' => '2', + 'param3' => [3], + ] + ]; + $container = new Container([], $constructorParameters); + + /** @var ClassOne $class1 */ + $class1 = $container->get(ClassOne::class); + + $this->assertInstanceOf(ClassOne::class, $class1); + $this->assertIsInt($class1->getParam1()); + $this->assertIsString($class1->getParam2()); + $this->assertIsArray($class1->getParam3()); + } + + public function test_MissingParameterThrowException() + { + $this->expectException(MissingRequiredParameterException::class); + $this->expectExceptionMessage("Constructor parameter 'string \$param2' in class '" . ClassOne::class . "' cannot be guessed and must be manually defined"); + + $constructorParameters = [ + ClassOne::class => [ + 'param1' => 1, + // param2 is missing on purpose + 'param3' => [3], + ] + ]; + $container = new Container([], $constructorParameters); + + /** @var ClassOne $class1 */ + $container->get(ClassOne::class); + } +} diff --git a/tests/UnitTest/ClassThreeTest.php b/tests/UnitTest/ClassThreeTest.php new file mode 100644 index 0000000..130323a --- /dev/null +++ b/tests/UnitTest/ClassThreeTest.php @@ -0,0 +1,24 @@ +get(ClassThree::class); + $this->assertInstanceOf(ClassThree::class, $class3); + $this->assertInstanceOf(Foo::class, $class3->getFoo()); + $this->assertInstanceOf(Bar::class, $class3->getBar()); + $this->assertInstanceOf(Baz::class, $class3->getFoo()->getBaz()); + $this->assertEquals('the lazy fox', $class3->getFoo()->getBaz()->getText()); + } +} diff --git a/tests/UnitTest/ClassTwoTest.php b/tests/UnitTest/ClassTwoTest.php new file mode 100644 index 0000000..adfdc2f --- /dev/null +++ b/tests/UnitTest/ClassTwoTest.php @@ -0,0 +1,49 @@ + [ + 'param1' => 1, + 'param2' => new \StdClass, + // param3 should be optional so is not defined + ] + ]; + $container = new Container([], $constructorParameters); + + /** @var ClassTwo $class2 */ + $class2 = $container->get(ClassTwo::class); + + $this->assertInstanceOf(ClassTwo::class, $class2); + $this->assertIsInt($class2->getParam1()); + $this->assertIsObject($class2->getParam2()); + $this->assertIsArray($class2->getParam3()); + $this->assertEquals(['default'], $class2->getParam3()); + } + + public function test_MissingParameterThrowException() + { + $this->expectException(MissingRequiredParameterException::class); + $this->expectExceptionMessage("Constructor parameter without type '\$param2' in class '" . ClassTwo::class . "' cannot be guessed and must be manually defined"); + + $constructorParameters = [ + ClassTwo::class => [ + 'param1' => 1, + // param2 is missing on purpose + 'param3' => ['something'], + ] + ]; + $container = new Container([], $constructorParameters); + + /** @var ClassOne $class1 */ + $container->get(ClassTwo::class); + } +} diff --git a/tests/UnitTest/ClassWithOneMixedParameterTest.php b/tests/UnitTest/ClassWithOneMixedParameterTest.php new file mode 100644 index 0000000..a15108c --- /dev/null +++ b/tests/UnitTest/ClassWithOneMixedParameterTest.php @@ -0,0 +1,20 @@ + [ + 'mixedParameter' => 'pepe' + ] + ]); + + $classWithOneMixedParameter = $container->get(ClassWithOneMixedParameter::class); + $this->assertInstanceOf(ClassWithOneMixedParameter::class, $classWithOneMixedParameter); + } +} diff --git a/tests/UnitTest/DiTest.php b/tests/UnitTest/DiTest.php new file mode 100644 index 0000000..92d190d --- /dev/null +++ b/tests/UnitTest/DiTest.php @@ -0,0 +1,20 @@ +assertInstanceOf(Container::class, $container); + $this->assertInstanceOf(\Psr\Container\ContainerInterface::class, $container); + } + + public function testFactory() + { + $factoryClass = \Mdarc\DI\DI::factory(function () {}); + $this->assertInstanceOf(\Mdarc\DI\Factory::class, $factoryClass); + } +} diff --git a/tests/UnitTest/InstantiationTest.php b/tests/UnitTest/InstantiationTest.php new file mode 100644 index 0000000..62b5b01 --- /dev/null +++ b/tests/UnitTest/InstantiationTest.php @@ -0,0 +1,76 @@ +get(Foo::class); + /** @var Foo $fooAnotherInstance */ + $fooAnotherInstance = $container->get(Foo::class); + + $this->assertInstanceOf(Foo::class, $foo); + $this->assertInstanceOf(Foo::class, $fooAnotherInstance); + $this->assertSame($foo, $fooAnotherInstance); + } + + public function test_Factory() + { + $container = new Container([ + Baz::class => DI::factory(function() { + return new Baz(); + }) + ]); + + $baz = $container->get(Baz::class); + $anotherBar = $container->get(Baz::class); + $this->assertInstanceOf(Baz::class, $baz); + $this->assertInstanceOf(Baz::class, $anotherBar); + $this->assertNotSame($baz, $anotherBar); + } + + public function test_NonExistingClassThrowsException() + { + $this->expectException(NotFoundException::class); + $this->expectExceptionMessage("Exception building 'pepe': Class pepe does not exist"); + + $container = new Container(); + $container->get('pepe'); + } + + public function test_InstantiateWithoutWiring() + { + $container = new Container([ + 'a_baz' => function() { + return new Baz(); + }, + ]); + + $baz = $container->get('a_baz'); + $this->assertInstanceOf(Baz::class, $baz); + } + + public function test_Has() + { + $container = new Container(); + $this->assertTrue($container->has("something")); + } + + public function test_Set() + { + $container = new Container(); + $container->set(Baz::class, new Baz()); + + $baz = $container->get(Baz::class); + $this->assertInstanceOf(Baz::class, $baz); + } +} diff --git a/tests/UnitTest/NoConstructorTest.php b/tests/UnitTest/NoConstructorTest.php new file mode 100644 index 0000000..e3a31f3 --- /dev/null +++ b/tests/UnitTest/NoConstructorTest.php @@ -0,0 +1,27 @@ +get(Bar::class); + $this->assertInstanceOf(Bar::class, $object); + } + + public function test_ClassWithoutConstructor() + { + $container = new Container(); + + /** @var ClassWithoutConstructor $object */ + $object = $container->get(ClassWithoutConstructor::class); + $this->assertInstanceOf(ClassWithoutConstructor::class, $object); + } +} diff --git a/tests/UnitTest/ParameterCallbackTest.php b/tests/UnitTest/ParameterCallbackTest.php new file mode 100644 index 0000000..4ceaf09 --- /dev/null +++ b/tests/UnitTest/ParameterCallbackTest.php @@ -0,0 +1,23 @@ + [ + 'pepe' => function () { return date('Y-m-d'); }, + ] + ]; + $container = new Container([], $constructorParameters); + + /** @var OneParamConstructor $object */ + $object = $container->get(OneParamConstructor::class); + $this->assertInstanceOf(OneParamConstructor::class, $object); + $this->assertEquals(date('Y-m-d'), $object->getPepe()); + } +} diff --git a/tests/UnitTest/TestClasses/Bar.php b/tests/UnitTest/TestClasses/Bar.php new file mode 100644 index 0000000..8777180 --- /dev/null +++ b/tests/UnitTest/TestClasses/Bar.php @@ -0,0 +1,11 @@ +text = 'the lazy fox'; + } + + /** + * @return string + */ + public function getText(): string + { + return $this->text; + } +} diff --git a/tests/UnitTest/TestClasses/CircularA.php b/tests/UnitTest/TestClasses/CircularA.php new file mode 100644 index 0000000..5b4c440 --- /dev/null +++ b/tests/UnitTest/TestClasses/CircularA.php @@ -0,0 +1,19 @@ +circularB = $circularB; + } +} diff --git a/tests/UnitTest/TestClasses/CircularB.php b/tests/UnitTest/TestClasses/CircularB.php new file mode 100644 index 0000000..d9efe85 --- /dev/null +++ b/tests/UnitTest/TestClasses/CircularB.php @@ -0,0 +1,19 @@ +circularA = $circularA; + } +} diff --git a/tests/UnitTest/TestClasses/ClassOne.php b/tests/UnitTest/TestClasses/ClassOne.php new file mode 100644 index 0000000..830825e --- /dev/null +++ b/tests/UnitTest/TestClasses/ClassOne.php @@ -0,0 +1,52 @@ +param1 = $param1; + $this->param2 = $param2; + $this->param3 = $param3; + } + + /** + * @return int + */ + public function getParam1(): int + { + return $this->param1; + } + + /** + * @return string + */ + public function getParam2(): string + { + return $this->param2; + } + + /** + * @return array + */ + public function getParam3(): array + { + return $this->param3; + } +} diff --git a/tests/UnitTest/TestClasses/ClassThree.php b/tests/UnitTest/TestClasses/ClassThree.php new file mode 100644 index 0000000..90ad563 --- /dev/null +++ b/tests/UnitTest/TestClasses/ClassThree.php @@ -0,0 +1,40 @@ +foo = $foo; + $this->bar = $bar; + } + + /** + * @return Foo + */ + public function getFoo(): Foo + { + return $this->foo; + } + + /** + * @return Bar + */ + public function getBar(): Bar + { + return $this->bar; + } +} diff --git a/tests/UnitTest/TestClasses/ClassTwo.php b/tests/UnitTest/TestClasses/ClassTwo.php new file mode 100644 index 0000000..0542bcd --- /dev/null +++ b/tests/UnitTest/TestClasses/ClassTwo.php @@ -0,0 +1,54 @@ +param1 = $param1; + $this->param2 = $param2; + $this->param3 = $param3; + } + + /** + * @return int + */ + public function getParam1(): int + { + return $this->param1; + } + + /** + * @return mixed + */ + public function getParam2() + { + return $this->param2; + } + + /** + * @return array + */ + public function getParam3(): array + { + return $this->param3; + } + +} diff --git a/tests/UnitTest/TestClasses/ClassWithOneMixedParameter.php b/tests/UnitTest/TestClasses/ClassWithOneMixedParameter.php new file mode 100644 index 0000000..e001bc9 --- /dev/null +++ b/tests/UnitTest/TestClasses/ClassWithOneMixedParameter.php @@ -0,0 +1,19 @@ +mixedParameter = $mixedParameter; + } +} diff --git a/tests/UnitTest/TestClasses/ClassWithoutConstructor.php b/tests/UnitTest/TestClasses/ClassWithoutConstructor.php new file mode 100644 index 0000000..cd8d3e7 --- /dev/null +++ b/tests/UnitTest/TestClasses/ClassWithoutConstructor.php @@ -0,0 +1,8 @@ +baz = $baz; + } + + /** + * @return Baz + */ + public function getBaz(): Baz + { + return $this->baz; + } +} diff --git a/tests/UnitTest/TestClasses/OneParamConstructor.php b/tests/UnitTest/TestClasses/OneParamConstructor.php new file mode 100644 index 0000000..c64098d --- /dev/null +++ b/tests/UnitTest/TestClasses/OneParamConstructor.php @@ -0,0 +1,24 @@ +pepe = $pepe; + } + + public function getPepe(): string + { + return $this->pepe; + } +}