├── 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 | [](https://travis-ci.org/acelot/resolver)
4 | [](https://codeclimate.com/github/acelot/resolver)
5 | 
6 | 
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 |