├── LICENSE.md ├── README.md ├── composer.json ├── psalm.xml └── src ├── Definition ├── FactoryDefinition.php ├── ObjectDefinition.php ├── Traits │ ├── ArgumentsTrait.php │ └── ShareTrait.php └── ValueDefinition.php ├── DefinitionInterface.php ├── Exception ├── DefinitionException.php └── ResolverException.php ├── InvokerInterface.php ├── Resolver.php ├── ResolverInterface.php └── functions.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Valeriy Protopopov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resolver 2 | 3 | [![Build Status](https://travis-ci.org/acelot/resolver.svg?branch=master)](https://travis-ci.org/acelot/resolver) 4 | [![Code Climate](https://img.shields.io/codeclimate/coverage/acelot/resolver.svg)](https://codeclimate.com/github/acelot/resolver) 5 | ![](https://img.shields.io/badge/dependencies-zero-blue.svg) 6 | ![](https://img.shields.io/badge/license-MIT-green.svg) 7 | 8 | **Resolver** is a dependency auto resolver for PHP 7 and 8. Supports PSR-11 `ContainerInterface`. 9 | 10 | ### Installation 11 | 12 | ``` 13 | composer require acelot/resolver 14 | ``` 15 | 16 | ### Why? 17 | 18 | Imagine that you have a controller: 19 | 20 | ```php 21 | class UsersController 22 | { 23 | public function __construct(UsersService $service) 24 | { 25 | // ... 26 | } 27 | } 28 | ``` 29 | 30 | As you can see the controller requires `UsersService` in constructor. To resolve this dependency you can just pass 31 | the new instance of `UsersService`. Let's do this: 32 | 33 | ```php 34 | $service = new UsersService(); 35 | $controller = new UsersController($service); 36 | ``` 37 | 38 | It doesn't work, because `UsersService`, in turn, requires `UsersRepository` to access the data. 39 | 40 | ```php 41 | class UsersService 42 | { 43 | public function __construct(UsersRepository $repository) 44 | { 45 | // ... 46 | } 47 | } 48 | ``` 49 | 50 | Okay, let's create the repository instance! 51 | 52 | ```php 53 | $repository = new UsersRepository(); 54 | $service = new UsersService($repository); 55 | $controller = new UsersController($service); 56 | ``` 57 | 58 | Sadly, it still doesn't work, because we encountering the new dependency! The repository, surprisingly, requires 59 | a database connection :) 60 | 61 | ```php 62 | class UsersRepository 63 | { 64 | public function __construct(Database $db) 65 | { 66 | // ... 67 | } 68 | } 69 | ``` 70 | 71 | You say "Eat this!". 72 | 73 | ```php 74 | $db = new Database('connection string here'); 75 | $repository = new UsersRepository($db); 76 | $service = new UsersService($repository); 77 | $controller = new UsersController($service); 78 | ``` 79 | 80 | Success! We have finally created the instance of `UsersController`! 81 | Now imagine that you have ten or hundred controllers like this?! 82 | With **Resolver** you can greatly simplify creation of classes. 83 | In what turns this code using **Resolver**: 84 | 85 | ```php 86 | $resolver = new Resolver([ 87 | Database::class => ObjectDefinition::define(Database::class)->withArgument('connectionString', 'connection string here') 88 | ]); 89 | 90 | $controller = $resolver->resolve(UsersController::class); 91 | ``` 92 | 93 | And it's all. 94 | 95 | 96 | ### How it works? 97 | 98 | **Resolver** resolves the classes by using [Reflection](http://php.net/manual/ru/book.reflection.php). 99 | Through reflection the **Resolver** finds out all dependencies of the class and all dependencies of 100 | dependencies and so on. When **Resolver** reaches the deepest dependency it starts creating instances 101 | of these one by one until the top class. The resolved classes are stored in local array to avoid re-resolving. 102 | 103 | ### Available definitions 104 | 105 | - FactoryDefinition (short alias `factory()`) 106 | - ObjectDefinition (short alias `object()`) 107 | - ValueDefinition (short alias `value()`) 108 | 109 | ### Instance sharing 110 | 111 | Resolved definitions can be shared between calls via `->shared()` method. This method available in `FactoryDefinition` and `ObjectDefinition`. `ValueDefinition` is shared always by design. 112 | 113 | --- 114 | 115 | **Resolver** (c) by Valeriy Protopopov. 116 | 117 | **Resolver** is licensed under a MIT license. 118 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "acelot/resolver", 3 | "description": "Dependency auto resolver for PHP 7 and 8", 4 | "keywords": [ 5 | "dependency-injection", 6 | "resolver", 7 | "inversion-of-control" 8 | ], 9 | "type": "library", 10 | "homepage": "https://github.com/acelot/resolver", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Valeriy Protopopov", 15 | "email": "provaleriy@gmail.com" 16 | } 17 | ], 18 | "require": { 19 | "php": "^7.3 || ^8.0", 20 | "psr/container": "^1.0" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^9.0", 24 | "vimeo/psalm": "^4.8" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Acelot\\Resolver\\": "src/" 29 | }, 30 | "files": [ 31 | "src/functions.php" 32 | ] 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Acelot\\Resolver\\Tests\\": "tests/" 37 | } 38 | }, 39 | "scripts": { 40 | "test": "phpunit -c phpunit.xml", 41 | "psalm": "psalm" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Definition/FactoryDefinition.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 47 | $this->isShared = true; 48 | } 49 | 50 | /** 51 | * @return callable 52 | */ 53 | public function getCallable(): callable 54 | { 55 | return $this->callable; 56 | } 57 | 58 | /** 59 | * @param callable $callable 60 | * 61 | * @return FactoryDefinition 62 | */ 63 | public function withCallable(callable $callable): FactoryDefinition 64 | { 65 | $clone = clone $this; 66 | $clone->callable = $callable; 67 | 68 | return $clone; 69 | } 70 | 71 | /** 72 | * Resolves and invoke the callable. 73 | * 74 | * @param ResolverInterface $resolver 75 | * 76 | * @return object 77 | * @throws DefinitionException 78 | */ 79 | public function resolve(ResolverInterface $resolver) 80 | { 81 | $type = self::getCallableType($this->callable); 82 | 83 | switch ($type) { 84 | case self::TYPE_CLOSURE: 85 | case self::TYPE_STRING: 86 | /** @var \Closure|callable-string $callable */ 87 | $callable = $this->callable; 88 | $ref = new \ReflectionFunction($callable); 89 | $args = iterator_to_array($this->resolveParameters($ref->getParameters(), $resolver)); 90 | 91 | return call_user_func($this->callable, ...$args); 92 | 93 | case self::TYPE_OBJECT: 94 | /** @var object $callable */ 95 | $callable = $this->callable; 96 | $ref = new \ReflectionMethod($callable, '__invoke'); 97 | $args = iterator_to_array($this->resolveParameters($ref->getParameters(), $resolver)); 98 | 99 | return call_user_func($this->callable, ...$args); 100 | 101 | case self::TYPE_ARRAY_OBJECT: 102 | /** @var array $callable */ 103 | $callable = $this->callable; 104 | /** @var object $callableObject */ 105 | $callableObject = $callable[0]; 106 | /** @var string $callableMethod */ 107 | $callableMethod = $callable[1]; 108 | $ref = new \ReflectionMethod($callableObject, $callableMethod); 109 | $args = iterator_to_array($this->resolveParameters($ref->getParameters(), $resolver)); 110 | 111 | return $ref->invoke($callableObject, ...$args); 112 | 113 | case self::TYPE_ARRAY: 114 | /** @var array $callable */ 115 | $callable = $this->callable; 116 | /** @var class-string $fqcn */ 117 | $fqcn = $callable[0]; 118 | /** @var string $method */ 119 | $method = $callable[1]; 120 | if ($method === '__construct') { 121 | throw new DefinitionException('Use ObjectDefinition instead of FactoryDefinition'); 122 | } 123 | 124 | $ref = new \ReflectionMethod($fqcn, $method); 125 | $args = iterator_to_array($this->resolveParameters($ref->getParameters(), $resolver)); 126 | 127 | if ($ref->isStatic()) { 128 | return call_user_func($this->callable, ...$args); 129 | } 130 | 131 | return $ref->invoke($resolver->resolve($fqcn), ...$args); 132 | 133 | case self::TYPE_STRING_SEPARATED: 134 | /** @var string $callable */ 135 | $callable = $this->callable; 136 | 137 | /** @var class-string $fqcn */ 138 | list($fqcn, $method) = explode('::', $callable); 139 | if ($method === '__construct') { 140 | throw new DefinitionException('Use ObjectDefinition instead of FactoryDefinition'); 141 | } 142 | 143 | $ref = new \ReflectionMethod($fqcn, $method); 144 | $args = iterator_to_array($this->resolveParameters($ref->getParameters(), $resolver)); 145 | 146 | return call_user_func($this->callable, ...$args); 147 | 148 | default: 149 | throw new DefinitionException('Unknown callable type'); 150 | } 151 | } 152 | 153 | /** 154 | * Returns the type of callable. 155 | * 156 | * @param callable $callable 157 | * 158 | * @return int 159 | * 160 | * @psalm-suppress RedundantCondition 161 | */ 162 | protected static function getCallableType(callable $callable): int 163 | { 164 | // Closure 165 | if ($callable instanceof \Closure) { 166 | return self::TYPE_CLOSURE; 167 | } 168 | 169 | // Object 170 | if (is_object($callable)) { 171 | return self::TYPE_OBJECT; 172 | } 173 | 174 | // Array 175 | if (is_array($callable)) { 176 | if (is_object($callable[0])) { 177 | return self::TYPE_ARRAY_OBJECT; 178 | } 179 | 180 | return self::TYPE_ARRAY; 181 | } 182 | 183 | // String 184 | if (is_string($callable)) { 185 | if (strpos($callable, '::') !== false) { 186 | return self::TYPE_STRING_SEPARATED; 187 | } 188 | 189 | return self::TYPE_STRING; 190 | } 191 | 192 | return self::TYPE_UNKNOWN; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Definition/ObjectDefinition.php: -------------------------------------------------------------------------------- 1 | fqcn = $fqcn; 39 | } 40 | 41 | /** 42 | * Resolves and returns the instance of the class. 43 | * 44 | * @param ResolverInterface $resolver 45 | * 46 | * @return object 47 | * @throws DefinitionException 48 | */ 49 | public function resolve(ResolverInterface $resolver) 50 | { 51 | if (!class_exists($this->fqcn)) { 52 | throw new DefinitionException(sprintf('The class "%s" does not exists', $this->fqcn)); 53 | } 54 | 55 | try { 56 | $ref = new \ReflectionMethod($this->fqcn, '__construct'); 57 | } catch (\ReflectionException $e) { 58 | return new $this->fqcn(); 59 | } 60 | 61 | $args = $this->resolveParameters($ref->getParameters(), $resolver); 62 | 63 | return new $this->fqcn(...$args); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Definition/Traits/ArgumentsTrait.php: -------------------------------------------------------------------------------- 1 | args); 25 | } 26 | 27 | /** 28 | * Returns the argument value. 29 | * 30 | * @param string $name Argument name 31 | * 32 | * @return mixed 33 | * @throws \OutOfBoundsException 34 | */ 35 | public function getArgument(string $name) 36 | { 37 | if (!$this->hasArgument($name)) { 38 | throw new \OutOfBoundsException(sprintf('The argument "%s" is not exists', $name)); 39 | } 40 | 41 | return $this->args[$name]; 42 | } 43 | 44 | /** 45 | * Returns the new instance of the trait holding class with new argument. 46 | * 47 | * @param string $name Argument name 48 | * @param mixed $value Argument value 49 | * 50 | * @return static 51 | */ 52 | public function withArgument(string $name, $value) 53 | { 54 | $clone = clone $this; 55 | $clone->args[$name] = $value; 56 | 57 | return $clone; 58 | } 59 | 60 | /** 61 | * Returns the new instance of the trait holding class without argument. 62 | * 63 | * @param string $name 64 | * 65 | * @return static 66 | */ 67 | public function withoutArgument(string $name) 68 | { 69 | $clone = clone $this; 70 | unset($clone->args[$name]); 71 | 72 | return $clone; 73 | } 74 | 75 | /** 76 | * Returns the new instance of the trait holding class with new arguments. 77 | * 78 | * @param array $args Arguments 79 | * 80 | * @return static 81 | */ 82 | public function withArguments(array $args) 83 | { 84 | $clone = clone $this; 85 | $clone->args = $args; 86 | 87 | return $clone; 88 | } 89 | 90 | /** 91 | * Resolves function parameters of the given function meta information. 92 | * 93 | * @param \ReflectionParameter[] $parameters 94 | * @param ResolverInterface $resolver 95 | * 96 | * @return \Iterator 97 | * @throws DefinitionException 98 | */ 99 | protected function resolveParameters($parameters, ResolverInterface $resolver): \Iterator 100 | { 101 | foreach ($parameters as $param) { 102 | if ($this->hasArgument($param->getName())) { 103 | yield $this->getArgument($param->getName()); 104 | continue; 105 | } 106 | 107 | if ($param->isDefaultValueAvailable()) { 108 | yield $param->getDefaultValue(); 109 | continue; 110 | } 111 | 112 | $paramClass = $param->getType(); 113 | if ($paramClass instanceof \ReflectionNamedType) { 114 | yield $resolver->resolve($paramClass->getName()); 115 | continue; 116 | } 117 | 118 | throw new DefinitionException(sprintf( 119 | 'Cannot resolve the function because parameter "%s" requires unknown value', 120 | $param->getName() 121 | )); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Definition/Traits/ShareTrait.php: -------------------------------------------------------------------------------- 1 | isShared; 18 | } 19 | 20 | /** 21 | * @param bool $shared 22 | * 23 | * @return static 24 | */ 25 | public function shared(bool $shared = true) 26 | { 27 | $clone = clone $this; 28 | $clone->isShared = $shared; 29 | 30 | return $clone; 31 | } 32 | } -------------------------------------------------------------------------------- /src/Definition/ValueDefinition.php: -------------------------------------------------------------------------------- 1 | value = $value; 40 | } 41 | 42 | /** 43 | * Is definition result must be shared between calls. 44 | * 45 | * @return bool 46 | */ 47 | public function isShared(): bool 48 | { 49 | return true; 50 | } 51 | 52 | /** 53 | * Resolves the definition. Simply returns the value. 54 | * 55 | * @param ResolverInterface $resolver 56 | * 57 | * @return object 58 | */ 59 | public function resolve(ResolverInterface $resolver) 60 | { 61 | return $this->value; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/DefinitionInterface.php: -------------------------------------------------------------------------------- 1 | ClassDefinition::define(LoggerFactory::class), 16 | * Database::class => ClassDefinition::define(MongoDbFactory::class) 17 | * ]); 18 | * 19 | */ 20 | class Resolver implements ResolverInterface, InvokerInterface, ContainerInterface 21 | { 22 | private const ALIASES = [ 23 | Resolver::class, 24 | ContainerInterface::class, 25 | ResolverInterface::class, 26 | InvokerInterface::class, 27 | ]; 28 | 29 | /** 30 | * @var array 31 | */ 32 | protected $definitions; 33 | 34 | /** 35 | * @var array 36 | */ 37 | protected $shared = []; 38 | 39 | /** 40 | * @param array $definitions Definitions mapping 41 | * 42 | * @return Resolver 43 | */ 44 | public static function create(array $definitions = []): Resolver 45 | { 46 | return new Resolver($definitions); 47 | } 48 | 49 | /** 50 | * @param array $definitions Definitions mapping 51 | */ 52 | public function __construct(array $definitions = []) 53 | { 54 | foreach ($definitions as $fqcn => $definition) { 55 | if (!$definition instanceof DefinitionInterface) { 56 | throw new \InvalidArgumentException( 57 | sprintf('Definition of "%s" must implement DefinitionInterface', $fqcn) 58 | ); 59 | } 60 | } 61 | 62 | $this->definitions = $definitions; 63 | } 64 | 65 | /** 66 | * Returns all bound definitions. 67 | * 68 | * @return array 69 | */ 70 | public function getDefinitions(): array 71 | { 72 | return $this->definitions; 73 | } 74 | 75 | /** 76 | * Binds the class name to definition. Immutable. 77 | * 78 | * @param string $fqcn Fully qualified class name 79 | * @param DefinitionInterface $definition Definition 80 | * 81 | * @return Resolver 82 | */ 83 | public function withDefinition(string $fqcn, DefinitionInterface $definition): Resolver 84 | { 85 | $clone = clone $this; 86 | $clone->definitions[$fqcn] = $definition; 87 | return $clone; 88 | } 89 | 90 | /** 91 | * Unbinds the definition by class name. Immutable. 92 | * 93 | * @param string $fqcn Fully qualified class name 94 | * 95 | * @return Resolver 96 | */ 97 | public function withoutDefinition(string $fqcn): Resolver 98 | { 99 | $clone = clone $this; 100 | unset($clone->definitions[$fqcn]); 101 | return $clone; 102 | } 103 | 104 | /** 105 | * Resolves and returns the instance of the class. 106 | * 107 | * @param string $fqcn Fully qualified class name 108 | * 109 | * @return object 110 | * @throws DefinitionException 111 | */ 112 | public function resolve(string $fqcn) 113 | { 114 | // Resolver aliases 115 | if (in_array($fqcn, self::ALIASES)) { 116 | return $this; 117 | } 118 | 119 | // Search class in shared 120 | if (array_key_exists($fqcn, $this->shared)) { 121 | return $this->shared[$fqcn]; 122 | } 123 | 124 | // Search definition in predefined definition, otherwise use ObjectDefinition 125 | if (array_key_exists($fqcn, $this->definitions)) { 126 | $definition = $this->definitions[$fqcn]; 127 | } else { 128 | $definition = ObjectDefinition::define($fqcn); 129 | } 130 | 131 | // Resolving 132 | $result = $definition->resolve($this); 133 | 134 | // Sharing result between calls 135 | if ($definition->isShared()) { 136 | $this->shared[$fqcn] = $result; 137 | } 138 | 139 | return $result; 140 | } 141 | 142 | /** 143 | * Invoke a callable. 144 | * 145 | * @param callable $callable Callable 146 | * @param array $args Arguments 147 | * 148 | * @return mixed 149 | * @throws Exception\ResolverException 150 | */ 151 | public function invoke(callable $callable, array $args = []) 152 | { 153 | return FactoryDefinition::define($callable) 154 | ->withArguments($args) 155 | ->resolve($this); 156 | } 157 | 158 | /** 159 | * @param string $id 160 | * 161 | * @return mixed|object 162 | */ 163 | public function get(string $id) 164 | { 165 | return $this->resolve($id); 166 | } 167 | 168 | /** 169 | * @param string $id 170 | * 171 | * @return bool 172 | */ 173 | public function has(string $id) 174 | { 175 | return true; 176 | } 177 | 178 | /** 179 | * Removes the resolved object from shared items. 180 | * This is useful when you want the object to be re-resolved. 181 | * 182 | * @param string $fqcn 183 | */ 184 | public function unshare(string $fqcn): void 185 | { 186 | unset($this->shared[$fqcn]); 187 | } 188 | 189 | /** 190 | * Clear all the resolved objects from shared items. 191 | * @see `removeShared` method 192 | */ 193 | public function unshareAll(): void 194 | { 195 | $this->shared = []; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/ResolverInterface.php: -------------------------------------------------------------------------------- 1 |