diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c2431df --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,82 @@ +name: "CI" + +on: + pull_request: + push: + +permissions: + contents: read + +jobs: + tests: + name: "Test on PHP ${{ matrix.php-version }}" + + runs-on: 'ubuntu-latest' + + continue-on-error: true + + strategy: + matrix: + php-version: + - '7.2' + - '7.3' + - '7.4' + - '8.0' + - '8.1' + - '8.2' + + steps: + - name: "Checkout code" + uses: actions/checkout@v3 + + - name: "Install PHP with extensions" + uses: shivammathur/setup-php@v2 + with: + coverage: "none" + php-version: ${{ matrix.php-version }} + ini-values: memory_limit=-1 + + - name: "Add PHPUnit matcher" + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - run: composer install + + - name: "Install PHPUnit" + run: vendor/bin/simple-phpunit install + + - name: "PHPUnit version" + run: vendor/bin/simple-phpunit --version + + - name: "Run tests" + run: vendor/bin/simple-phpunit + + + + benchmark: + name: "Benchmark on PHP ${{ matrix.php-version }}" + + runs-on: 'ubuntu-latest' + + continue-on-error: true + + strategy: + matrix: + php-version: + - '7.4' + - '8.2' + + steps: + - name: "Checkout code" + uses: actions/checkout@v3 + + - name: "Install PHP with extensions" + uses: shivammathur/setup-php@v2 + with: + coverage: "none" + php-version: ${{ matrix.php-version }} + ini-values: memory_limit=-1 + + - run: composer install + + - name: Execute benchmarks + run: vendor/bin/phpbench run \ No newline at end of file diff --git a/.gitignore b/.gitignore index 61064cb..3e06538 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ vendor/ composer.lock .idea +/.phpunit.result.cache +/composer.phar diff --git a/.php_cs b/.php_cs index 0f1e9ba..58f9369 100644 --- a/.php_cs +++ b/.php_cs @@ -1,15 +1,14 @@ level(Symfony\CS\FixerInterface::NONE_LEVEL) - ->fixers([ - "-psr0", - "psr2", - "psr4", - ]) - ->finder( - Symfony\CS\Finder\DefaultFinder::create() - ->in(__DIR__ . "/lib") - ->in(__DIR__ . "/test") - ) -; + +$finder = PhpCsFixer\Finder::create() + ->in(__DIR__ . "/lib") + ->in(__DIR__ . "/test"); + +$config = new PhpCsFixer\Config(); + +return $config + ->setRules([ + '@PSR2' => true, +]) + ->setFinder($finder); diff --git a/README.md b/README.md index 23237e9..12be920 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,29 @@ -# auryn [![Build Status](https://travis-ci.org/rdlowrey/auryn.svg?branch=master)](https://travis-ci.org/rdlowrey/auryn) +# auryn [![Build Status](https://github.com/rdlowrey/Auryn/actions/workflows/ci.yml/badge.svg?branch=adding_ci)](https://github.com/rdlowrey/Auryn/actions) auryn is a recursive dependency injector. Use auryn to bootstrap and wire together S.O.L.I.D., object-oriented PHP applications. +## Maintenance status + +`rdlowrey/auryn` is in low maintenance mode. i.e. new features are very unlikely to be added, and +new releases to support new versions of PHP are not guaranteed to be timely. Notes on why some +features were not added to Auryn are listed [here](excluded_features.md). + +There are similar libraries available at: + +* [`martin-hughes/auryn`](https://github.com/martin-hughes/auryn) is a fork from this repo + and maintains the current namespace and interfaces. It is unlikely to introduce significant new + features, instead focussing on bugfixes and testing. + +* [`overclokk/auryn`](https://github.com/overclokk/auryn) is a fork from this repo + and maintains the current namespace and interfaces. It has added the ability to lazy + instantiate dependencies using + [`Ocramius/ProxyManager`](https://github.com/Ocramius/ProxyManager). + +* [`amphp/injector`](https://github.com/amphp/injector) is a significant rewrite using a new + namespace and slightly different interfaces, requiring you to update your code. It will + introduce new features and diverge over time from this repo. + ##### How It Works Among other things, auryn recursively instantiates class dependencies based on the parameter @@ -72,6 +93,27 @@ Archived tagged release versions are also available for manual download on the p [tags page](https://github.com/rdlowrey/auryn/tags) +##### Running tests + +To allow an appropriate version of PHPUnit to be installed across all of the supported +versions of PHP, instead of directly depending on PHPUnit, Auryn instead depends on +simple-phpunit. + +After doing composer update, you need to tell simple-phpunit to install PHPUnit: + +```bash +vendor/bin/simple-phpunit install + +vendor/bin/simple-phpunit --version +``` + +The tests can then be run with the command: + +```bash +vendor/bin/simple-phpunit +``` + + ## Basic Usage To start using the injector, simply create a new instance of the `Auryn\Injector` ("the Injector") @@ -393,7 +435,7 @@ Because we specified a global definition for `myValue`, all parameters that are way defined (as below) that match the specified parameter name are auto-filled with the global value. If a parameter matches any of the following criteria the global value is not used: -- A typehint +- A parameter type - A predefined injection definition - A custom call time definition @@ -580,6 +622,35 @@ var_dump($myObj->myProperty); // int(42) While the above example is contrived, the usefulness should be clear. +Additionally, the prepare method is able to replace the object being prepared with another of the same or descendant type: + +```php +prepare(FooGreeter::class, function($myObj, $injector) { + return new BarGreeter(); +}); + +$myObj = $injector->make(FooGreeter::class); +echo $myObj->getMessage(); // Output is: "Hello, I am bar." +``` +The usefulness of this is much less clear. + +Any value returned that is not the same or descendant type will be ignored. ### Injecting for Execution @@ -622,6 +693,40 @@ $injector = new Auryn\Injector; var_dump($injector->execute('Example::myMethod', $args = [':arg2' => 42])); ``` +### Injector::make and Injector::execute custom args + +The args parameter in both of Injector::make($name, array $args = array()) and Injector::execute($callableOrMethodStr, array $args = array())) allow you to pass in a bespoke set of parameters to be used during the creation/execution. + +The rules for how those injector args are used is as follows. + +Given a parameter named 'foo' at parameter position 'i' which has a type of 'bar', for the thing being created/executed: + +1. If an integer indexed key 'i' is present (i.e. does `$args[$i]` exist?) then use the value of `$args[$i]` directly for that parameter. + +2. If an string indexed key 'foo' is present (i.e. does `$args['foo']` exist?) then use the value of `$args['foo']` for that parameter. + +3. If a string indexed key `Injector::A_DELEGATE . 'foo'` is present (i.e. does `$args['+foo']` exist?) then interpret `$args['+' . $i]` as a delegate callable to be invoked, and the return value to be used for that parameter. + +4. If a string indexed key `Injector::A_DEFINE . 'foo'` is present (i.e. does `$args['@foo']` exist?) then interpret `$args['+' . $i]` as an array with + +``` +$params = [ + PrefixDefineDependency::class, + [Injector::A_RAW . 'message' => $message] +]; + +$object = $injector->make( + PrefixDefineTest::class, + [Injector::A_DEFINE . 'pdd' => $params] +); +``` +i.e. when the injector is making the class `'PrefixDefineTest` which has a dependency on the class `PrefixDefineDependency`, which is named as parameter 'pdd' in the constructor, use the values in the array `$params[1]`, to instantiate the `PrefixDefineDependency` class. + + +5. If a string indexed key `Injector::A_DEFINE . '+foo'` is present (i.e. does `$args[':foo']` exist?) then interpret `$args['+' . $i]` as a value to be used a parameter defined by name. This is similar behaviour to `$injector->define('foo', 'bar');` + +6. Try to build the arg through the normal Auryn argument building process. + ### Dependency Resolution @@ -845,3 +950,214 @@ eliminate evil Singletons using the sharing capabilities of the auryn DIC. In th code, we share the request object so that any classes instantiated by the `Auryn\Injector` that ask for a `Request` will receive the same instance. This feature not only helps eliminate Singletons, but also the need for hard-to-test `static` properties. + +### When app-bootstrapping by Auryn is not possible + +Sometimes, the initialisation of the application is outside of your control. One example would be writing plugins for Wordpress, where Wordpress is initialising your plugin, not the other way round. + +You can still use Auryn by using a function to make a single instance of the injector: + +```php +function getAurynInjector() +{ + static $injector = null; + if ($injector == null) { + $injector = new \Auryn\Injector(); + // Do injector defines/shares/aliases/delegates here + } + + return $injector; +} +``` + + +## Advanced patterns + +### "Variadic" dependencies + +Sometimes your code might need a variable number of objects to be passed as a parameter. + +```php + +class Foo { + public function __construct(Repository ...$repositories) { + // do stuff with $repositories + } +} +``` + +In this scenario `$repositories` does not represent a single simple variable, instead `$repositories` represents a complex type. + +As Auryn works by defining rules about types, Auryn isn't able to do injection and so you'll need to use a more advanced technique to be able to inject. + + +#### Variadics using delegate function + +The simplest way to support being able to create objects that themselves have variadic dependencies, is to use a delegate function to create it: + +```php +function createFoo(RepositoryLocator $repoLocator) +{ + // Or whatever code is needed to find the repos. + $repositories = $repoLocator->getRepos('Foo'); + + return new Foo($repositories); +} + +$injector->delegate('Foo', 'createFoo'); +``` + +This should only take a few moments to write the code for, but it has the downside that it moves some application logic into injector. + +#### Variadics using factory classes + +A very slightly longer way to create objects that themselves have variadic dependencies, is to refactor them to use a factory object to get the dependencies: + +```php + +class RepositoryList +{ + /** + * @return Repository[] + */ + public function getRelevantRepositories() { + // do stuff with $repositories + } +} + +class Foo { + public function __construct(RepositoryList $respositoryList) + { + $repositories = $respositoryList->getRelevantRepositories(); + + // error handling goes here + + // do stuff with $repositories + } +} +``` + +This probably a slightly better approach than using the delegate method, as it avoids business/application logic being in the dependency injector, and give you an appropriate place inside your own code to handle errors. + +### Context objects and multiple instances of the same types + +Sometimes you might need to have multiple instances of the same type. + +For example, a background job that moves data from the live database, into the archive database might need to have two instances of a DB class injected + +```php + +class DataArchiver +{ + public function __construct(private PDO $live_db, private PDO $archive_db) + { + } +} +``` + +This _can_ be worked around by using the type system to create more specific types: + +```php +class LivePDO extends PDO {} +class ArchivePDO extends PDO {} + +class DataArchiver +{ + public function __construct(private LivePDO $live_db, private ArchivePDO $archive_db) + { + } +} +``` + +The more specific types can then be created through Auryn, by configuring an appropriate delegate function for each of them + +This approach works, and is actually a reasonable one for small projects, there is an more comprehensive approach that is more appropriate for larger projects. + +#### Encapsulated contexts + +Or to give it the full name, using the 'Encapsulated context pattern'](https://www.allankelly.net/static/patterns/encapsulatecontext.pdf). + +The short description of 'Encapsulated contexts' is that you create specific types that hold all of the needed types for a particular business/domain problem, and allow you to wire them up specifically: + +```php +class DataArchiverContext +{ + public function __construct( + private PDO $live_db, + private PDO $archive_db + ) { + + public function get_live_db(): PDO + { + return $this->live_db; + } + + public function get_archive_db(): PDO + { + return $this->archive_db; + } +} + +class DataArchiver +{ + public function __construct(private DataArchiverContext $dac) + { + } +} + +function createDataArchiver() +{ + return new DataArchiver( + createLiveDB(), + createArchiveDB() + ); +} + +$injector->delegate(DataArchiverContext::class, 'createDataArchiver'); +``` + +Encapsulated contexts makes your code far easier to reason about. You can see: + +* where a particular context is used. +* what types are in it. +* how it is created, including any special rules for it. + +This makes maintaining and reasoning about large programs easier. + +### Running tests and benchmarks + +#### Running tests + +As there is no single version of PHPUnit that works on all the versions of PHP that Auryn supports, we use simple-phpunit to install an appropriate version of PHP. + +After running the `composer update` to get the latest dependencies, run: + +``` +php vendor/bin/simple-phpunit install +``` + +to make simple-phpunit install PHPUnit. The tests can then be with with the command: + +``` +php vendor/bin/simple-phpunit +``` + +simple-phpunit accepts PHPUnit commandline options and passes them through to PHPUnit e.g. `php php vendor/bin/simple-phpunit --group wip` to only run the tests tagged as being part of group 'wip'. + +#### Running benchamarks + +We use PHPBench to allow checking performance gains/regressions when making code changes. The simplest way to use it is as follows: + +1. Create a benchmark baseline by running: + +``` +vendor/bin/phpbench run --tag=benchmark_original --retry-threshold=5 --iterations=10 +``` + +2. Apply your code changes. + +3. Run a benchmark, and compare the results to the 'benchmark_original' by running: + +``` +vendor/bin/phpbench run --report=aggregate --ref=benchmark_original --retry-threshold=5 --iterations=10 +``` diff --git a/composer.json b/composer.json index 8c79baf..431be65 100644 --- a/composer.json +++ b/composer.json @@ -25,12 +25,12 @@ } ], "require": { - "php": ">=5.3.0" + "php": ">=7.2.0" }, "require-dev": { - "phpunit/phpunit": "^4.8", - "fabpot/php-cs-fixer": "~1.9", - "athletic/athletic": "~0.1" + "friendsofphp/php-cs-fixer" : "3.3.1", + "phpbench/phpbench": "1.1.1", + "symfony/phpunit-bridge": "^6.2.0" }, "autoload": { "psr-4": { diff --git a/excluded_features.md b/excluded_features.md new file mode 100644 index 0000000..4b1b056 --- /dev/null +++ b/excluded_features.md @@ -0,0 +1,411 @@ + +# Excluded features + +Sometimes, you just have to say 'no'. + +Although none of the features below would be ridiculous to include in this library, for various reasons they just haven't felt quite 'felt right' to include. + +The notes below try to describe the feature, explain why it was not included, and suggest some alternative solutions. + +## Lazy instantiation + +Original discussion: https://github.com/rdlowrey/auryn/issues/143 + +### Description + +Some dependencies can be costly to create, either in terms of CPU time or memory, or both. + +If the dependency is not guaranteed to be used for all code paths then it can be worth delaying the initialization of that dependency until it is actually used. + +### Alternative solution - use a factory + +Instead of having a direct dependency on the class that is costly to create: + +```php +class CostlyDependency { + function do_the_needful() { + ... + } +} + +class Foo { + + function __construct(private CostlyDependency $costlyDependency) {} + + function bar() { + $this->costlyDependency->do_the_needful(); + } +} +``` + +Create a factory class and depend on that instead, using it to create the costly dependency just before you know it's going to be used. + +```php +class CostlyDependency { + ... +} + +class CostlyDependencyFactory { + + function __construct(private Injector $injector) {} + + function create(): CostlyDependency { + return $this->injector->make(CostlyDependency::class); + } +} + +class Foo { + + function __construct(private CostlyDependencyFactory $costlyDependencyFactory) {} + + function bar() { + $costlyDependency = $this->costlyDependencyFactory->create(); + $costlyDependency->do_the_needful(); + } +} +``` + +And so that it is available to factory classes, in your bootstrap code share the injector: + +```php +$injector->share($injector); +``` + +Although people frown on Service Locators, using the injector in a factory class is a fine tradeoff. + +### Why Lazy Instantiation wasn't included + +* Fewer dependencies is nicer. Having Auryn remain a library with zero external dependencies is nicer than having a non-trivial external dependency. +* Debugging problems inside the injector is a nightmare. Due to how complicated the call-stack is within the injector itself, it can be quite hard to understand what the problem is when an error occurs. By using a factory class, any problem should be a lot easier to debug. +* The alternative solution is trivial and better. In particular, it leaves the place where your code is going to have a costly instantiation of an object obvious, rather than it becoming hidden magic. + + +## Resolving dependencies based on constructor chain + +Original discussion: https://github.com/rdlowrey/auryn/issues/35 + +### Description + +Imagine you have two classes that each depend on a UtilityClass: + +```php +interface Logger{} + +class UtilityClass { + function __construct(private Logger $logger) {} + + function foo() { + $this->logger->log(Logger::info, "About to foo."); + } +} + +class ClassThatIsWorkingCorrectly{ + function __construct(UtilityClass $utilityClass) {} +} + +class ClassRelatedToReportedBugs{ + function __construct(UtilityClass $utilityClass) {} +} +``` + +One of the classes is working correctly and you aren't interested in logging information it generates. The other class is misbehaving slightly. + +Being able to configure the injector so that: + +* the 'Logger' injected into most classes is a logger that only reports notices at the 'error' level. + +* any 'Logger' created by 'ClassRelatedToReportedBugs' or any of its dependencies uses a Logger that reports notices at the 'info' level. + +would allow you to manage your logging behaviour, so that your log files only contain relevant info. + +### Alternative solutions + +#### Get a more powerful logger or logging system + +One example would be the `FingersCrossedHandler` listed in the [Monolog wiki](https://seldaek.github.io/monolog/doc/02-handlers-formatters-processors.html). It allows you to configure logging so that log messages are only actually logged if a certain triggering event has occurred. + +Most hosted logging systems are quite powerful and can be configured in real-time, without having to touch the config of individual servers. + +#### Use more contextual logging + +Logging plain text is just not great. Due to it lacking structure, it makes it harder than it could be to filter, display and understand the logging information. Instead of logging plain text, logging structured data that other tools can understand programmatically makes understanding what is happening in your application much easier. + +### Why it wasn't included + +Mostly, it's just too much complexity for an injector. Although the complexity needs to live somewhere, it should be in the logger code and infrastructure, not in the injector. + +Also, as only one person has requested this feature, it sounds like it shouldn't be included. + + +## Variadic dependencies + +Original discussion: https://github.com/rdlowrey/auryn/issues/134 + +### Description + +Sometimes, particularly when dealing with legacy code, you might have a dependency on multiple instances on the same type of object: + +```php + +interface Repository { + function findInfo(): Info|null; +} + +class Foo { + public function __construct(Repository ...$repositories) { + // ... + } +} +``` + +Auryn does not support injecting variadic dependencies, so any class that has variadic dependencies cannot be directly instantiated by Auryn. + +### Alternative solutions + +#### Use a delegate method + +The simplest work around is to use a delegate function for creating objects that have variadic dependencies: + +```php +function createFoo(RepositoryLocator $repoLocator) +{ + // Or whatever code is needed to find the repos. + $repositories = $repoLocator->getRepos('Foo'); + + return new Foo($repositories); +} + +$injector->delegate('Foo', 'createFoo'); +``` + +#### Use an object that collects the variadic dependencies + +If you have many objects that have variadic dependencies, creating a separate delegate function for each of them might be a tedious task. + +Instead of that, creating a single class that collects the variadic dependencies, and then can be injected through autowiring into other classes, could be substantially less work and also easier to understand. + +```php +interface Repository { + public function findInfo(): Info|null {} +} + +class RepositoryCollection { + private $repos = []; + + public function __construct(Repository ...$repositories) { + foreach ($repositories as $repo) { + $this->repos[] = $repo; + } + } + + public function findInfo(): Info|null { + foreach ($this->repos as $repo) { + $info = $repo->findInfo(); + if ($info !== null) { + return $info; + } + } + + return null; + } +} + +class Foo { + public function __construct(RepositoryCollection $repositories) { + // ... + } +} + +function createRepositoryCollection(RepositoryLocator $repoLocator) +{ + // Or whatever code is needed to find the repos. + $repositories = $repoLocator->getRepos('Foo'); + + return new Foo($repositories); +} + +$injector->delegate(RepositoryCollection::class, 'createRepositoryCollection'); + +``` + +### Why support for variadic dependency wasn't included + +Variadics aren't a type and so can't be reasoned about by a dependency injector. + +People should either use delegation or contexts to achieve what they're trying to do in a way that is comportable with dependency injection. + +## Parameter definitions not inherited from parent classes + +Original discussion: https://github.com/rdlowrey/auryn/issues/133 + +### Description + +Some people expect classes to 'inherit' definitions when definitions exist for their parent class. + +```php +class Foo +{ + public function __construct($key) { } +} + +class Bar extends Foo +{ +} + +$injector = new Auryn\Injector; +$injector->define('Foo', [ + ':key' => 'secret', +]); + +$foo_object = $injector->make(Foo::class); +$bar_object = $injector->make(Bar::class); + +// Gives error: +// Uncaught Auryn\InjectionException: No definition available to provision +// typeless parameter $key at position 0 in Bar::__construct() declared in... +``` + +### Why support for parameter definitions are not inherited from parent classes + +The choice for this library is that all configuration must be explicit, which helps make it easier to reason about the configuration. + +If a class 'inherited' parameter definitions from a parent class, you would have to know that it had a parent class, and that a definition existed for that to be able to understand what parameters the object was going to receive. + +### Alternative solutions + +#### Explicit definition + +Just define parameters for each class that needs them. + +```php +$injector->define('Foo', [':key' => 'secret']); +$injector->define('Bar', [':key' => 'secret']); +``` + +#### Wrap values in a type and share it + +As types can be shared, that can avoid needing to define a scalar value multiple times. + +```php +class ApiKey +{ + public function __construct(private string $value) {} + + public function getValue(): string + { + return $this->value; + } +} + +class Foo +{ + public function __construct(ApiKey $apiKey) { } +} + +class Bar extends Foo +{ +} + +$injector = new Auryn\Injector; +$injector->share(new ApiKey('secret')); + +$foo_object = $injector->make(Foo::class); +$bar_object = $injector->make(Bar::class); +``` + +## Resolve interfaces and abstracts to shared instances + +Original discussion: https://github.com/rdlowrey/auryn/issues/145 + +### Description + +Some people expect an injector to find classes that implement an interface whe + +```php +interface A { } +class B implements A { } +class C { + function __construct(A $a) { } +} + +$b = new B(); +$injector->share($b); + +$injector->make(C::class); + +// Gives error: +// Uncaught Auryn\InjectionException: Injection definition required for interface A in... +``` + +### Alternative solutions + +Just define aliases for each interface to a concrete class. That will only take a few minutes per environment your code runs in. + +### Why support for Resolve interfaces and abstracts to shared instances wasn't included + +The choice for this library is that all configuration must be explicit, which helps make it easier to reason about the configuration. + +If you wanted to see what class was going to be created for an interface, not being able to inspect the configuration of the injector, and instead having to either search through the code or run some test code would be 'ungood'. + +Additionally, if a second class that implements the interface was added, the injector would need to either pick one or throw an exception of "multiple available types". Either choice would be quite surprising and take more time to resolve than simply writing the explicit configuration. + + +## Optional aka null default parameters + +Original discussion: https://github.com/rdlowrey/auryn/issues/190 + +### Description + + +When a dependency is optional and/or nullable, then Auryn will always pass null as the parameter: + +```php +class NullableDependency {} + +class DependsOnNullableDependency +{ + public ?NullableDependency $string; + + public function __construct(?NullableDependency $instance = null) + { + $this->instance = $instance; + } +} +``` + +Some people might expect that the NullableDependency would be created. + + +### Why the parameter is always null + + +* Auryn defaults to explicit configuration. If an dependency is optional, then defaulting to not creating it, is more consistent with that principle. + +* Some classes are not instantiable e.g. abstract classes, or classes with uncreatable dependencies. If Auryn defaulted to attempting to create the type, we would need to add "don't create this class" rules/config. + + +### Alternative solution - be explicit + +```php +$injector->alias(NullableDependency::class, NullableDependency::class); +``` + + +### Alternative solution - new in the initializer + +As of PHP 8.1, you can use create objects the initializer: + + +```php +class NullableDependency {} + +class DependsOnNullableDependency +{ + public ?NullableDependency $string; + + public function __construct(?NullableDependency $instance = new NullableDependency) + { + $this->instance = $instance; + } +} +``` \ No newline at end of file diff --git a/lib/CachingReflector.php b/lib/CachingReflector.php index 7784bc4..76535a8 100644 --- a/lib/CachingReflector.php +++ b/lib/CachingReflector.php @@ -52,7 +52,7 @@ public function getCtorParams($class) return $reflectedCtorParams; } - public function getParamTypeHint(\ReflectionFunctionAbstract $function, \ReflectionParameter $param) + public function getParamType(\ReflectionFunctionAbstract $function, \ReflectionParameter $param) { $lowParam = strtolower($param->name); @@ -67,16 +67,16 @@ public function getParamTypeHint(\ReflectionFunctionAbstract $function, \Reflect : null; } - $typeHint = ($paramCacheKey === null) ? false : $this->cache->fetch($paramCacheKey); + $type = ($paramCacheKey === null) ? false : $this->cache->fetch($paramCacheKey); - if (false === $typeHint) { - $typeHint = $this->reflector->getParamTypeHint($function, $param); + if (false === $type) { + $type = $this->reflector->getParamType($function, $param); if ($paramCacheKey !== null) { - $this->cache->store($paramCacheKey, $typeHint); + $this->cache->store($paramCacheKey, $type); } } - return $typeHint; + return $type; } public function getFunction($functionName) diff --git a/lib/ConfigException.php b/lib/ConfigException.php index f7a0ae0..a94eb63 100644 --- a/lib/ConfigException.php +++ b/lib/ConfigException.php @@ -4,4 +4,18 @@ class ConfigException extends InjectorException { + /** + * Add a human readable version of the invalid callable to the standard 'invalid invokable' message. + */ + public static function fromInvalidCallable( + $callableOrMethodStr, + \Exception $previous = null + ) { + + $message = InjectionException::getInvalidCallableMessage( + $callableOrMethodStr + ); + + return new self($message, Injector::E_INVOKABLE, $previous); + } } diff --git a/lib/Executable.php b/lib/Executable.php index dc1b77a..069dd23 100644 --- a/lib/Executable.php +++ b/lib/Executable.php @@ -42,27 +42,7 @@ public function __invoke() return $reflection->invokeArgs($this->invocationObject, $args); } - return $this->callableReflection->isClosure() - ? $this->invokeClosureCompat($reflection, $args) - : $reflection->invokeArgs($args); - } - - /** - * @TODO Remove this extra indirection when 5.3 support is dropped - */ - private function invokeClosureCompat($reflection, $args) - { - if (version_compare(PHP_VERSION, '5.4.0') >= 0) { - $scope = $reflection->getClosureScopeClass(); - $closure = \Closure::bind( - $reflection->getClosure(), - $reflection->getClosureThis(), - $scope ? $scope->name : null - ); - return call_user_func_array($closure, $args); - } else { - return $reflection->invokeArgs($args); - } + return $reflection->invokeArgs($args); } public function getCallableReflection() diff --git a/lib/InjectionException.php b/lib/InjectionException.php index d7ab70b..2c4ba96 100644 --- a/lib/InjectionException.php +++ b/lib/InjectionException.php @@ -14,24 +14,59 @@ public function __construct(array $inProgressMakes, $message = "", $code = 0, \E parent::__construct($message, $code, $previous); } + public static function fromInvalidDefineParamsNotArray($definition, array $inProgressMakes): self + { + $message = sprintf( + Injector::M_INVALID_DEFINE_ARGUMENT_NOT_ARRAY, + gettype($definition) + ); + + return new self( + $inProgressMakes, + $message, + Injector::E_INVALID_DEFINE_ARGUMENT_NOT_ARRAY + ); + } + + public static function fromInvalidDefineParamsBadKeys($definition, array $inProgressMakes) + { + $missingKeys = []; + if (!isset($definition[0])) { + $missingKeys[] = "array key 0 not set"; + } + if (!isset($definition[1])) { + $missingKeys[] = "array key 1 not set"; + } + + $message = sprintf( + Injector::M_INVALID_DEFINE_ARGUMENT_BAD_KEYS, + implode(" ", $missingKeys) + ); + + return new self( + $inProgressMakes, + $message, + Injector::E_INVALID_DEFINE_ARGUMENT_BAD_KEYS + ); + } + /** - * Add a human readable version of the invalid callable to the standard 'invalid invokable' message. + * If PHP had package based privacy rules, this could be package private + * or this could be 'just' a function. + * @param $callableOrMethodStr + * @return string */ - public static function fromInvalidCallable( - array $inProgressMakes, - $callableOrMethodStr, - \Exception $previous = null - ) { + public static function getInvalidCallableMessage($callableOrMethodStr) + { $callableString = null; if (is_string($callableOrMethodStr)) { $callableString .= $callableOrMethodStr; - } else if (is_array($callableOrMethodStr) && - array_key_exists(0, $callableOrMethodStr) && + } elseif (is_array($callableOrMethodStr) && array_key_exists(0, $callableOrMethodStr)) { if (is_string($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) { $callableString .= $callableOrMethodStr[0].'::'.$callableOrMethodStr[1]; - } else if (is_object($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) { + } elseif (is_object($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) { $callableString .= sprintf( "[object(%s), '%s']", get_class($callableOrMethodStr[0]), @@ -41,17 +76,30 @@ public static function fromInvalidCallable( } if ($callableString) { - // Prevent accidental usage of long strings from filling logs. + // Prevent accidental usage of long strings from filling logs. $callableString = substr($callableString, 0, 250); - $message = sprintf( + return sprintf( "%s. Invalid callable was '%s'", Injector::M_INVOKABLE, $callableString ); - } else { - $message = \Auryn\Injector::M_INVOKABLE; } + return Injector::M_INVOKABLE; + } + + /** + * Add a human readable version of the invalid callable to the standard 'invalid invokable' message. + */ + public static function fromInvalidCallable( + array $inProgressMakes, + $callableOrMethodStr, + \Exception $previous = null + ) { + $message = self::getInvalidCallableMessage( + $callableOrMethodStr + ); + return new self($inProgressMakes, $message, Injector::E_INVOKABLE, $previous); } diff --git a/lib/Injector.php b/lib/Injector.php index 1b5b3c0..c504b06 100644 --- a/lib/Injector.php +++ b/lib/Injector.php @@ -38,6 +38,14 @@ class Injector const M_CYCLIC_DEPENDENCY = "Detected a cyclic dependency while provisioning %s"; const E_MAKING_FAILED = 12; const M_MAKING_FAILED = "Making %s did not result in an object, instead result is of type '%s'"; + const E_DOUBLE_SHARE = 13; + const M_DOUBLE_SHARE = "An instance of type %s has already been shared. Cannot share a second instance of the same type."; + + const E_INVALID_DEFINE_ARGUMENT_NOT_ARRAY = 12; + const M_INVALID_DEFINE_ARGUMENT_NOT_ARRAY = "Define parameters needs to be an array with contents of {0:class-string, 1:array of injector params}. Value passed was of type '%s'."; + + const E_INVALID_DEFINE_ARGUMENT_BAD_KEYS = 13; + const M_INVALID_DEFINE_ARGUMENT_BAD_KEYS = "Define parameters needs to be an array with contents of {0:class-string, 1:array of injector params}. %s."; private $reflector; private $classDefinitions = array(); @@ -76,7 +84,7 @@ public function define($name, array $args) /** * Assign a global default value for all parameters named $paramName * - * Global parameter definitions are only used for parameters with no typehint, pre-defined or + * Global parameter definitions are only used for parameters with no type, pre-defined or * call-time definition. * * @param string $paramName The parameter name for which this value applies @@ -91,11 +99,11 @@ public function defineParam($paramName, $value) } /** - * Define an alias for all occurrences of a given typehint + * Define an alias for all occurrences of a given type * - * Use this method to specify implementation classes for interface and abstract class typehints. + * Use this method to specify implementation classes for interface and abstract class types. * - * @param string $original The typehint to replace + * @param string $original The type to replace * @param string $alias The implementation name * @throws ConfigException if any argument is empty or not a string * @return self @@ -141,7 +149,7 @@ public function alias($original, $alias) private function normalizeName($className) { - return ltrim(strtolower($className), '\\'); + return ltrim(strtolower($className), '?\\'); } /** @@ -153,6 +161,8 @@ private function normalizeName($className) */ public function share($nameOrInstance) { + + if (is_string($nameOrInstance)) { $this->shareClass($nameOrInstance); } elseif (is_object($nameOrInstance)) { @@ -204,6 +214,16 @@ private function shareInstance($obj) self::E_ALIASED_CANNOT_SHARE ); } + + if (isset($this->shares[$normalizedName])) { + throw new ConfigException( + sprintf( + self::M_DOUBLE_SHARE, + get_class($obj) + ), + self::E_DOUBLE_SHARE + ); + } $this->shares[$normalizedName] = $obj; } @@ -215,15 +235,14 @@ private function shareInstance($obj) * * @param string $name * @param mixed $callableOrMethodStr Any callable or provisionable invokable method - * @throws InjectionException if $callableOrMethodStr is not a callable. - * See https://github.com/rdlowrey/auryn#injecting-for-execution + * @throws ConfigException if $callableOrMethodStr is not a callable. + * * @return self */ public function prepare($name, $callableOrMethodStr) { if ($this->isExecutable($callableOrMethodStr) === false) { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, + throw ConfigException::fromInvalidCallable( $callableOrMethodStr ); } @@ -369,7 +388,18 @@ public function make($name, array $args = array()) $reflectionFunction = $executable->getCallableReflection(); $args = $this->provisionFuncArgs($reflectionFunction, $args, null, $className); $obj = call_user_func_array(array($executable, '__invoke'), $args); - } else { + if (!($obj instanceof $normalizedClass)) { + throw new InjectionException( + $this->inProgressMakes, + sprintf( + self::M_MAKING_FAILED, + $normalizedClass, + gettype($obj) + ), + self::E_MAKING_FAILED + ); + } + } else { $obj = $this->provisionInstance($className, $normalizedClass, $args); } @@ -380,12 +410,7 @@ public function make($name, array $args = array()) } unset($this->inProgressMakes[$normalizedClass]); - } - catch (\Throwable $exception) { - unset($this->inProgressMakes[$normalizedClass]); - throw $exception; - } - catch (\Exception $exception) { + } catch (\Throwable $exception) { unset($this->inProgressMakes[$normalizedClass]); throw $exception; } @@ -471,10 +496,10 @@ private function provisionFuncArgs(\ReflectionFunctionAbstract $reflFunc, array } elseif (($prefix = self::A_DEFINE . $name) && isset($definition[$prefix])) { // interpret the param as a class definition $arg = $this->buildArgFromParamDefineArr($definition[$prefix]); - } elseif (!$arg = $this->buildArgFromTypeHint($reflFunc, $reflParam)) { + } elseif (!$arg = $this->buildArgFromType($reflFunc, $reflParam)) { $arg = $this->buildArgFromReflParam($reflParam, $className); - if ($arg === null && PHP_VERSION_ID >= 50600 && $reflParam->isVariadic()) { + if ($arg === null && ($reflParam->isVariadic() || $reflParam->isOptional())) { // buildArgFromReflParam might return null in case the parameter is optional // in case of variadics, the parameter is optional, but null might not be allowed continue; @@ -490,16 +515,16 @@ private function provisionFuncArgs(\ReflectionFunctionAbstract $reflFunc, array private function buildArgFromParamDefineArr($definition) { if (!is_array($definition)) { - throw new InjectionException( + throw InjectionException::fromInvalidDefineParamsNotArray( + $definition, $this->inProgressMakes - // @TODO Add message ); } if (!isset($definition[0], $definition[1])) { - throw new InjectionException( + throw InjectionException::fromInvalidDefineParamsBadKeys( + $definition, $this->inProgressMakes - // @TODO Add message ); } @@ -522,24 +547,24 @@ private function buildArgFromDelegate($paramName, $callableOrMethodStr) return $executable($paramName, $this); } - private function buildArgFromTypeHint(\ReflectionFunctionAbstract $reflFunc, \ReflectionParameter $reflParam) + private function buildArgFromType(\ReflectionFunctionAbstract $reflFunc, \ReflectionParameter $reflParam) { - $typeHint = $this->reflector->getParamTypeHint($reflFunc, $reflParam); + $type = $this->reflector->getParamType($reflFunc, $reflParam); - if (!$typeHint) { + if (!$type) { $obj = null; } elseif ($reflParam->isDefaultValueAvailable()) { - $normalizedName = $this->normalizeName($typeHint); + $normalizedName = $this->normalizeName($type); // Injector has been told explicitly how to make this type if (isset($this->aliases[$normalizedName]) || isset($this->delegates[$normalizedName]) || isset($this->shares[$normalizedName])) { - $obj = $this->make($typeHint); + $obj = $this->make($type); } else { $obj = $reflParam->getDefaultValue(); } } else { - $obj = $this->make($typeHint); + $obj = $this->make($type); } return $obj; @@ -584,6 +609,7 @@ private function buildArgFromReflParam(\ReflectionParameter $reflParam, $classNa private function prepareInstance($obj, $normalizedClass) { + // Check and call any prepares for a class. if (isset($this->prepares[$normalizedClass])) { $prepare = $this->prepares[$normalizedClass]; $executable = $this->buildExecutable($prepare); @@ -593,19 +619,7 @@ private function prepareInstance($obj, $normalizedClass) } } - $interfaces = @class_implements($obj); - - if ($interfaces === false) { - throw new InjectionException( - $this->inProgressMakes, - sprintf( - self::M_MAKING_FAILED, - $normalizedClass, - gettype($obj) - ), - self::E_MAKING_FAILED - ); - } + $interfaces = class_implements($obj); if (empty($interfaces)) { return $obj; diff --git a/lib/Reflector.php b/lib/Reflector.php index afe052d..e3ce787 100644 --- a/lib/Reflector.php +++ b/lib/Reflector.php @@ -40,7 +40,7 @@ public function getCtorParams($class); * @param \ReflectionFunctionAbstract $function * @param \ReflectionParameter $param */ - public function getParamTypeHint(\ReflectionFunctionAbstract $function, \ReflectionParameter $param); + public function getParamType(\ReflectionFunctionAbstract $function, \ReflectionParameter $param); /** * Retrieves and caches a reflection for the specified function diff --git a/lib/StandardReflector.php b/lib/StandardReflector.php index 17ae1ef..723d394 100644 --- a/lib/StandardReflector.php +++ b/lib/StandardReflector.php @@ -23,7 +23,7 @@ public function getCtorParams($class) : null; } - public function getParamTypeHint(\ReflectionFunctionAbstract $function, \ReflectionParameter $param) + public function getParamType(\ReflectionFunctionAbstract $function, \ReflectionParameter $param) { // php 8 deprecates getClass method if (PHP_VERSION_ID >= 80000) { @@ -34,6 +34,8 @@ public function getParamTypeHint(\ReflectionFunctionAbstract $function, \Reflect return $type ? (string) $type : null; } else { + // @codeCoverageIgnoreStart + // Can't be tested on PHP 8 /** @var ?\ReflectionClass $reflectionClass */ $reflectionClass = $param->getClass(); if ($reflectionClass) { @@ -41,6 +43,7 @@ public function getParamTypeHint(\ReflectionFunctionAbstract $function, \Reflect } return null; + // @codeCoverageIgnoreEnd } } diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 0000000..ec0a8b3 --- /dev/null +++ b/phpbench.json @@ -0,0 +1,5 @@ +{ + "runner.bootstrap": "vendor/autoload.php", + "runner.path": "test/Benchmark", + "runner.file_pattern": "*Bench.php" +} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index c997caa..e86a108 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,34 +1,22 @@ - - - - ./test - - - - - ./lib - - - - - - - + - + diff --git a/test/BaseTest.php b/test/BaseTest.php new file mode 100644 index 0000000..f7656cf --- /dev/null +++ b/test/BaseTest.php @@ -0,0 +1,87 @@ + '.*', // strings can be empty, so * + // decimal (integer) number (base 10) + '%d' => '[+-]?\d+', // numbers can't be empty so + + + // character + '%c' => '[\s\S]', + + // todo + // exponential floating-point number + // %e + + // floating-point number + '%f' => "[+-]?([0-9]*[.])?[0-9]+", + + // integer (base 10) + '%i' => '[+-]?\d+', + + // octal number (base 8) + '%o' => "[+-]?([0-7])+", + + // unsigned decimal (integer) number + '%u' => '[0-9][0-9]*', + + // number in hexadecimal (base 16) + '%x' => '[[:xdigit:]]+' + ]; + + $string = str_replace( + array_keys($replacements), + array_values($replacements), + $string + ); + + return '#' . $string . '#iu'; +} + +abstract class BaseTest extends TestCase +{ + /** + * @param string $templateString A template string for printf e.g. "Hello %s" + * @param string $actualString The string to test to see if it matches e.g. "Hello John" + */ + public function assertStringMatchesTemplateString(string $templateString, string $actualString): void + { + $regExp = templateStringToRegExp($templateString); + $this->assertMatchesRegularExpression($regExp, $actualString); + } + + /** + * @param string $templateString A template string for printf e.g. "Hello %s" + */ + public function expectExceptionMessageMatchesTemplateString(string $templateString): void + { + $regexp = templateStringToRegExp($templateString); + $this->expectExceptionMessageMatches($regexp); + } + + /** + * @param string $text The string to look for. + * @param string $flags The regexp flags to use. + */ + public function expectExceptionMessageContains(string $text, string $flags = 'iu'): void + { + $regexp = '/' . preg_quote($text, '/') . '/' . $flags; + $this->expectExceptionMessageMatches($regexp); + } +} \ No newline at end of file diff --git a/test/Benchmark/AliasedImplementation.php b/test/Benchmark/AliasedImplementation.php new file mode 100644 index 0000000..9e22c92 --- /dev/null +++ b/test/Benchmark/AliasedImplementation.php @@ -0,0 +1,8 @@ +injector = new Injector(); + $this->noop = new Noop(); + } + + /** + * @Revs(10000) + */ + public function benchnative_invoke_closure() + { + call_user_func(function () { + // call-target, intenionally left empty + }); + } + + /** + * @Revs(10000) + */ + public function benchnative_invoke_method() + { + call_user_func(array($this->noop, 'noop')); + } + + /** + * @Revs(10000) + */ + public function benchinvoke_closure() + { + $this->injector->execute(function () { + // call-target, intentionally left empty + }); + } + + /** + * @Revs(10000) + */ + public function benchinvoke_method() + { + $this->injector->execute(array($this->noop, 'noop')); + } + + /** + * @Revs(10000) + */ + public function benchinvoke_with_named_parameters() + { + $this->injector->execute(array($this->noop, 'namedNoop'), array(':name' => 'foo')); + } + + /** + * @Revs(10000) + */ + public function bench_make_noop() + { + $this->injector->make(Noop::class); + } + + /** + * @Revs(10000) + */ + public function bench_make_two_dependency_object() + { + $this->injector->make(TwoDeps::class); + } +} diff --git a/test/Benchmark/ExecuteBenchmark.php b/test/Benchmark/ExecuteBenchmark.php deleted file mode 100644 index 85f74ed..0000000 --- a/test/Benchmark/ExecuteBenchmark.php +++ /dev/null @@ -1,63 +0,0 @@ -injector = new Injector(); - $this->noop = new Noop(); - } - - /** - * @baseline - * @iterations 10000 - */ - public function native_invoke_closure() - { - call_user_func(function () { - // call-target, intenionally left empty - }); - } - - /** - * @iterations 10000 - */ - public function native_invoke_method() - { - call_user_func(array($this->noop, 'noop')); - } - - /** - * @iterations 10000 - */ - public function invoke_closure() - { - $this->injector->execute(function () { - // call-target, intenionally left empty - }); - } - - /** - * @iterations 10000 - */ - public function invoke_method() - { - $this->injector->execute(array($this->noop, 'noop')); - } - - /** - * @iterations 10000 - */ - public function invoke_with_named_parameters() - { - $this->injector->execute(array($this->noop, 'namedNoop'), array(':name' => 'foo')); - } -} diff --git a/test/Benchmark/NonTrivial.php b/test/Benchmark/NonTrivial.php new file mode 100644 index 0000000..836f12d --- /dev/null +++ b/test/Benchmark/NonTrivial.php @@ -0,0 +1,19 @@ +injector = new Injector(); + + $this->injector->delegate( + DelegatedClass::class, + [\Auryn\Test\Benchmark\DelegatedClass::class, 'create'] + ); + $this->injector->alias( + AliasedInterface::class, + AliasedImplementation::class + ); + + $this->injector->share(new SharedInstance('John')); + } + + /** + * @Revs(10000) + */ + public function bench_make_non_trivial_object() + { + $this->injector->make(NonTrivial::class); + } +} diff --git a/test/Benchmark/TwoDeps.php b/test/Benchmark/TwoDeps.php new file mode 100644 index 0000000..85eb6e1 --- /dev/null +++ b/test/Benchmark/TwoDeps.php @@ -0,0 +1,10 @@ +getMethod('foo'); + $obj = $rc->newInstanceWithoutConstructor(); + $executable = new Executable($rm, $obj); + + $this->assertSame($obj, $executable->getInvocationObject()); + $this->assertSame($rm, $executable->getCallableReflection()); + $this->assertTrue($executable->isInstanceMethod()); + } + + public function testBasicErrors() + { + $rc = new \ReflectionClass(ExecutableHelper::class); + $rm = $rc->getMethod('foo'); + $this->expectExceptionMessage("ReflectionMethod callables must specify an invocation object"); + $this->expectException(\InvalidArgumentException::class); + new Executable($rm, null); + } +} \ No newline at end of file diff --git a/test/InjectorTest.php b/test/InjectorTest.php index 60977b8..b344f3a 100644 --- a/test/InjectorTest.php +++ b/test/InjectorTest.php @@ -1,13 +1,12 @@ defineParam('parameter', []); @@ -28,31 +27,28 @@ public function testMakeInstanceReturnsNewInstanceIfClassHasNoConstructor() $this->assertEquals(new TestNoConstructor, $injector->make('Auryn\Test\TestNoConstructor')); } - public function testMakeInstanceReturnsAliasInstanceOnNonConcreteTypehint() + public function testMakeInstanceReturnsAliasInstanceOnNonConcreteType() { $injector = new Injector; $injector->alias('Auryn\Test\DepInterface', 'Auryn\Test\DepImplementation'); $this->assertEquals(new DepImplementation, $injector->make('Auryn\Test\DepInterface')); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Injection definition required for interface Auryn\Test\DepInterface - * @expectedExceptionCode \Auryn\Injector::E_NEEDS_DEFINITION - */ public function testMakeInstanceThrowsExceptionOnInterfaceWithoutAlias() { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage("Injection definition required for interface Auryn\Test\DepInterface"); + $this->expectExceptionCode(\Auryn\Injector::E_NEEDS_DEFINITION); $injector = new Injector; $injector->make('Auryn\Test\DepInterface'); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Injection definition required for interface Auryn\Test\DepInterface - * @expectedExceptionCode \Auryn\Injector::E_NEEDS_DEFINITION - */ public function testMakeInstanceThrowsExceptionOnNonConcreteCtorParamWithoutImplementation() { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage("Injection definition required for interface Auryn\Test\DepInterface"); + $this->expectExceptionCode(\Auryn\Injector::E_NEEDS_DEFINITION); + $injector = new Injector; $injector->make('Auryn\Test\RequiresInterface'); } @@ -65,7 +61,7 @@ public function testMakeInstanceBuildsNonConcreteCtorParamWithAlias() $this->assertInstanceOf('Auryn\Test\RequiresInterface', $obj); } - public function testMakeInstancePassesNullCtorParameterIfNoTypehintOrDefaultCanBeDetermined() + public function testMakeInstancePassesNullCtorParameterIfNoTypeOrDefaultCanBeDetermined() { $injector = new Injector; $nullCtorParamObj = $injector->make('Auryn\Test\ProvTestNoDefinitionNullDefaultClass'); @@ -87,12 +83,16 @@ public function testMakeInstanceReturnsSharedInstanceIfAvailable() $this->assertEquals('something else', $injected2->testDep->testProp); } - /** - * @expectedException \Auryn\InjectorException - * @expectedExceptionMessage Could not make ClassThatDoesntExist: Class ClassThatDoesntExist does not exist - */ public function testMakeInstanceThrowsExceptionOnClassLoadFailure() { + $classname = 'ClassThatDoesntExist'; + if (PHP_VERSION_ID >= 80000) { + $classname = "\"" . $classname . "\""; + } + + $this->expectException(\Auryn\InjectorException::class); + $this->expectExceptionMessage("Could not make ClassThatDoesntExist: Class $classname does not exist"); + $injector = new Injector; $injector->make('ClassThatDoesntExist'); } @@ -127,10 +127,10 @@ public function testMakeInstanceUsesReflectionForUnknownParamsInMultiBuildWithDe $obj = $injector->make('Auryn\Test\TestMultiDepsWithCtor', array('val1'=>'Auryn\Test\TestDependency')); $this->assertInstanceOf('Auryn\Test\TestMultiDepsWithCtor', $obj); - $obj = $injector->make('Auryn\Test\NoTypehintNoDefaultConstructorClass', + $obj = $injector->make('Auryn\Test\NoTypeNoDefaultConstructorClass', array('val1'=>'Auryn\Test\TestDependency') ); - $this->assertInstanceOf('Auryn\Test\NoTypehintNoDefaultConstructorClass', $obj); + $this->assertInstanceOf('Auryn\Test\NoTypeNoDefaultConstructorClass', $obj); $this->assertNull($obj->testParam); } @@ -146,17 +146,17 @@ public function testMakeInstanceUsesReflectionForUnknownParamsInMultiBuildWithDe require_once __DIR__ . "/fixtures_5_6.php"; $injector = new Injector; - $obj = $injector->make('Auryn\Test\NoTypehintNoDefaultConstructorVariadicClass', + $obj = $injector->make('Auryn\Test\NoTypeNoDefaultConstructorVariadicClass', array('val1'=>'Auryn\Test\TestDependency') ); - $this->assertInstanceOf('Auryn\Test\NoTypehintNoDefaultConstructorVariadicClass', $obj); + $this->assertInstanceOf('Auryn\Test\NoTypeNoDefaultConstructorVariadicClass', $obj); $this->assertEquals(array(), $obj->testParam); } /** * @requires PHP 5.6 */ - public function testMakeInstanceUsesReflectionForUnknownParamsWithDepsAndVariadicsWithTypeHint() + public function testMakeInstanceUsesReflectionForUnknownParamsWithDepsAndVariadicsWithType() { if (defined('HHVM_VERSION')) { $this->markTestSkipped("HHVM doesn't support variadics with type declarations."); @@ -165,44 +165,50 @@ public function testMakeInstanceUsesReflectionForUnknownParamsWithDepsAndVariadi require_once __DIR__ . "/fixtures_5_6.php"; $injector = new Injector; - $obj = $injector->make('Auryn\Test\TypehintNoDefaultConstructorVariadicClass', + $obj = $injector->make('Auryn\Test\TypeNoDefaultConstructorVariadicClass', array('arg'=>'Auryn\Test\TestDependency') ); - $this->assertInstanceOf('Auryn\Test\TypehintNoDefaultConstructorVariadicClass', $obj); - $this->assertInternalType("array", $obj->testParam); + $this->assertInstanceOf('Auryn\Test\TypeNoDefaultConstructorVariadicClass', $obj); + $this->assertIsArray($obj->testParam); $this->assertInstanceOf('Auryn\Test\TestDependency', $obj->testParam[0]); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage No definition available to provision typeless parameter $val at position 0 in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault::__construct() declared in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault:: - * @expectedExceptionCode \Auryn\Injector::E_UNDEFINED_PARAM - */ - public function testMakeInstanceThrowsExceptionOnUntypehintedParameterWithoutDefinitionOrDefault() + public function testMakeInstanceThrowsExceptionOnUntypedParameterWithoutDefinitionOrDefault() { + $this->expectException(\Auryn\InjectionException::class); + // TODO - why does this message end with double-colon? + $this->expectExceptionMessage('No definition available to provision typeless parameter $val at position 0 in Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault::__construct() declared in Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault::'); + $this->expectExceptionCode(\Auryn\Injector::E_UNDEFINED_PARAM); + $injector = new Injector; - $injector->make('Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault'); + $injector->make('Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault'); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage No definition available to provision typeless parameter $val at position 0 in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault::__construct() declared in Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault:: - * @expectedExceptionCode \Auryn\Injector::E_UNDEFINED_PARAM - */ - public function testMakeInstanceThrowsExceptionOnUntypehintedParameterWithoutDefinitionOrDefaultThroughAliasedTypehint() + public function testbuildArgFromReflParamCoverageNonClassCase() + { + $injector= new Injector; + + $this->expectExceptionCode(\Auryn\Injector::E_UNDEFINED_PARAM); + $injector->execute('Auryn\Test\aFunctionWithAParam'); + } + + public function testMakeInstanceThrowsExceptionOnUntypedParameterWithoutDefinitionOrDefaultThroughAliasedType() { + $this->expectException(\Auryn\InjectionException::class); + // TODO - why does this message end with double-colon? + $this->expectExceptionMessage('No definition available to provision typeless parameter $val at position 0 in Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault::__construct() declared in Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault::'); + $this->expectExceptionCode(\Auryn\Injector::E_UNDEFINED_PARAM); + $injector = new Injector; - $injector->alias('Auryn\Test\TestNoExplicitDefine', 'Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefault'); - $injector->make('Auryn\Test\InjectorTestCtorParamWithNoTypehintOrDefaultDependent'); + $injector->alias('Auryn\Test\TestNoExplicitDefine', 'Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefault'); + $injector->make('Auryn\Test\InjectorTestCtorParamWithNoTypeOrDefaultDependent'); } - /** - * @TODO - * @expectedException \Auryn\InjectorException - * @expectedExceptionMessage Injection definition required for interface Auryn\Test\DepInterface - */ - public function testMakeInstanceThrowsExceptionOnUninstantiableTypehintWithoutDefinition() + public function testMakeInstanceThrowsExceptionOnUninstantiableTypeWithoutDefinition() { + $this->expectException(\Auryn\InjectorException::class); + $this->expectExceptionMessage("Injection definition required for interface Auryn\Test\DepInterface"); + $injector = new Injector; $injector->make('Auryn\Test\RequiresInterface'); } @@ -221,8 +227,8 @@ public function testTypelessDefineForAliasedDependency() $injector = new Injector; $injector->defineParam('val', 42); - $injector->alias('Auryn\Test\TestNoExplicitDefine', 'Auryn\Test\ProviderTestCtorParamWithNoTypehintOrDefault'); - $obj = $injector->make('Auryn\Test\ProviderTestCtorParamWithNoTypehintOrDefaultDependent'); + $injector->alias('Auryn\Test\TestNoExplicitDefine', 'Auryn\Test\ProviderTestCtorParamWithNoTypeOrDefault'); + $obj = $injector->make('Auryn\Test\ProviderTestCtorParamWithNoTypeOrDefaultDependent'); } public function testMakeInstanceInjectsRawParametersDirectly() @@ -239,34 +245,33 @@ public function testMakeInstanceInjectsRawParametersDirectly() )); $obj = $injector->make('Auryn\Test\InjectorTestRawCtorParams'); - $this->assertInternalType('string', $obj->string); + $this->assertIsString($obj->string); $this->assertInstanceOf('StdClass', $obj->obj); - $this->assertInternalType('int', $obj->int); - $this->assertInternalType('array', $obj->array); - $this->assertInternalType('float', $obj->float); - $this->assertInternalType('bool', $obj->bool); + $this->assertIsInt($obj->int); + $this->assertIsArray($obj->array); + $this->assertIsFloat($obj->float); + $this->assertIsBool($obj->bool); $this->assertNull($obj->null); } - /** - * @TODO - * @expectedException \Exception - * @expectedExceptionMessage - */ public function testMakeInstanceThrowsExceptionWhenDelegateDoes() { $injector= new Injector; - $callable = $this->getMock( - 'CallableMock', + $callable = $this->createPartialMock( + 'Auryn\test\CallableMock', array('__invoke') ); $injector->delegate('TestDependency', $callable); + $message = "This is the expected exception."; $callable->expects($this->once()) ->method('__invoke') - ->will($this->throwException(new \Exception())); + ->will($this->throwException(new \Exception($message))); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage($message); $injector->make('TestDependency'); } @@ -281,10 +286,11 @@ public function testMakeInstanceDelegate() { $injector= new Injector; - $callable = $this->getMock( - 'CallableMock', + $callable = $this->createPartialMock( + 'Auryn\test\CallableMock', array('__invoke') ); + $callable->expects($this->once()) ->method('__invoke') ->will($this->returnValue(new TestDependency())); @@ -304,32 +310,30 @@ public function testMakeInstanceWithStringDelegate() $this->assertEquals(42, $obj->test); } - /** - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Auryn\Injector::delegate expects a valid callable or executable class::method string at Argument 2 but received 'StringDelegateWithNoInvokeMethod' - */ public function testMakeInstanceThrowsExceptionIfStringDelegateClassHasNoInvokeMethod() { $injector= new Injector; + + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage("Auryn\\Injector::delegate expects a valid callable or executable class::method string at Argument 2 but received 'StringDelegateWithNoInvokeMethod'"); + $injector->delegate('StdClass', 'StringDelegateWithNoInvokeMethod'); } - /** - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Auryn\Injector::delegate expects a valid callable or executable class::method string at Argument 2 but received 'SomeClassThatDefinitelyDoesNotExistForReal' - */ public function testMakeInstanceThrowsExceptionIfStringDelegateClassInstantiationFails() { + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage("Auryn\\Injector::delegate expects a valid callable or executable class::method string at Argument 2 but received 'SomeClassThatDefinitelyDoesNotExistForReal'"); + $injector= new Injector; $injector->delegate('StdClass', 'SomeClassThatDefinitelyDoesNotExistForReal'); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Injection definition required for interface Auryn\Test\DepInterface - */ - public function testMakeInstanceThrowsExceptionOnUntypehintedParameterWithNoDefinition() + public function testMakeInstanceThrowsExceptionOnUntypedParameterWithNoDefinition() { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('Injection definition required for interface Auryn\Test\DepInterface'); + $injector = new Injector; $injector->make('Auryn\Test\RequiresInterface'); } @@ -359,12 +363,11 @@ public function testShareMarksClassSharedOnNullObjectParameter() $this->assertInstanceOf('Auryn\Injector', $injector->share('SomeClass')); } - /** - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Auryn\Injector::share() requires a string class name or object instance at Argument 1; integer specified - */ public function testShareThrowsExceptionOnInvalidArgument() { + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage('Auryn\Injector::share() requires a string class name or object instance at Argument 1; integer specified'); + $injector = new Injector; $injector->share(42); } @@ -386,11 +389,12 @@ public function provideInvalidDelegates() /** * @dataProvider provideInvalidDelegates - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Auryn\Injector::delegate expects a valid callable or executable class::method string at Argument 2 */ public function testDelegateThrowsExceptionIfDelegateIsNotCallableOrString($badDelegate) { + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage('Auryn\Injector::delegate expects a valid callable or executable class::method string at Argument 2'); + $injector = new Injector; $injector->delegate('Auryn\Test\TestDependency', $badDelegate); } @@ -416,7 +420,10 @@ public function testUnknownDelegationFunction() $injector->delegate('Auryn\Test\DelegatableInterface', 'FunctionWhichDoesNotExist'); $this->fail("Delegation was supposed to fail."); } catch (\Auryn\InjectorException $ie) { - $this->assertContains('FunctionWhichDoesNotExist', $ie->getMessage()); + $this->assertStringContainsString( + 'FunctionWhichDoesNotExist', + $ie->getMessage() + ); $this->assertEquals(\Auryn\Injector::E_DELEGATE_ARGUMENT, $ie->getCode()); } } @@ -428,8 +435,14 @@ public function testUnknownDelegationMethod() $injector->delegate('Auryn\Test\DelegatableInterface', array('stdClass', 'methodWhichDoesNotExist')); $this->fail("Delegation was supposed to fail."); } catch (\Auryn\InjectorException $ie) { - $this->assertContains('stdClass', $ie->getMessage()); - $this->assertContains('methodWhichDoesNotExist', $ie->getMessage()); + $this->assertStringContainsString( + 'stdClass', + $ie->getMessage() + ); + $this->assertStringContainsString( + 'methodWhichDoesNotExist', + $ie->getMessage() + ); $this->assertEquals(\Auryn\Injector::E_DELEGATE_ARGUMENT, $ie->getCode()); } } @@ -612,12 +625,20 @@ public function testInterfaceFactoryDelegation() $requiresDelegatedInterface->foo(); } - /** - * @expectedException \Auryn\InjectorException - * @expectedExceptionMessage Could not make Auryn\Test\TestMissingDependency: Class Auryn\Test\TypoInTypehint does not exist - */ public function testMissingAlias() { + $reportedClassname = 'TestMissingDependency'; + $classname = 'Auryn\Test\TypoInType'; + if (PHP_VERSION_ID >= 80000) { + $classname = "\"" . $classname . "\""; + $reportedClassname = 'TypoInType'; + } + + $this->expectException(\Auryn\InjectorException::class); + $this->expectExceptionMessage( + "Could not make Auryn\\Test\\$reportedClassname: Class $classname does not exist" + ); + $injector = new Injector; $testClass = $injector->make('Auryn\Test\TestMissingDependency'); } @@ -738,11 +759,12 @@ public function provideCyclicDependencies() /** * @dataProvider provideCyclicDependencies - * @expectedException \Auryn\InjectionException - * @expectedExceptionCode \Auryn\Injector::E_CYCLIC_DEPENDENCY */ public function testCyclicDependencies($class) { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionCode(\Auryn\Injector::E_CYCLIC_DEPENDENCY); + $injector = new Injector; $injector->make($class); } @@ -790,16 +812,16 @@ public function testDependencyWithDefaultValueThroughShare() $this->assertInstanceOf('StdClass', $instance->dependency); } - /** - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Cannot share class stdclass because it is currently aliased to Auryn\Test\SomeOtherClass - * @expectedExceptionCode \Auryn\Injector::E_ALIASED_CANNOT_SHARE - */ public function testShareAfterAliasException() { $injector = new Injector(); $testClass = new \StdClass(); $injector->alias('StdClass', 'Auryn\Test\SomeOtherClass'); + + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage('Cannot share class stdclass because it is currently aliased to Auryn\Test\SomeOtherClass'); + $this->expectExceptionCode(\Auryn\Injector::E_ALIASED_CANNOT_SHARE); + $injector->share($testClass); } @@ -835,37 +857,34 @@ public function testAliasAfterShareBySharingAliasAllowed() $this->assertEquals($obj, $obj2); } - /** - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Cannot alias class stdclass to Auryn\Test\SomeOtherClass because it is currently shared - * @expectedExceptionCode \Auryn\Injector::E_SHARED_CANNOT_ALIAS - */ public function testAliasAfterShareException() { $injector = new Injector(); $testClass = new \StdClass(); $injector->share($testClass); + + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage('Cannot alias class stdclass to Auryn\Test\SomeOtherClass because it is currently shared'); + $this->expectExceptionCode(\Auryn\Injector::E_SHARED_CANNOT_ALIAS); $injector->alias('StdClass', 'Auryn\Test\SomeOtherClass'); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Cannot instantiate protected/private constructor in class Auryn\Test\HasNonPublicConstructor - * @expectedExceptionCode \Auryn\Injector::E_NON_PUBLIC_CONSTRUCTOR - */ public function testAppropriateExceptionThrownOnNonPublicConstructor() { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('Cannot instantiate protected/private constructor in class Auryn\Test\HasNonPublicConstructor'); + $this->expectExceptionCode(\Auryn\Injector::E_NON_PUBLIC_CONSTRUCTOR); + $injector = new Injector(); $injector->make('Auryn\Test\HasNonPublicConstructor'); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Cannot instantiate protected/private constructor in class Auryn\Test\HasNonPublicConstructorWithArgs - * @expectedExceptionCode \Auryn\Injector::E_NON_PUBLIC_CONSTRUCTOR - */ public function testAppropriateExceptionThrownOnNonPublicConstructorWithArgs() { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('Cannot instantiate protected/private constructor in class Auryn\Test\HasNonPublicConstructorWithArgs'); + $this->expectExceptionCode(\Auryn\Injector::E_NON_PUBLIC_CONSTRUCTOR); + $injector = new Injector(); $injector->make('Auryn\Test\HasNonPublicConstructorWithArgs'); } @@ -873,11 +892,11 @@ public function testAppropriateExceptionThrownOnNonPublicConstructorWithArgs() public function testMakeExecutableFailsOnNonExistentFunction() { $injector = new Injector(); - $this->setExpectedException( - 'Auryn\InjectionException', - 'nonExistentFunction', - \Auryn\Injector::E_INVOKABLE - ); + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('nonExistentFunction'); + $this->expectExceptionCode(\Auryn\Injector::E_INVOKABLE); + + $injector->buildExecutable('nonExistentFunction'); } @@ -885,46 +904,54 @@ public function testMakeExecutableFailsOnNonExistentInstanceMethod() { $injector = new Injector(); $object = new \StdClass(); - $this->setExpectedException( - 'Auryn\InjectionException', - "[object(stdClass), 'nonExistentMethod']", - \Auryn\Injector::E_INVOKABLE - ); + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage("[object(stdClass), 'nonExistentMethod']"); + $this->expectExceptionCode(\Auryn\Injector::E_INVOKABLE); $injector->buildExecutable(array($object, 'nonExistentMethod')); } public function testMakeExecutableFailsOnNonExistentStaticMethod() { $injector = new Injector(); - $this->setExpectedException( - 'Auryn\InjectionException', - "StdClass::nonExistentMethod", - \Auryn\Injector::E_INVOKABLE - ); + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage("StdClass::nonExistentMethod"); + $this->expectExceptionCode(\Auryn\Injector::E_INVOKABLE); + $injector->buildExecutable(array('StdClass', 'nonExistentMethod')); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Invalid invokable: callable or provisional string required - * @expectedExceptionCode \Auryn\Injector::E_INVOKABLE - */ public function testMakeExecutableFailsOnClassWithoutInvoke() { + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('Invalid invokable: callable or provisional string required'); + $this->expectExceptionCode(\Auryn\Injector::E_INVOKABLE); + $injector = new Injector(); $object = new \StdClass(); $injector->buildExecutable($object); } - /** - * @expectedException \Auryn\ConfigException - * @expectedExceptionMessage Invalid alias: non-empty string required at arguments 1 and 2 - * @expectedExceptionCode \Auryn\Injector::E_NON_EMPTY_STRING_ALIAS - */ - public function testBadAlias() + public function testBadAliasFirstArg() + { + $injector = new Injector; + + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage(Injector::M_NON_EMPTY_STRING_ALIAS); + $this->expectExceptionCode(\Auryn\Injector::E_NON_EMPTY_STRING_ALIAS); + + $injector->alias('', 'Auryn\Test\DepImplementation'); + } + + public function testBadAliasSecondArg() { $injector = new Injector(); $injector->share('Auryn\Test\DepInterface'); + + + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage('Invalid alias: non-empty string required at arguments 1 and 2'); + $this->expectExceptionCode(\Auryn\Injector::E_NON_EMPTY_STRING_ALIAS); + $injector->alias('Auryn\Test\DepInterface', ''); } @@ -938,8 +965,8 @@ public function testShareNewAlias() public function testDefineWithBackslashAndMakeWithoutBackslash() { $injector = new Injector(); - $injector->define('Auryn\Test\SimpleNoTypehintClass', array(':arg' => 'tested')); - $testClass = $injector->make('Auryn\Test\SimpleNoTypehintClass'); + $injector->define('Auryn\Test\SimpleNoTypeClass', array(':arg' => 'tested')); + $testClass = $injector->make('Auryn\Test\SimpleNoTypeClass'); $this->assertEquals('tested', $testClass->testParam); } @@ -982,14 +1009,17 @@ public function testInterfaceMutate() /** * Test that custom definitions are not passed through to dependencies. * Surprising things would happen if this did occur. - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage No definition available to provision typeless parameter $foo at position 0 in Auryn\Test\DependencyWithDefinedParam::__construct() declared in Auryn\Test\DependencyWithDefinedParam:: - * @expectedExceptionCode \Auryn\Injector::E_UNDEFINED_PARAM */ public function testCustomDefinitionNotPassedThrough() { $injector = new Injector(); $injector->share('Auryn\Test\DependencyWithDefinedParam'); + + $this->expectException(\Auryn\InjectionException::class); + // TODO - why does this message end with double-colon? + $this->expectExceptionMessage('No definition available to provision typeless parameter $foo at position 0 in Auryn\Test\DependencyWithDefinedParam::__construct() declared in Auryn\Test\DependencyWithDefinedParam::'); + $this->expectExceptionCode(\Auryn\Injector::E_UNDEFINED_PARAM); + $injector->make('Auryn\Test\RequiresDependencyWithDefinedParam', array(':foo' => 5)); } @@ -1115,11 +1145,6 @@ public function testInspectAll() $this->assertCount(2, array_filter($some)); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Making auryn\test\someclassname did not result in an object, instead result is of type 'NULL' - * @expectedExceptionCode \Auryn\Injector::E_MAKING_FAILED - */ public function testDelegationDoesntMakeObject() { $delegate = function () { @@ -1127,14 +1152,14 @@ public function testDelegationDoesntMakeObject() }; $injector = new Injector(); $injector->delegate('Auryn\Test\SomeClassName', $delegate); + + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('Making auryn\test\someclassname did not result in an object, instead result is of type \'NULL\''); + $this->expectExceptionCode(\Auryn\Injector::E_MAKING_FAILED); + $injector->make('Auryn\Test\SomeClassName'); } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionMessage Making auryn\test\someclassname did not result in an object, instead result is of type 'string' - * @expectedExceptionCode \Auryn\Injector::E_MAKING_FAILED - */ public function testDelegationDoesntMakeObjectMakesString() { $delegate = function () { @@ -1142,6 +1167,11 @@ public function testDelegationDoesntMakeObjectMakesString() }; $injector = new Injector(); $injector->delegate('Auryn\Test\SomeClassName', $delegate); + + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('Making auryn\test\someclassname did not result in an object, instead result is of type \'string\''); + $this->expectExceptionCode(\Auryn\Injector::E_MAKING_FAILED); + $injector->make('Auryn\Test\SomeClassName'); } @@ -1149,7 +1179,9 @@ public function testPrepareInvalidCallable() { $injector = new Injector; $invalidCallable = 'This_does_not_exist'; - $this->setExpectedException('Auryn\InjectionException', $invalidCallable); + $this->expectException(\Auryn\ConfigException::class); + $this->expectExceptionMessage($invalidCallable); + $injector->prepare("StdClass", $invalidCallable); } @@ -1197,15 +1229,15 @@ public function testChildWithoutConstructorWorks() { } } - /** - * @expectedException \Auryn\InjectionException - * @expectedExceptionCode \Auryn\Injector::E_UNDEFINED_PARAM - * @expectedExceptionMessage No definition available to provision typeless parameter $foo at position 0 in Auryn\Test\ChildWithoutConstructor::__construct() declared in Auryn\Test\ParentWithConstructor - */ public function testChildWithoutConstructorMissingParam() { $injector = new Injector; $injector->define('Auryn\Test\ParentWithConstructor', array(':foo' => 'parent')); + + + $this->expectException(\Auryn\InjectionException::class); + $this->expectExceptionMessage('No definition available to provision typeless parameter $foo at position 0 in Auryn\Test\ChildWithoutConstructor::__construct() declared in Auryn\Test\ParentWithConstructor'); + $injector->make('Auryn\Test\ChildWithoutConstructor'); } @@ -1226,20 +1258,246 @@ public function testInstanceClosureDelegates() $this->assertInstanceOf('Auryn\Test\DelegateB', $b->b); } - /** - * @expectedException \Exception - * @expectedExceptionMessage Exception in constructor - */ + public function testThatExceptionInConstructorDoesntCauseCyclicDependencyException() { $injector = new Injector; try { $injector->make('Auryn\Test\ThrowsExceptionInConstructor'); + } catch (\Exception $e) { } - catch (\Exception $e) { - } + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Exception in constructor'); $injector->make('Auryn\Test\ThrowsExceptionInConstructor'); } + + public function testProvidesExtensionsOfArrayMap() + { + $injector = new Injector; + $obj = $injector->make('\Auryn\Test\ExtendedExtendedArrayObject'); + + $this->assertInstanceOf('\ArrayObject', $obj); + } + + // interpret the param as an invokable delegate + public function testMakeWithParameter_delegate() + { + $value = 'testMakeWithParameter_delegate'; + $closure_was_called = false; + $fn = function () use (&$closure_was_called, $value) { + $closure_was_called = true; + return \Auryn\Test\PrefixDelegateTestDependency::create($value); + }; + + $injector = new Injector; + $object = $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_DELEGATE . 'b' => $fn] + ); + $this->assertInstanceOf( + \Auryn\Test\PrefixDelegateTest::class, + $object + ); + $this->assertTrue($closure_was_called); + $this->assertSame($value, $object->getB()->getValue()); + } + + // // interpret the param as a raw value to be injected + public function testMakeWithParameter_raw() + { + $value = 'testMakeWithParameter_raw'; + $injector = new Injector; + $object = $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_RAW . 'b' => \Auryn\Test\PrefixDelegateTestDependency::create($value)] + ); + $this->assertInstanceOf( + \Auryn\Test\PrefixDelegateTest::class, + $object + ); + + $this->assertSame($value, $object->getB()->getValue()); + } + + // interpret the param as a class definition + public function testMakeWithParameter_define() + { + $injector = new Injector; + $object = $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_DEFINE . 'b' => [\Auryn\Test\PrefixDelegateTestDependencyInstantiable::class, []]] + ); + $this->assertInstanceOf( + \Auryn\Test\PrefixDelegateTest::class, + $object + ); + + $this->assertSame('this is the child class', $object->getB()->getValue()); + } + + /** + * interpret the param as a class definition + * @return void + * @throws \Auryn\InjectionException + */ + public function testMakeWithParameter_define_uses_info() + { + $message = "great success"; + $injector = new Injector; + + $params = [ + \Auryn\Test\PrefixDefineDependency::class, + [Injector::A_RAW . 'message' => $message] + ]; + + $object = $injector->make( + \Auryn\Test\PrefixDefineTest::class, + [Injector::A_DEFINE . 'pdd' => $params] + ); + + $this->assertInstanceOf( + \Auryn\Test\PrefixDefineTest::class, + $object + ); + + $dependency = $object->getPdd(); + $this->assertInstanceOf(\Auryn\Test\PrefixDefineDependency::class, $dependency); + $this->assertSame($message, $dependency->message); + } + + public function testIndexedArrayElementOverridesNamedPostion() + { + $message = "This is used."; + $params = [ + Injector::A_RAW . 'message' => "This is not used", + 0 => $message + ]; + $injector = new Injector; + $object = $injector->make( + \Auryn\Test\PrefixDefineDependency::class, + $params + ); + + $this->assertInstanceOf( + \Auryn\Test\PrefixDefineDependency::class, + $object + ); + + $this->assertSame($message, $object->message); + } + + public function testMakeWithParameter_define_errors_not_array() + { + $injector = new Injector; + $this->expectExceptionCode(Injector::E_INVALID_DEFINE_ARGUMENT_NOT_ARRAY); + $this->expectExceptionMessageMatchesTemplateString( + Injector::M_INVALID_DEFINE_ARGUMENT_NOT_ARRAY + ); + $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_DEFINE . 'b' => 'this is not an array'] + ); + } + + public function testMakeWithParameter_define_errors_bad_indexed_array_empty() + { + $injector = new Injector; + $this->expectExceptionCode(Injector::E_INVALID_DEFINE_ARGUMENT_BAD_KEYS); + $this->expectExceptionMessageMatchesTemplateString( + Injector::M_INVALID_DEFINE_ARGUMENT_BAD_KEYS + ); + + $this->expectExceptionMessageContains("array key 0 not set array key 1 not set"); + + $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_DEFINE . 'b' => []] + ); + } + + public function testMakeWithParameter_define_errors_bad_indexed_array_wrong_position() + { + $injector = new Injector; + $this->expectExceptionCode(Injector::E_INVALID_DEFINE_ARGUMENT_BAD_KEYS); + $this->expectExceptionMessageMatchesTemplateString( + Injector::M_INVALID_DEFINE_ARGUMENT_BAD_KEYS + ); + + $this->expectExceptionMessageContains("array key 1 not set"); + + $params = [ + \Auryn\Test\PrefixDelegateTestDependencyInstantiable::class, + 2 => [] + ]; + $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_DEFINE . 'b' => $params] + ); + } + + public function testMakeWithParameter_delegate_errors_not_callable() + { + $injector = new Injector; + $this->expectExceptionCode(Injector::E_INVOKABLE); + $this->expectExceptionMessage(Injector::M_INVOKABLE); + $injector->make( + \Auryn\Test\PrefixDelegateTest::class, + [Injector::A_DELEGATE . 'b' => 'this is not callable'] + ); + } + + public function testDoubleShareClassThrows() + { + $injector = new Injector; + $injector->share(new \StdClass); + + $this->expectExceptionCode(Injector::E_DOUBLE_SHARE); + $this->expectExceptionMessageMatchesTemplateString(Injector::M_DOUBLE_SHARE); + $this->expectExceptionMessageContains('stdclass'); + + $injector->share(new \StdClass); + } + + + /** + * This test is duplication of other tests. It is present to check + * that the behaviour of three different ways of params being null-ish + * are consistent. + * + * @requires PHP 8.0 + */ + public function testNullConsistency() + { + require_once __DIR__ . "/fixtures_8_0.php"; + + $injector = new Injector; + $obj = $injector->make(\NullableDependency::class); + $this->assertInstanceOf(\NullableDependency::class, $obj); + $this->assertNull($obj->instance); + + $obj = $injector->make(\UnionNullDependency::class); + $this->assertInstanceOf(\UnionNullDependency::class, $obj); + $this->assertNull($obj->instance); + + $obj = $injector->make(\DefaultNullDependency::class); + $this->assertInstanceOf(\DefaultNullDependency::class, $obj); + $this->assertNull($obj->instance); + } + + /** + * @requires PHP 8.1 + */ + public function testNewInIntializer() + { + require_once __DIR__ . "/fixtures_8_1.php"; + + $injector = new Injector; + $obj = $injector->make(\NewInInitializer::class); + + $this->assertInstanceOf(\NewInInitializer::class, $obj); + $this->assertInstanceOf(\NewInInitializerDependency::class, $obj->instance); + } } diff --git a/test/fixtures.php b/test/fixtures.php index c562e0a..c92a526 100644 --- a/test/fixtures.php +++ b/test/fixtures.php @@ -158,13 +158,15 @@ class SpecdTestDependency extends TestDependency class TestNeedsDep { + public $testDep; + public function __construct(TestDependency $testDep) { $this->testDep = $testDep; } } -class TestClassWithNoCtorTypehints +class TestClassWithNoCtorTypes { public function __construct($val = 42) { @@ -184,6 +186,7 @@ public function __construct(TestDependency $val1, TestDependency2 $val2) class TestMultiDepsWithCtor { + private $testDep; public function __construct(TestDependency $val1, TestNeedsDep $val2) { $this->testDep = $val1; @@ -191,7 +194,7 @@ public function __construct(TestDependency $val1, TestNeedsDep $val2) } } -class NoTypehintNullDefaultConstructorClass +class NoTypeNullDefaultConstructorClass { public $testParam = 1; public function __construct(TestDependency $val1, $arg=42) @@ -200,7 +203,7 @@ public function __construct(TestDependency $val1, $arg=42) } } -class NoTypehintNoDefaultConstructorClass +class NoTypeNoDefaultConstructorClass { public $testParam = 1; public function __construct(TestDependency $val1, $arg = null) @@ -231,6 +234,7 @@ class DepImplementation implements DepInterface class RequiresInterface { public $dep; + public $testDep; public function __construct(DepInterface $dep) { $this->testDep = $dep; @@ -262,6 +266,7 @@ public function __construct(ClassInnerA $dep) class ProvTestNoDefinitionNullDefaultClass { + public $arg; public function __construct($arg = null) { $this->arg = $arg; @@ -272,7 +277,7 @@ interface TestNoExplicitDefine { } -class InjectorTestCtorParamWithNoTypehintOrDefault implements TestNoExplicitDefine +class InjectorTestCtorParamWithNoTypeOrDefault implements TestNoExplicitDefine { public $val = 42; public function __construct($val) @@ -281,7 +286,7 @@ public function __construct($val) } } -class InjectorTestCtorParamWithNoTypehintOrDefaultDependent +class InjectorTestCtorParamWithNoTypeOrDefaultDependent { private $param; public function __construct(TestNoExplicitDefine $param) @@ -314,6 +319,7 @@ public function __construct($string, $obj, $int, $array, $float, $bool, $null) class InjectorTestParentClass { + public $arg1; public function __construct($arg1) { $this->arg1 = $arg1; @@ -322,6 +328,8 @@ public function __construct($arg1) class InjectorTestChildClass extends InjectorTestParentClass { + public $arg1; + public $arg2; public function __construct($arg1, $arg2) { parent::__construct($arg1); @@ -336,7 +344,7 @@ public function __invoke() } } -class ProviderTestCtorParamWithNoTypehintOrDefault implements TestNoExplicitDefine +class ProviderTestCtorParamWithNoTypeOrDefault implements TestNoExplicitDefine { public $val = 42; public function __construct($val) @@ -345,7 +353,7 @@ public function __construct($val) } } -class ProviderTestCtorParamWithNoTypehintOrDefaultDependent +class ProviderTestCtorParamWithNoTypeOrDefaultDependent { private $param; public function __construct(TestNoExplicitDefine $param) @@ -489,7 +497,7 @@ public function foo() class TestMissingDependency { - public function __construct(TypoInTypehint $class) + public function __construct(TypoInType $class) { } } @@ -573,13 +581,14 @@ public static function create() class TestNeedsDepWithProtCons { + public $dep; public function __construct(TestDependencyWithProtectedConstructor $dep) { $this->dep = $dep; } } -class SimpleNoTypehintClass +class SimpleNoTypeClass { public $testParam = 1; @@ -595,12 +604,12 @@ class SomeClassName class TestDelegationSimple { - public $delgateCalled = false; + public $delegateCalled = false; } class TestDelegationDependency { - public $delgateCalled = false; + public $delegateCalled = false; public function __construct(TestDelegationSimple $testDelegationSimple) { } @@ -721,6 +730,7 @@ class ChildWithoutConstructor extends ParentWithConstructor { class DelegateA {} class DelegatingInstanceA { + public $a; public function __construct(DelegateA $a) { $this->a = $a; } @@ -728,13 +738,88 @@ public function __construct(DelegateA $a) { class DelegateB {} class DelegatingInstanceB { + public $b; public function __construct(DelegateB $b) { $this->b = $b; } } + class ThrowsExceptionInConstructor { public function __construct() { throw new \Exception('Exception in constructor'); } } + +class ExtendedArrayObject extends \ArrayObject {} +class ExtendedExtendedArrayObject extends ExtendedArrayObject {} + +class PrefixDelegateTestDependency { + private $value; + protected function __construct($value) + { + $this->value = $value; + } + public static function create($value) + { + return new static($value); + } + + public function getValue() + { + return $this->value; + } +} + +class PrefixDelegateTestDependencyInstantiable extends PrefixDelegateTestDependency +{ + public function __construct() + { + parent::__construct('this is the child class'); + } +} + +class PrefixDelegateTest +{ + private $b; + public function __construct(PrefixDelegateTestDependency $b) + { + $this->b = $b; + } + public function getB() + { + return $this->b; + } +} + +class PrefixDefineDependency { + public $message; + public function __construct($message) + { + $this->message = $message; + } +} + +class PrefixDefineTest +{ + private $pdd; + public function __construct(PrefixDefineDependency $pdd) + { + $this->pdd = $pdd; + } + public function getPdd() + { + return $this->pdd; + } +} + +function aFunctionWithAParam($foo) +{ + return $foo; +} + + +class ExecutableHelper +{ + public function foo() {} +} \ No newline at end of file diff --git a/test/fixtures_5_6.php b/test/fixtures_5_6.php index f23dda5..0f85ea3 100644 --- a/test/fixtures_5_6.php +++ b/test/fixtures_5_6.php @@ -2,7 +2,7 @@ namespace Auryn\Test; -class NoTypehintNoDefaultConstructorVariadicClass +class NoTypeNoDefaultConstructorVariadicClass { public $testParam = 1; public function __construct(TestDependency $val1, ...$arg) @@ -11,7 +11,7 @@ public function __construct(TestDependency $val1, ...$arg) } } -class TypehintNoDefaultConstructorVariadicClass +class TypeNoDefaultConstructorVariadicClass { public $testParam = 1; public function __construct(TestDependency ...$arg) diff --git a/test/fixtures_8_0.php b/test/fixtures_8_0.php new file mode 100644 index 0000000..a1c3354 --- /dev/null +++ b/test/fixtures_8_0.php @@ -0,0 +1,37 @@ +instance = $instance; + } +} + +class UnionNullDependency +{ + public ?Dependency $instance; + + public function __construct(?Dependency $instance = null) + { + $this->instance = $instance; + } +} + + + +class DefaultNullDependency +{ + public ?Dependency $instance; + + public function __construct(Dependency $instance = null) + { + $this->instance = $instance; + } +} \ No newline at end of file diff --git a/test/fixtures_8_1.php b/test/fixtures_8_1.php new file mode 100644 index 0000000..d6b4d4b --- /dev/null +++ b/test/fixtures_8_1.php @@ -0,0 +1,16 @@ +instance = $instance; + } +} + + + + +