├── .github └── workflows │ └── test.yml ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── Container.php ├── Definition.php ├── Exception ├── ConfigException.php ├── DependencyInjectionException.php └── NotFoundException.php ├── ParameterBag.php ├── README.md ├── Reference.php ├── Resolver.php ├── Tests ├── ContainerTest.php ├── DefinitionTest.php ├── ParameterBagTest.php ├── ReferenceTest.php ├── ResolverTest.php └── TestClass │ ├── Actor.php │ ├── ActorInterface.php │ ├── Actress.php │ ├── Bar.php │ ├── Director.php │ ├── Foo.php │ └── Movie.php ├── composer.json └── phpunit.xml.dist /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.operating-system }} 15 | strategy: 16 | max-parallel: 15 17 | matrix: 18 | operating-system: [ubuntu-latest] 19 | php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3'] 20 | name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | 26 | - name: Install PHP 27 | uses: shivammathur/setup-php@v2 28 | with: 29 | php-version: ${{ matrix.php-versions }} 30 | 31 | - name: Check PHP Version 32 | run: php -v 33 | 34 | - name: Validate composer.json and composer.lock 35 | run: composer validate --strict 36 | 37 | - name: Cache Composer packages 38 | id: composer-cache 39 | uses: actions/cache@v3 40 | with: 41 | path: vendor 42 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 43 | restore-keys: | 44 | ${{ runner.os }}-php- 45 | 46 | - name: Install dependencies 47 | run: composer install --prefer-dist --no-progress 48 | 49 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 50 | # Docs: https://getcomposer.org/doc/articles/scripts.md 51 | 52 | - name: Run test suite 53 | run: ./vendor/bin/phpunit --coverage-clover coverage.xml 54 | 55 | - name: Upload coverage to Codecov 56 | uses: codecov/codecov-action@v1 57 | with: 58 | token: ${{ secrets.CODECOV_TOKEN }} 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .settings/ 3 | .project 4 | .buildpath 5 | composer.lock 6 | .idea/ -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - "Tests/*" 4 | - "vendor/*" 5 | 6 | tools: 7 | external_code_coverage: 8 | timeout: 600 9 | 10 | php_sim: true 11 | 12 | php_changetracking: true 13 | 14 | php_cs_fixer: 15 | enabled: true 16 | config: 17 | level: all 18 | filter: 19 | excluded_paths: 20 | - "Tests/*" 21 | - "vendor/*" 22 | 23 | php_mess_detector: 24 | enabled: true 25 | filter: 26 | excluded_paths: 27 | - "Tests/*" 28 | - "vendor/*" 29 | 30 | php_pdepend: 31 | enabled: true 32 | filter: 33 | excluded_paths: 34 | - "Tests/*" 35 | - "vendor/*" 36 | 37 | php_analyzer: 38 | enabled: true 39 | filter: 40 | excluded_paths: 41 | - "Tests/*" 42 | - "vendor/*" 43 | 44 | 45 | php_cpd: 46 | enabled: true 47 | excluded_dirs: 48 | - "Tests/*" 49 | - "vendor/*" 50 | 51 | php_loc: 52 | enabled: true 53 | excluded_dirs: 54 | - "Tests/*" 55 | - "vendor/*" 56 | 57 | build: 58 | nodes: 59 | analysis: 60 | tests: 61 | override: 62 | - php-scrutinizer-run 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.4 5 | - 8.0 6 | - 8.1 7 | - 8.2 8 | - 8.3 9 | 10 | before_script: 11 | - composer install 12 | 13 | script: ./vendor/bin/phpunit --coverage-clover=coverage.xml 14 | 15 | after_success: 16 | - bash <(curl -s https://codecov.io/bash) 17 | 18 | notifications: 19 | email: false -------------------------------------------------------------------------------- /Container.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Slince\Di; 15 | 16 | use Psr\Container\ContainerInterface; 17 | use ReflectionException; 18 | use Slince\Di\Exception\DependencyInjectionException; 19 | use Slince\Di\Exception\NotFoundException; 20 | 21 | class Container implements \ArrayAccess, ContainerInterface 22 | { 23 | /** 24 | * @var array 25 | */ 26 | protected array $aliases = []; 27 | 28 | /** 29 | * Array of Definitions. 30 | * 31 | * @var Definition[] 32 | */ 33 | protected array $definitions = []; 34 | 35 | /** 36 | * @var array 37 | */ 38 | protected array $instances; 39 | 40 | /** 41 | * Array of parameters. 42 | * 43 | * @var ParameterBag 44 | */ 45 | protected ParameterBag $parameters; 46 | 47 | /** 48 | * @var Resolver 49 | */ 50 | protected Resolver $resolver; 51 | 52 | /** 53 | * Defaults for the container. 54 | * 55 | * [ 56 | * 'share' => true, 57 | * 'autowire' => true, 58 | * 'autoregister' => true 59 | * ] 60 | * 61 | * @var array 62 | */ 63 | protected array $defaults = [ 64 | 'share' => true, 65 | 'autowire' => true, 66 | 'autoregister' => true 67 | ]; 68 | 69 | public function __construct() 70 | { 71 | $this->parameters = new ParameterBag(); 72 | $this->resolver = new Resolver($this); 73 | $this->register($this); 74 | } 75 | 76 | /** 77 | * Determine if a given offset exists. 78 | * 79 | * @param string $key 80 | * 81 | * @return bool 82 | */ 83 | public function offsetExists($key): bool 84 | { 85 | return $this->has($key); 86 | } 87 | 88 | /** 89 | * Get the value at a given offset. 90 | * 91 | * @param string $key 92 | * 93 | * @return mixed 94 | */ 95 | public function offsetGet($key): object 96 | { 97 | return $this->get($key); 98 | } 99 | 100 | /** 101 | * Set the value at a given offset. 102 | * 103 | * @param string $key 104 | * @param mixed $value 105 | */ 106 | public function offsetSet($key, $value): void 107 | { 108 | $this->register($key, $value); 109 | } 110 | 111 | /** 112 | * Unset the value at a given offset. 113 | * 114 | * @param string $key 115 | */ 116 | public function offsetUnset($key): void 117 | { 118 | unset($this->definitions[$key], $this->instances[$key]); 119 | } 120 | 121 | /** 122 | * Register a definition. 123 | * 124 | * @param string|object $id 125 | * @param mixed $concrete 126 | * 127 | * @return Definition 128 | */ 129 | public function register($id, $concrete = null): Definition 130 | { 131 | if (null === $concrete) { 132 | $concrete = $id; 133 | } 134 | if (is_object($id)) { 135 | $id = get_class($id); 136 | } 137 | //Apply defaults. 138 | $definition = (new Definition($concrete)) 139 | ->setShared($this->defaults['share']) 140 | ->setAutowired($this->defaults['autowire']); 141 | 142 | return $this->setDefinition($id, $definition); 143 | } 144 | 145 | /** 146 | * Set a definition. 147 | * 148 | * @param string $id 149 | * @param Definition $definition 150 | * 151 | * @return Definition 152 | */ 153 | public function setDefinition(string $id, Definition $definition): Definition 154 | { 155 | unset($this->aliases[$id]); 156 | return $this->definitions[$id] = $definition; 157 | } 158 | 159 | /** 160 | * Adds the service definitions. 161 | * 162 | * @param Definition[] $definitions An array of service definitions 163 | */ 164 | public function addDefinitions(array $definitions) 165 | { 166 | foreach ($definitions as $id => $definition) { 167 | $this->setDefinition($id, $definition); 168 | } 169 | } 170 | 171 | /** 172 | * Sets the service definitions. 173 | * 174 | * @param Definition[] $definitions An array of service definitions 175 | */ 176 | public function setDefinitions(array $definitions) 177 | { 178 | $this->definitions = []; 179 | $this->addDefinitions($definitions); 180 | } 181 | 182 | /** 183 | * Checks whether the definition exists. 184 | * 185 | * @param string $id 186 | * @return bool 187 | */ 188 | public function hasDefinition(string $id): bool 189 | { 190 | return isset($this->definitions[$id]); 191 | } 192 | 193 | /** 194 | * Sets an alias for an existing service. 195 | * 196 | * @param string $alias 197 | * @param string $id 198 | */ 199 | public function setAlias(string $alias, string $id) 200 | { 201 | $this->aliases[$alias] = $id; 202 | } 203 | 204 | /** 205 | * Get id of the alias. 206 | * 207 | * @param string $alias 208 | * 209 | * @return string|null 210 | */ 211 | public function getAlias(string $alias): ?string 212 | { 213 | return $this->aliases[$alias] ?? null; 214 | } 215 | 216 | /** 217 | * Get a service instance by specified ID. 218 | * 219 | * @param string $id 220 | * 221 | * @return object 222 | * @throws DependencyInjectionException 223 | */ 224 | public function get(string $id): object 225 | { 226 | $id = $this->resolveAlias($id); 227 | 228 | if (isset($this->instances[$id])) { 229 | return $this->instances[$id]; 230 | } 231 | 232 | return $this->resolveInstance($id); 233 | } 234 | 235 | /** 236 | * Get a service instance by specified ID. 237 | * 238 | * @param string $id 239 | * 240 | * @return object 241 | * @throws DependencyInjectionException 242 | */ 243 | public function getNew(string $id): object 244 | { 245 | $id = $this->resolveAlias($id); 246 | 247 | return $this->resolveInstance($id); 248 | } 249 | 250 | protected function resolveAlias(string $id): string 251 | { 252 | if (isset($this->aliases[$id])) { 253 | $id = $this->aliases[$id]; 254 | } 255 | return $id; 256 | } 257 | 258 | /** 259 | * @throws DependencyInjectionException 260 | */ 261 | protected function resolveInstance(string $id): object 262 | { 263 | if (!$this->has($id)) { 264 | throw new NotFoundException(sprintf('There is no definition named "%s"', $id)); 265 | } 266 | // resolve instance. 267 | $instance = $this->resolver->resolve($this->definitions[$id]); 268 | if ($this->definitions[$id]->isShared()) { 269 | $this->instances[$id] = $instance; 270 | } 271 | 272 | return $instance; 273 | } 274 | 275 | /** 276 | * {@inheritdoc} 277 | */ 278 | public function has($id): bool 279 | { 280 | if (!$this->hasDefinition($id)) { 281 | //If there is no matching definition, creates a definition. 282 | if ($autoRegistrable = ($this->defaults['autoregister'] && class_exists($id))) { 283 | $this->register($id); 284 | } 285 | return $autoRegistrable; 286 | } 287 | return true; 288 | } 289 | 290 | /** 291 | * Extends a definition. 292 | * 293 | * @param string $id 294 | * 295 | * @return Definition 296 | * @throws DependencyInjectionException 297 | */ 298 | public function extend(string $id): Definition 299 | { 300 | if (!$this->hasDefinition($id)) { 301 | throw new NotFoundException(sprintf('There is no definition named "%s"', $id)); 302 | } 303 | $definition = $this->definitions[$id]; 304 | if ($definition->getResolved()) { 305 | throw new DependencyInjectionException(sprintf('Cannot override frozen service "%s".', $id)); 306 | } 307 | 308 | return $definition; 309 | } 310 | 311 | /** 312 | * Returns service ids for a given tag. 313 | * 314 | * Example: 315 | * 316 | * $container->register('foo')->addTag('my.tag', array('hello' => 'world')); 317 | * 318 | * $serviceIds = $container->findTaggedServiceIds('my.tag'); 319 | * foreach ($serviceIds as $serviceId => $tags) { 320 | * foreach ($tags as $tag) { 321 | * echo $tag['hello']; 322 | * } 323 | * } 324 | * 325 | * @param string $name 326 | * 327 | * @return array 328 | */ 329 | public function findTaggedServiceIds(string $name): array 330 | { 331 | $tags = array(); 332 | foreach ($this->definitions as $id => $definition) { 333 | if ($definition->hasTag($name)) { 334 | $tags[$id] = $definition->getTag($name); 335 | } 336 | } 337 | 338 | return $tags; 339 | } 340 | 341 | /** 342 | * Gets all global parameters. 343 | * 344 | * @return array 345 | */ 346 | public function getParameters(): array 347 | { 348 | return $this->parameters->toArray(); 349 | } 350 | 351 | /** 352 | * Sets array of parameters. 353 | * 354 | * @param array $parameterStore 355 | */ 356 | public function setParameters(array $parameterStore) 357 | { 358 | $this->parameters->setParameters($parameterStore); 359 | } 360 | 361 | /** 362 | * Add some parameters. 363 | * 364 | * @param array $parameters 365 | */ 366 | public function addParameters(array $parameters) 367 | { 368 | $this->parameters->addParameters($parameters); 369 | } 370 | 371 | /** 372 | * Sets a parameter with its name and value. 373 | * 374 | * @param string $name 375 | * @param mixed $value 376 | */ 377 | public function setParameter(string $name, $value) 378 | { 379 | $this->parameters->setParameter($name, $value); 380 | } 381 | 382 | /** 383 | * Gets a parameter by given name. 384 | * 385 | * @param string $name 386 | * @param mixed $default 387 | * 388 | * @return mixed 389 | */ 390 | public function getParameter(string $name, $default = null) 391 | { 392 | return $this->parameters->getParameter($name, $default); 393 | } 394 | 395 | /** 396 | * Gets a default option of the container. 397 | * 398 | * @param string $option 399 | * 400 | * @return mixed 401 | */ 402 | public function getDefault(string $option) 403 | { 404 | return $this->defaults[$option] ?? null; 405 | } 406 | 407 | /** 408 | * Configure defaults. 409 | * 410 | * @param array $defaults 411 | * 412 | */ 413 | public function setDefaults(array $defaults) 414 | { 415 | $this->defaults = array_merge($this->defaults, $defaults); 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /Definition.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Slince\Di; 15 | 16 | final class Definition 17 | { 18 | /** 19 | * @var mixed 20 | */ 21 | protected $concrete; 22 | 23 | /** 24 | * @var ?string 25 | */ 26 | protected ?string $class = null; 27 | 28 | /** 29 | * Array of arguments. 30 | * 31 | * @var array 32 | */ 33 | protected array $arguments = []; 34 | 35 | /** 36 | * Array of setters. 37 | * 38 | * @var array 39 | */ 40 | protected array $calls = []; 41 | 42 | /** 43 | * Array of properties. 44 | * 45 | * @var array 46 | */ 47 | protected array $properties = []; 48 | 49 | /** 50 | * ['@Foo\Bar', 'createBaz'] 51 | * or 52 | * ['Foo\Bar', 'createBaz']. 53 | * 54 | * @var callable|array 55 | */ 56 | protected $factory; 57 | 58 | /** 59 | * @var array 60 | */ 61 | protected array $tags; 62 | 63 | /** 64 | * @var boolean 65 | */ 66 | protected bool $autowired = true; 67 | 68 | /** 69 | * @var boolean 70 | */ 71 | protected bool $shared = true; 72 | 73 | /** 74 | * @var ?object 75 | */ 76 | protected ?object $resolved = null; 77 | 78 | public function __construct($concrete) 79 | { 80 | $this->concrete = $concrete; 81 | } 82 | 83 | /** 84 | * Set the concrete of the definition. 85 | * 86 | * @param mixed $concrete 87 | */ 88 | public function setConcrete($concrete) 89 | { 90 | $this->concrete = $concrete; 91 | } 92 | 93 | /** 94 | * Get the concrete of the definition. 95 | * 96 | * @return mixed 97 | */ 98 | public function getConcrete() 99 | { 100 | return $this->concrete; 101 | } 102 | 103 | /** 104 | * Set class for the definition. 105 | * 106 | * @param string $class 107 | * 108 | * @return $this 109 | */ 110 | public function setClass(string $class): Definition 111 | { 112 | $this->class = $class; 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * Gets the class. 119 | * 120 | * @return ?string 121 | */ 122 | public function getClass(): ?string 123 | { 124 | return $this->class; 125 | } 126 | 127 | /** 128 | * @param callable|array $factory 129 | * 130 | * @return $this 131 | */ 132 | public function setFactory($factory): Definition 133 | { 134 | $this->factory = $factory; 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * @return callable|array 141 | */ 142 | public function getFactory() 143 | { 144 | return $this->factory; 145 | } 146 | 147 | /** 148 | * @param array $properties 149 | */ 150 | public function setProperties(array $properties) 151 | { 152 | $this->properties = $properties; 153 | } 154 | 155 | /** 156 | * Gets all properties. 157 | * 158 | * @return array 159 | */ 160 | public function getProperties(): array 161 | { 162 | return $this->properties; 163 | } 164 | 165 | /** 166 | * Adds a property. 167 | * 168 | * @param int|string $name 169 | * @param mixed $value 170 | * 171 | * @return $this 172 | */ 173 | public function setProperty(string $name, $value): Definition 174 | { 175 | $this->properties[$name] = $value; 176 | 177 | return $this; 178 | } 179 | 180 | /** 181 | * Gets the property by given name. 182 | * 183 | * @param string $name 184 | * 185 | * @return mixed 186 | */ 187 | public function getProperty(string $name) 188 | { 189 | return $this->properties[$name] ?? null; 190 | } 191 | 192 | /** 193 | * add an argument. 194 | * 195 | * @param mixed $value 196 | * 197 | * @return $this 198 | */ 199 | public function addArgument($value): Definition 200 | { 201 | $this->arguments[] = $value; 202 | 203 | return $this; 204 | } 205 | 206 | /** 207 | * Sets a specific argument. 208 | * 209 | * @param string $key 210 | * @param mixed $value 211 | * 212 | * @return $this 213 | */ 214 | public function setArgument(string $key, $value): Definition 215 | { 216 | $this->arguments[$key] = $value; 217 | 218 | return $this; 219 | } 220 | 221 | /** 222 | * Sets the arguments to pass to the service constructor/factory method. 223 | * 224 | * @param array $arguments 225 | * 226 | * @return $this 227 | */ 228 | public function setArguments(array $arguments): Definition 229 | { 230 | $this->arguments = $arguments; 231 | 232 | return $this; 233 | } 234 | 235 | /** 236 | * Gets all arguments of constructor. 237 | * 238 | * @return array 239 | */ 240 | public function getArguments(): array 241 | { 242 | return $this->arguments; 243 | } 244 | 245 | /** 246 | * Gets the argument at the specified position of constructor. 247 | * 248 | * @param int|string $index 249 | * 250 | * @return mixed 251 | */ 252 | public function getArgument($index) 253 | { 254 | return $this->arguments[$index] ?? null; 255 | } 256 | 257 | /** 258 | * Adds a method. 259 | * 260 | * @param string $method 261 | * @param string|array $arguments 262 | * 263 | * @return $this 264 | */ 265 | public function addMethodCall(string $method, $arguments): Definition 266 | { 267 | $this->calls[] = [ 268 | $method, 269 | (array) $arguments, 270 | ]; 271 | 272 | return $this; 273 | } 274 | 275 | /** 276 | * Sets the methods to call after service initialization. 277 | * 278 | * @param array $methods methods 279 | * 280 | * @return $this 281 | */ 282 | public function setMethodCalls(array $methods): Definition 283 | { 284 | $this->calls = array(); 285 | foreach ($methods as $call) { 286 | $this->addMethodCall($call[0], $call[1]); 287 | } 288 | 289 | return $this; 290 | } 291 | 292 | /** 293 | * Gets all methods. 294 | * 295 | * @return array 296 | */ 297 | public function getMethodCalls(): array 298 | { 299 | return $this->calls; 300 | } 301 | 302 | /** 303 | * Check if the current definition has a given method to call after service initialization. 304 | * 305 | * @param string $method The method name to search for 306 | * 307 | * @return bool 308 | */ 309 | public function hasMethodCall(string $method): bool 310 | { 311 | foreach ($this->calls as $call) { 312 | if ($call[0] === $method) { 313 | return true; 314 | } 315 | } 316 | 317 | return false; 318 | } 319 | 320 | /** 321 | * Sets tags for this definition. 322 | * 323 | * @param array $tags 324 | * 325 | * @return $this 326 | */ 327 | public function setTags(array $tags): Definition 328 | { 329 | $this->tags = $tags; 330 | 331 | return $this; 332 | } 333 | 334 | /** 335 | * Returns all tags. 336 | * 337 | * @return array An array of tags 338 | */ 339 | public function getTags(): array 340 | { 341 | return $this->tags; 342 | } 343 | 344 | /** 345 | * Gets a tag by name. 346 | * 347 | * @param string $name The tag name 348 | * 349 | * @return array An array of attributes 350 | */ 351 | public function getTag(string $name): array 352 | { 353 | return $this->tags[$name] ?? array(); 354 | } 355 | 356 | /** 357 | * Adds a tag for this definition. 358 | * 359 | * @param string $name The tag name 360 | * @param array $attributes An array of attributes 361 | * 362 | * @return $this 363 | */ 364 | public function addTag(string $name, array $attributes = array()): Definition 365 | { 366 | $this->tags[$name][] = $attributes; 367 | 368 | return $this; 369 | } 370 | 371 | /** 372 | * Whether this definition has a tag with the given name. 373 | * 374 | * @param string $name 375 | * 376 | * @return bool 377 | */ 378 | public function hasTag(string $name): bool 379 | { 380 | return isset($this->tags[$name]); 381 | } 382 | 383 | /** 384 | * Clears all tags for a given name. 385 | * 386 | * @param string $name The tag name 387 | * 388 | * @return $this 389 | */ 390 | public function clearTag(string $name): Definition 391 | { 392 | unset($this->tags[$name]); 393 | 394 | return $this; 395 | } 396 | 397 | /** 398 | * Clears the tags for this definition. 399 | * 400 | * @return $this 401 | */ 402 | public function clearTags(): Definition 403 | { 404 | $this->tags = array(); 405 | 406 | return $this; 407 | } 408 | 409 | /** 410 | * Is the definition autowired? 411 | * 412 | * @return bool 413 | */ 414 | public function isAutowired(): bool 415 | { 416 | return $this->autowired; 417 | } 418 | 419 | /** 420 | * Enables/disables autowiring. 421 | * 422 | * @param bool $autowired 423 | * 424 | * @return $this 425 | */ 426 | public function setAutowired(bool $autowired): Definition 427 | { 428 | $this->autowired = $autowired; 429 | 430 | return $this; 431 | } 432 | 433 | /** 434 | * Sets if the service must be shared or not. 435 | * 436 | * @param bool $shared Whether the service must be shared or not 437 | * 438 | * @return $this 439 | */ 440 | public function setShared(bool $shared): Definition 441 | { 442 | $this->shared = $shared; 443 | 444 | return $this; 445 | } 446 | 447 | /** 448 | * Whether this service is shared. 449 | * 450 | * @return bool 451 | */ 452 | public function isShared(): bool 453 | { 454 | return $this->shared; 455 | } 456 | 457 | /** 458 | * Get resolved instance of the definition. 459 | * 460 | * @return object 461 | */ 462 | public function getResolved(): ?object 463 | { 464 | return $this->resolved; 465 | } 466 | 467 | /** 468 | * Set the resolved instance for the definition. 469 | * 470 | * @param object $resolved 471 | * 472 | * @return $this 473 | */ 474 | public function setResolved(object $resolved): Definition 475 | { 476 | $this->resolved = $resolved; 477 | 478 | return $this; 479 | } 480 | } 481 | -------------------------------------------------------------------------------- /Exception/ConfigException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Slince\Di\Exception; 13 | 14 | use Psr\Container\ContainerExceptionInterface; 15 | 16 | class ConfigException extends DependencyInjectionException implements ContainerExceptionInterface 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /Exception/DependencyInjectionException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Slince\Di\Exception; 13 | 14 | use Psr\Container\ContainerExceptionInterface; 15 | 16 | class DependencyInjectionException extends \Exception implements ContainerExceptionInterface 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /Exception/NotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Slince\Di\Exception; 13 | 14 | use Psr\Container\NotFoundExceptionInterface; 15 | 16 | class NotFoundException extends DependencyInjectionException implements NotFoundExceptionInterface 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /ParameterBag.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Slince\Di; 15 | 16 | use Dflydev\DotAccessData\Data; 17 | 18 | final class ParameterBag extends Data 19 | { 20 | /** 21 | * Sets array of parameters. 22 | * 23 | * @param $parameters 24 | */ 25 | public function setParameters($parameters) 26 | { 27 | $this->data = $parameters; 28 | } 29 | 30 | /** 31 | * Adds array of parameters. 32 | * 33 | * @param array $parameters 34 | */ 35 | public function addParameters(array $parameters) 36 | { 37 | $this->data = array_replace($this->data, $parameters); 38 | } 39 | 40 | /** 41 | * Sets parameter with given name and value. 42 | * 43 | * @param int|string $name 44 | * @param mixed $value 45 | */ 46 | public function setParameter($name, $value) 47 | { 48 | $this->data[$name] = $value; 49 | } 50 | 51 | /** 52 | * Gets the parameter by its name. 53 | * 54 | * @param string $name 55 | * @param mixed $default 56 | * 57 | * @return mixed 58 | */ 59 | public function getParameter(string $name, $default = null) 60 | { 61 | if (isset($this->data[$name])) { 62 | return $this->data[$name]; 63 | } 64 | 65 | return parent::get($name, $default); 66 | } 67 | 68 | /** 69 | * Gets all parameters. 70 | * 71 | * @return array 72 | */ 73 | public function toArray(): array 74 | { 75 | return $this->data; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dependency Injection Container 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/slince/di/test.yml?style=flat-square)](https://github.com/slince/di/actions) 4 | [![Coverage Status](https://img.shields.io/codecov/c/github/slince/di.svg?style=flat-square)](https://codecov.io/github/slince/di) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/slince/di.svg?style=flat-square)](https://packagist.org/packages/slince/di) 6 | [![Latest Stable Version](https://img.shields.io/packagist/v/slince/di.svg?style=flat-square&label=stable)](https://packagist.org/packages/slince/di) 7 | [![Scrutinizer](https://img.shields.io/scrutinizer/g/slince/di.svg?style=flat-square)](https://scrutinizer-ci.com/g/slince/di/?branch=master) 8 | 9 | This package is a flexible IOC container for PHP with a focus on being lightweight and fast as well as requiring as little 10 | configuration as possible. It is an implementation of [PSR-11](https://github.com/container-interop/fig-standards/blob/master/proposed/container.md) 11 | 12 | ## Installation 13 | 14 | Install via composer. 15 | 16 | ```json 17 | { 18 | "require": { 19 | "slince/di": "^3.0" 20 | } 21 | } 22 | ``` 23 | 24 | Alternatively, require package use composer cli: 25 | 26 | ```bash 27 | composer require slince/di ^3.0 28 | ``` 29 | 30 | ## Usage 31 | 32 | Container is dependency injection container. It allows you to implement the dependency injection design pattern meaning that you can decouple your class dependencies and have the container inject them where they are needed. 33 | 34 | ```php 35 | namespace Acme; 36 | 37 | class Foo 38 | { 39 | /** 40 | * @var \Acme\Bar 41 | */ 42 | public $bar; 43 | 44 | /** 45 | * Construct. 46 | */ 47 | public function __construct(Bar $bar) 48 | { 49 | $this->bar = $bar; 50 | } 51 | } 52 | 53 | class Bar 54 | { 55 | public $foo; 56 | public $baz; 57 | 58 | public function __construct($foo, $baz) 59 | { 60 | $this->foo = $foo; 61 | $this->baz = $baz; 62 | } 63 | } 64 | 65 | $container = new Slince\Di\Container(); 66 | 67 | $container->register(Acme\Foo::class); 68 | $foo = $container->get(Acme\Foo::class); 69 | 70 | var_dump($foo instanceof Acme\Foo); // true 71 | var_dump($foo->bar instanceof Acme\Bar); // true 72 | ``` 73 | 74 | ### Make Service References 75 | 76 | ```php 77 | $container->register('bar', Acme\Bar::class); 78 | $container->register('foo', Acme\Foo::class) 79 | ->addArgument(new Slince\Di\Reference('bar')); //refer to 'bar' 80 | 81 | var_dump($container->get('bar') === $container->get('foo')->bar)); // true 82 | ``` 83 | 84 | ### Use a Factory to Create Services 85 | 86 | Suppose you have a factory that configures and returns a new `NewsletterManager` object 87 | by calling the static `createNewsletterManager()` method: 88 | 89 | ```php 90 | class NewsletterManagerStaticFactory 91 | { 92 | public static function createNewsletterManager($parameter) 93 | { 94 | $newsletterManager = new NewsletterManager($parameter); 95 | 96 | // ... 97 | 98 | return $newsletterManager; 99 | } 100 | } 101 | ``` 102 | 103 | ```php 104 | // call the static method 105 | $container->register( 106 | NewsletterManager::class, 107 | array(NewsletterManagerStaticFactory::class, 'createNewsletterManager') 108 | )->addArgument('foo'); 109 | 110 | ``` 111 | If your factory is not using a static function to configure and create your service, but a regular method, 112 | you can instantiate the factory itself as a service too. 113 | 114 | ```php 115 | // call a method on the specified factory service 116 | $container->register(NewsletterManager::class, [ 117 | new Reference(NewsletterManagerFactory::class), 118 | 'createNewsletterManager' 119 | ]); 120 | ``` 121 | 122 | ### Create Service Aliases 123 | 124 | ```php 125 | $container->register(Acme\Foo::class); 126 | $container->setAlias('foo-alias', Acme\Foo::class); 127 | $foo = $container->get('foo-alias'); 128 | 129 | var_dump($foo instanceof Acme\Foo); // true 130 | ``` 131 | 132 | ### Configure container 133 | 134 | - Singleton 135 | 136 | ```php 137 | $container->setDefaults([ 138 | 'share' => false 139 | ]); 140 | $container->register('foo', Acme\Foo::class); 141 | var_dump($container->get('foo') === $container->get('foo')); // false 142 | ``` 143 | 144 | - Autowiring 145 | 146 | ```php 147 | $container->setDefaults([ 148 | 'autowire' => false, 149 | ]); 150 | $container->register('foo', Acme\Foo::class) 151 | ->addArgument(new Acme\Bar()); // You have to provide $bar 152 | 153 | var_dump($container->get('foo') instanceof Acme\Foo::class); // true 154 | ``` 155 | 156 | ### Container Parameters 157 | 158 | ```php 159 | $container->setParameters([ 160 | 'foo' => 'hello', 161 | 'bar' => [ 162 | 'baz' => 'world' 163 | ] 164 | ]); 165 | 166 | $container->register('bar', Acme\Bar::class) 167 | ->setArguments([ 168 | 'foo' => $container->getParameter('foo'), 169 | 'baz' => $container->getParameter('bar.baz') 170 | ]); 171 | 172 | $bar = $container->get('bar'); 173 | var_dump($bar->foo); // hello 174 | var_dump($bar->bar); // world 175 | ``` 176 | 177 | ### Work with Service Tags 178 | 179 | ```php 180 | $container->register('foo')->addTag('my.tag', array('hello' => 'world')); 181 | 182 | $serviceIds = $container->findTaggedServiceIds('my.tag'); 183 | 184 | foreach ($serviceIds as $serviceId => $tags) { 185 | foreach ($tags as $tag) { 186 | echo $tag['hello']; 187 | } 188 | } 189 | ``` 190 | ## License 191 | 192 | The MIT license. See [MIT](https://opensource.org/licenses/MIT) 193 | -------------------------------------------------------------------------------- /Reference.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | namespace Slince\Di; 14 | 15 | final class Reference 16 | { 17 | /** 18 | * Service ID 19 | * @var string 20 | */ 21 | protected string $id; 22 | 23 | public function __construct(string $id) 24 | { 25 | $this->id = $id; 26 | } 27 | 28 | /** 29 | * Get service ID 30 | * 31 | * @return string 32 | */ 33 | public function getId(): string 34 | { 35 | return $this->id; 36 | } 37 | } -------------------------------------------------------------------------------- /Resolver.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Slince\Di; 15 | 16 | use ReflectionException; 17 | use Slince\Di\Exception\ConfigException; 18 | use Slince\Di\Exception\DependencyInjectionException; 19 | 20 | class Resolver 21 | { 22 | /** 23 | * @var Container 24 | */ 25 | protected Container $container; 26 | 27 | /** 28 | * @param Container $container 29 | */ 30 | public function __construct(Container $container) 31 | { 32 | $this->container = $container; 33 | } 34 | 35 | /** 36 | * Create one instance for the given definition. 37 | * 38 | * @param Definition $definition 39 | * 40 | * @return object 41 | * @throws DependencyInjectionException 42 | */ 43 | public function resolve(Definition $definition): object 44 | { 45 | $this->parseConcrete($definition); 46 | 47 | if (null !== $definition->getFactory()) { 48 | $instance = $this->createFromFactory($definition); 49 | } elseif (null !== $definition->getClass()) { 50 | $instance = $this->createFromClass($definition); 51 | } elseif (null !== $definition->getResolved()) { 52 | $instance = $definition->getResolved(); 53 | } else { 54 | throw new ConfigException('The definition is not invalid.'); 55 | } 56 | $this->invokeMethods($definition, $instance); 57 | $this->invokeProperties($definition, $instance); 58 | $definition->setResolved($instance); 59 | 60 | return $instance; 61 | } 62 | 63 | protected function parseConcrete(Definition $definition) 64 | { 65 | $concrete = $definition->getConcrete(); 66 | if (is_string($concrete)) { 67 | $definition->setClass($concrete); 68 | } elseif (is_array($concrete) || $concrete instanceof \Closure) { 69 | $definition->setFactory($concrete); 70 | } elseif (is_object($concrete)) { 71 | $definition->setResolved($concrete) 72 | ->setShared(true); 73 | } else { 74 | throw new ConfigException('The concrete of the definition is invalid'); 75 | } 76 | } 77 | 78 | /** 79 | * Create instance for the class. 80 | * 81 | * @throws DependencyInjectionException 82 | */ 83 | protected function createFromClass(Definition $definition): object 84 | { 85 | $class = $definition->getClass(); 86 | try { 87 | $reflection = new \ReflectionClass($definition->getClass()); 88 | } catch (ReflectionException $e) { 89 | throw new DependencyInjectionException(sprintf('Class "%s" is invalid', $definition->getClass())); 90 | } 91 | if (!$reflection->isInstantiable()) { 92 | throw new DependencyInjectionException(sprintf('Can not instantiate "%s"', $definition->getClass())); 93 | } 94 | $constructor = $reflection->getConstructor(); 95 | 96 | try { 97 | if (is_null($constructor)) { 98 | $instance = $reflection->newInstanceWithoutConstructor(); 99 | } else { 100 | $arguments = $this->resolveArguments($definition->getArguments()); 101 | if ($definition->isAutowired()) { 102 | $arguments = $this->resolveDependencies($constructor->getParameters(), $arguments); 103 | } 104 | if (count($arguments) < $constructor->getNumberOfRequiredParameters()) { 105 | throw new ConfigException(sprintf('Too few arguments for class "%s"', $class)); 106 | } 107 | $instance = $reflection->newInstanceArgs($arguments); 108 | } 109 | } catch (ReflectionException $exception) { 110 | throw new DependencyInjectionException($exception); 111 | } 112 | return $instance; 113 | } 114 | 115 | /** 116 | * @param Definition $definition 117 | * 118 | * @return object 119 | * @throws DependencyInjectionException 120 | */ 121 | protected function createFromFactory(Definition $definition): object 122 | { 123 | $factory = $definition->getFactory(); 124 | if (is_array($factory)) { 125 | $factory = $this->resolveArguments($factory); 126 | } 127 | return call_user_func_array($factory, 128 | $this->resolveArguments($definition->getArguments()) ?: [$this->container] 129 | ); 130 | } 131 | 132 | /** 133 | * @param Definition $definition 134 | * @param object $instance 135 | * @throws DependencyInjectionException 136 | */ 137 | protected function invokeMethods(Definition $definition, object $instance) 138 | { 139 | foreach ($definition->getMethodCalls() as $method) { 140 | call_user_func_array([$instance, $method[0]], $this->resolveArguments($method[1])); 141 | } 142 | } 143 | 144 | /** 145 | * @param Definition $definition 146 | * @param object $instance 147 | * @throws DependencyInjectionException 148 | */ 149 | protected function invokeProperties(Definition $definition, object $instance) 150 | { 151 | $properties = $this->resolveArguments($definition->getProperties()); 152 | foreach ($properties as $name => $value) { 153 | $instance->$name = $value; 154 | } 155 | } 156 | 157 | /** 158 | * Resolve dependencies. 159 | * 160 | * @param \ReflectionParameter[] $dependencies 161 | * @param array $arguments 162 | * @return array 163 | * @throws DependencyInjectionException 164 | */ 165 | protected function resolveDependencies(array $dependencies, array $arguments): array 166 | { 167 | $solved = []; 168 | foreach ($dependencies as $dependency) { 169 | if (isset($arguments[$dependency->getPosition()])) { 170 | $solved[] = $arguments[$dependency->getPosition()]; 171 | continue; 172 | } 173 | 174 | if (isset($arguments[$dependency->getName()])) { 175 | $solved[] = $arguments[$dependency->getName()]; 176 | continue; 177 | } 178 | 179 | $autoResolveDependencyException = null; 180 | if (null !== ($type = $dependency->getType()) && !$type->isBuiltin()) { 181 | try { 182 | $solved[] = $this->container->get($type->getName()); 183 | continue; 184 | } catch (DependencyInjectionException $exception) { 185 | $autoResolveDependencyException = $exception; 186 | } 187 | } 188 | 189 | if ($dependency->isDefaultValueAvailable()) { 190 | $solved[] = $dependency->getDefaultValue(); 191 | continue; 192 | } 193 | 194 | if (null !== $autoResolveDependencyException) { 195 | throw $autoResolveDependencyException; 196 | } 197 | 198 | throw new DependencyInjectionException(sprintf( 199 | 'Unresolvable dependency resolving "%s" in class "%s"', 200 | $dependency->name, 201 | $dependency->getDeclaringClass()->getName() 202 | )); 203 | } 204 | return $solved; 205 | } 206 | 207 | /** 208 | * Resolves array of parameters. 209 | * 210 | * @param array $arguments 211 | * 212 | * @return array 213 | * @throws DependencyInjectionException 214 | */ 215 | protected function resolveArguments(array $arguments): array 216 | { 217 | foreach ($arguments as &$argument) { 218 | if ($argument instanceof Reference) { 219 | $argument = $this->container->get($argument->getId()); 220 | } 221 | } 222 | return $arguments; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /Tests/ContainerTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($container->has('foo')); 24 | $container['foo'] = Foo::class; 25 | $this->assertTrue(isset($container['foo'])); 26 | $this->assertInstanceOf(Foo::class, $container['foo']); 27 | 28 | unset($container['foo']); 29 | $this->assertFalse(isset($container['foo'])); 30 | } 31 | 32 | public function testFactory() 33 | { 34 | $container = new Container(); 35 | $container->register('director1', function () { 36 | return new Director('James', 26); 37 | }); 38 | $this->assertInstanceOf(Director::class, $container->get('director1')); 39 | } 40 | 41 | public function testArrayFactory() 42 | { 43 | $container = new Container(); 44 | $container->register('director', [Director::class, 'factory']) 45 | ->setArguments(['James', 18]); 46 | 47 | $director = $container->get('director'); 48 | $this->assertInstanceOf(Director::class, $director); 49 | $this->assertEquals('James', $director->getName()); 50 | $this->assertEquals(18, $director->getAge()); 51 | 52 | // ['@service', 'factory'] 53 | $container->register('foo', Foo::class); 54 | $container->register('director2', [new Reference('foo'), 'createDirector']) 55 | ->setArguments(['James', 18]); 56 | 57 | $director2 = $container->get('director2'); 58 | $this->assertEquals('James', $director2->getName()); 59 | $this->assertEquals(18, $director2->getAge()); 60 | 61 | $container->register('director2', [new Reference('foo'), 'createDirector']) 62 | ->setArguments([1 => 18, 0 => 'James']); 63 | 64 | // [new Reference('service'), 'factory'] 65 | $container->register('director2', [new Reference('foo'), 'createDirector']) 66 | ->setArguments([1 => 18, 0 => 'James']); 67 | 68 | $director2 = $container->get('director2'); 69 | $this->assertEquals('James', $director2->getName()); 70 | $this->assertEquals(18, $director2->getAge()); 71 | } 72 | 73 | public function testFactoryWithParameters() 74 | { 75 | $container = new Container(); 76 | $container->register('director', function ($age, $name) { 77 | return new Director($name, $age); 78 | }) 79 | ->setArguments([1 => 18, 0 => 'James']); 80 | $director = $container->get('director'); 81 | $this->assertEquals('James', $director->getName()); 82 | $this->assertEquals(18, $director->getAge()); 83 | } 84 | 85 | public function testInstance() 86 | { 87 | $container = new Container(); 88 | $director = new Director(); 89 | $container->register('director', $director); 90 | $this->assertTrue($container->has('director')); 91 | $this->assertInstanceOf(Director::class, $container->get('director')); 92 | $this->assertTrue($container->get('director') === $director); 93 | $this->assertTrue($container->get('director') === $container->get('director')); 94 | 95 | $container->register(new Director()); 96 | $this->assertTrue($container->has(Director::class)); 97 | } 98 | 99 | public function testRegister() 100 | { 101 | $container = new Container(); 102 | $container->register('director', Director::class) 103 | ->setArguments([0 => 'Bob', 1 => 45]); 104 | $this->assertInstanceOf(Director::class, $director = $container->get('director')); 105 | $this->assertEquals('Bob', $director->getName()); 106 | $this->assertEquals(45, $director->getAge()); 107 | } 108 | 109 | public function testBind() 110 | { 111 | $container = new Container(); 112 | $container->register('director', Director::class); 113 | $this->assertInstanceOf(Director::class, $container->get('director')); 114 | } 115 | 116 | public function testInterfaceBind() 117 | { 118 | $container = new Container(); 119 | $container->register(ActorInterface::class, Actor::class); 120 | $this->assertInstanceOf(ActorInterface::class, $container->get(ActorInterface::class)); 121 | $this->assertInstanceOf(Actor::class, $container->get(ActorInterface::class)); 122 | 123 | $movie = $container->get(Movie::class); 124 | $this->assertInstanceOf(Movie::class, $movie); 125 | $this->assertInstanceOf(Actor::class, $movie->getActor()); 126 | } 127 | 128 | public function testRegisterWithNumericArguments() 129 | { 130 | $container = new Container(); 131 | $container->register('director', function ($name, $age) { 132 | return new Director($name, $age); 133 | })->addArgument('foo') 134 | ->addArgument('bar'); 135 | $director = $container->get('director'); 136 | $this->assertEquals('foo', $director->getName()); 137 | $this->assertEquals('bar', $director->getAge()); 138 | } 139 | 140 | public function testRegisterWithMethodCalls() 141 | { 142 | $container = new Container(); 143 | $container->register(Director::class) 144 | ->addMethodCall('setAge', [20]); 145 | $director = $container->get(Director::class); 146 | $this->assertEquals(20, $director->getAge()); 147 | 148 | $container->register('director2', Director::class) 149 | ->setMethodCalls([ 150 | ['setAge', ['age' => 25]], 151 | ['setName', ['foo']], 152 | ]); 153 | 154 | $director = $container->get('director2'); 155 | $this->assertEquals('foo', $director->getName()); 156 | $this->assertEquals(25, $director->getAge()); 157 | } 158 | 159 | public function testHasDefinition() 160 | { 161 | $container = new Container(); 162 | $this->assertFalse($container->hasDefinition('not_exists_class')); 163 | $this->assertFalse($container->hasDefinition(Director::class)); 164 | 165 | $container->register(new Director()); 166 | $container->get(Director::class); 167 | $this->assertTrue($container->hasDefinition(Director::class)); 168 | 169 | $container->register(ActorInterface::class, Actor::class); 170 | $this->assertTrue($container->hasDefinition(ActorInterface::class)); 171 | 172 | $container = new Container(); 173 | $container->register(new Director()); 174 | $this->assertTrue($container->hasDefinition(Director::class)); 175 | } 176 | 177 | public function testHas() 178 | { 179 | $container = new Container(); 180 | $this->assertFalse($container->has('not_exists_class')); 181 | // auto register 182 | $this->assertTrue($container->has(Director::class)); 183 | 184 | $container->register(new Director()); 185 | $container->get(Director::class); 186 | $this->assertTrue($container->has(Director::class)); 187 | 188 | $container->register(ActorInterface::class, Actor::class); 189 | $this->assertTrue($container->has(ActorInterface::class)); 190 | 191 | $container = new Container(); 192 | $container->register(new Director()); 193 | $this->assertTrue($container->has(Director::class)); 194 | } 195 | 196 | public function testGetWithMissingRequiredParameters() 197 | { 198 | $container = new Container(); 199 | $container->register('bar', Bar::class); 200 | $this->expectException(DependencyInjectionException::class); 201 | $container->get('bar'); 202 | } 203 | 204 | public function testGetWithMissingOptionalClassDependency() 205 | { 206 | $container = new Container(); 207 | $container->register('director', function ($name, $age, ActorInterface $actor = null) { 208 | $this->assertNull($actor); 209 | 210 | return new Director($name, $age); 211 | })->setArguments([ 212 | 'name' => 'bob', 213 | 'age' => 12, 214 | ]); 215 | $container->get('director'); 216 | } 217 | 218 | public function testGetNonExistsService() 219 | { 220 | $container = new Container(); 221 | $this->expectException(NotFoundException::class); 222 | $container->get('a_non_exists_id'); 223 | } 224 | 225 | public function testGetDefaults() 226 | { 227 | $container = new Container(); 228 | $this->assertTrue($container->getDefault('autowire')); 229 | $this->assertTrue($container->getDefault('share')); 230 | $container->setDefaults([ 231 | 'autowire' => false, 232 | 'share' => false 233 | ]); 234 | $this->assertFalse($container->getDefault('autowire')); 235 | $this->assertFalse($container->getDefault('share')); 236 | } 237 | 238 | 239 | public function testProperties() 240 | { 241 | $container = new Container(); 242 | $container->register('director', Director::class); 243 | $container->register('foo1', Foo::class)->setProperty('director', new Reference('director')); 244 | $this->assertSame($container->get('director'), $container->get('foo1')->director); 245 | } 246 | 247 | public function testShare() 248 | { 249 | $container = new Container(); 250 | $container->register('director', function () { 251 | return new Director('James', 26); 252 | })->setShared(true); 253 | $this->assertTrue($container->get('director') === $container->get('director')); 254 | 255 | $container->register('director2', function () { 256 | return new Director('James', 26); 257 | })->setShared(false); 258 | $this->assertFalse($container->get('director2') === $container->get('director2')); 259 | } 260 | 261 | public function testConfigureShare() 262 | { 263 | $container = new Container(); 264 | $container->setDefaults([ 265 | 'share' => false, 266 | ]); 267 | $container->register('director', function () { 268 | return new Director('James', 26); 269 | }); 270 | $this->assertFalse($container->get('director') === $container->get('director')); 271 | } 272 | 273 | public function testAutowire() 274 | { 275 | $container = new Container(); 276 | $container->register(Movie::class) 277 | ->setAutowired(false); 278 | 279 | try { 280 | $container->get(Movie::class); 281 | $this->fail(); 282 | } catch (\Exception $exception) { 283 | $this->assertInstanceOf(ConfigException::class, $exception); 284 | } 285 | 286 | $container->register(Movie::class) 287 | ->addArgument(new Director()) 288 | ->addArgument(new Actor()); 289 | $movie = $container->get(Movie::class); 290 | $this->assertInstanceOf(Movie::class, $movie); 291 | $this->assertInstanceOf(Director::class, $movie->getDirector()); 292 | $this->assertInstanceOf(Actor::class, $movie->getActor()); 293 | } 294 | 295 | public function testConfigureAutowire() 296 | { 297 | $container = new Container(); 298 | $container->setDefaults([ 299 | 'autowire' => false, 300 | ]); 301 | $container->register(Movie::class); 302 | 303 | try { 304 | $container->get(Movie::class); 305 | $this->fail(); 306 | } catch (\Exception $exception) { 307 | $this->assertInstanceOf(ConfigException::class, $exception); 308 | } 309 | } 310 | 311 | public function testReference() 312 | { 313 | $container = new Container(); 314 | $container->register('director', Director::class); 315 | $container->register('actor', Actor::class); 316 | $container->register(Movie::class) 317 | ->addArgument(new Reference('director')) 318 | ->addArgument(new Reference('actor')); 319 | 320 | $movie = $container->get(Movie::class); 321 | $this->assertInstanceOf(Movie::class, $movie); 322 | $this->assertSame($container->get('director'), $movie->getDirector()); 323 | $this->assertSame($container->get('actor'), $movie->getActor()); 324 | } 325 | 326 | public function testParameters() 327 | { 328 | $container = new Container(); 329 | $container->setParameters([ 330 | 'foo' => 'bar', 331 | ]); 332 | $this->assertEquals('bar', $container->getParameter('foo')); 333 | $container->addParameters([ 334 | 'foo' => 'baz', 335 | 'bar' => 'baz', 336 | ]); 337 | $this->assertEquals(['foo' => 'baz', 'bar' => 'baz'], $container->getParameters()); 338 | $container->setParameter('bar', 'baz'); 339 | $this->assertEquals('baz', $container->getParameter('bar')); 340 | } 341 | 342 | public function testResolveParameters() 343 | { 344 | $container = new Container(); 345 | $container->setParameters([ 346 | 'foo' => 'James', 347 | 'bar' => 45, 348 | ]); 349 | 350 | $container->register('director', function (array $profile) { 351 | return new Director($profile['name'], $profile['age']); 352 | })->setArguments([ 353 | 'profile' => [ 354 | 'name' => $container->getParameter('foo'), 355 | 'age' => $container->getParameter('bar'), 356 | ], 357 | ]); 358 | $director = $container->get('director'); 359 | $this->assertEquals('James', $director->getName()); 360 | $this->assertEquals(45, $director->getAge()); 361 | } 362 | 363 | public function testOptionalArgs() 364 | { 365 | $container = new Container(); 366 | $director = $container->get(Actor::class); 367 | $this->assertInstanceOf(Actor::class, $director); 368 | } 369 | 370 | public function testSimpleGlobalParameter() 371 | { 372 | $container = new Container(); 373 | $container->setParameters([ 374 | 'directorName' => 'James', 375 | ]); 376 | $container->register('director', function (Container $container) { 377 | return new Director($container->getParameter('directorName'), 26); 378 | }); 379 | $this->assertEquals('James', $container->get('director')->getName()); 380 | } 381 | 382 | public function testGlobalParameterUseDotAccess() 383 | { 384 | $container = new Container(); 385 | $container->setParameters([ 386 | 'directorName' => 'James', 387 | 'director' => [ 388 | 'age' => 26, 389 | ], 390 | ]); 391 | $container->register('director', Director::class)->setArguments([ 392 | $container->getParameter('directorName'), 393 | $container->getParameter('director.age'), 394 | ]); 395 | $this->assertEquals('James', $container->get('director')->getName()); 396 | $this->assertEquals(26, $container->get('director')->getAge()); 397 | } 398 | 399 | public function testAlias() 400 | { 401 | $container = new Container(); 402 | 403 | $container->register('director', function (array $profile) { 404 | return new Director($profile['name'], $profile['age']); 405 | })->setArguments([ 406 | 'profile' => [ 407 | 'name' => 'James', 408 | 'age' => 45, 409 | ], 410 | ]); 411 | $container->setAlias('director-alias', 'director'); 412 | $this->assertEquals('director', $container->getAlias('director-alias')); 413 | $this->assertSame($container->get('director'), $container->get('director-alias')); 414 | } 415 | 416 | public function testTags() 417 | { 418 | $container = new Container(); 419 | $container->register('director', Director::class) 420 | ->addTag('my.tag', array('hello' => 'world')); 421 | 422 | $serviceIds = $container->findTaggedServiceIds('my.tag'); 423 | $this->assertEquals([ 424 | 'director' => [['hello' => 'world']], 425 | ], $serviceIds); 426 | } 427 | 428 | public function testExtend() 429 | { 430 | $container = new Container(); 431 | $container->register('director', Director::class) 432 | ->setArguments(['James', 18]); 433 | $container->extend('director') 434 | ->setArguments(['Bob', 19]); 435 | $this->assertEquals('Bob', $container->get('director')->getName()); 436 | $this->assertEquals(19, $container->get('director')->getAge()); 437 | 438 | try { 439 | $container->extend('director'); 440 | $this->fail(); 441 | } catch (\Exception $exception) { 442 | $this->assertInstanceOf(DependencyInjectionException::class, $exception); 443 | } 444 | 445 | try { 446 | $container->extend('a_non_exists_id'); 447 | $this->fail(); 448 | } catch (\Exception $exception) { 449 | $this->assertInstanceOf(NotFoundException::class, $exception); 450 | } 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /Tests/DefinitionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(Director::class, $definition->getConcrete()); 15 | $definition->setConcrete(Foo::class); 16 | $this->assertEquals(Foo::class, $definition->getConcrete()); 17 | } 18 | public function testSetAndGetArgument() 19 | { 20 | $definition = new Definition(Director::class); 21 | $definition->setArgument(0, 'LiAn'); 22 | $this->assertEquals('LiAn', $definition->getArgument(0)); 23 | 24 | $arguments = ['Jumi', 12]; 25 | $definition->setArguments($arguments); 26 | $this->assertEquals($arguments, $definition->getArguments()); 27 | } 28 | 29 | public function testSetAndGetMethodCall() 30 | { 31 | $definition = new Definition(Director::class); 32 | $definition->addMethodCall('setName', ['LiAn']); 33 | $this->assertEquals(['setName', ['LiAn']], $definition->getMethodCalls()[0]); 34 | 35 | $definition->setMethodCalls([ 36 | ['setName', ['LiAn']], 37 | ['setAge', [20]], 38 | ]); 39 | $this->assertTrue($definition->hasMethodCall('setName')); 40 | $this->assertTrue($definition->hasMethodCall('setAge')); 41 | } 42 | 43 | public function testProperty() 44 | { 45 | $definition = new Definition(Director::class); 46 | $this->assertEmpty($definition->getProperties()); 47 | $definition->setProperties([ 48 | 'foo' => 'bar' 49 | ]); 50 | $this->assertEquals(['foo'=>'bar'], $definition->getProperties()); 51 | 52 | $definition->setProperty('bar', 'baz'); 53 | $this->assertEquals('baz', $definition->getProperty('bar')); 54 | } 55 | 56 | public function testAutowire() 57 | { 58 | $definition = new Definition('foo'); 59 | $definition->setAutowired(true); 60 | $this->assertTrue($definition->isAutowired()); 61 | } 62 | 63 | public function testShare() 64 | { 65 | $definition = new Definition('foo'); 66 | $definition->setShared(true); 67 | $this->assertTrue($definition->isShared()); 68 | } 69 | 70 | public function testTag() 71 | { 72 | $definition = new Definition('foo'); 73 | $definition->addTag('my.tag'); 74 | $this->assertEquals([[]], $definition->getTag('my.tag')); 75 | $definition->addTag('my.tag1'); 76 | $this->assertEquals([ 77 | 'my.tag' => [[]], 78 | 'my.tag1' => [[]], 79 | ], $definition->getTags()); 80 | $definition->clearTag('my.tag'); 81 | $this->assertFalse($definition->hasTag('my.tag')); 82 | 83 | $definition->clearTags(); 84 | $this->assertFalse($definition->hasTag('my.tag1')); 85 | } 86 | } -------------------------------------------------------------------------------- /Tests/ParameterBagTest.php: -------------------------------------------------------------------------------- 1 | assertEquals([], $parameters->toArray()); 14 | $parameters->setParameters([ 15 | 'foo' => 'bar', 16 | ]); 17 | $this->assertEquals(['foo' => 'bar'], $parameters->toArray()); 18 | $this->assertEquals('bar', $parameters->getParameter('foo')); 19 | $parameters->setParameter('foo', 'baz'); 20 | $this->assertEquals('baz', $parameters->getParameter('foo')); 21 | 22 | $parameters->addParameters([ 23 | 'foo' => 'bar', 24 | 'bar' => 'baz', 25 | ]); 26 | $this->assertEquals([ 27 | 'foo' => 'bar', 28 | 'bar' => 'baz', 29 | ], $parameters->toArray()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/ReferenceTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('di', $reference->getId()); 15 | } 16 | } -------------------------------------------------------------------------------- /Tests/ResolverTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Director::class, $resolver->resolve($definition)); 21 | } 22 | 23 | public function testResolveInvalidClass() 24 | { 25 | $container = new Container(); 26 | $resolver = new Resolver($container); 27 | $definition = new Definition('invalid-class'); 28 | $this->expectException(DependencyInjectionException::class); 29 | $resolver->resolve($definition); 30 | } 31 | 32 | public function testResolveNotInstantiateClass() 33 | { 34 | $container = new Container(); 35 | $resolver = new Resolver($container); 36 | $definition = new Definition(ActorInterface::class); 37 | $this->expectException(DependencyInjectionException::class); 38 | $resolver->resolve($definition); 39 | } 40 | 41 | public function testResolveWithProperty() 42 | { 43 | $container = new Container(); 44 | $resolver = new Resolver($container); 45 | $definition = new Definition(Director::class); 46 | $definition->setProperties([ 47 | 'gender' => 'male', 48 | ]); 49 | $this->assertEquals('male', $resolver->resolve($definition)->gender); 50 | $definition->setProperty('no_exist_property', 'foo'); 51 | $this->assertEquals('foo', $resolver->resolve($definition)->no_exist_property); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/TestClass/Actor.php: -------------------------------------------------------------------------------- 1 | birthday = $birthday; 12 | } 13 | 14 | /** 15 | * @return \DateTime|null 16 | */ 17 | public function getBirthday(): ?\DateTime 18 | { 19 | return $this->birthday; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/TestClass/ActorInterface.php: -------------------------------------------------------------------------------- 1 | baz = $baz; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/TestClass/Director.php: -------------------------------------------------------------------------------- 1 | name = $name; 16 | $this->age = $age; 17 | } 18 | 19 | /** 20 | * @return mixed 21 | */ 22 | public function getName() 23 | { 24 | return $this->name; 25 | } 26 | 27 | /** 28 | * @param mixed $name 29 | */ 30 | public function setName($name) 31 | { 32 | $this->name = $name; 33 | } 34 | 35 | /** 36 | * @return mixed 37 | */ 38 | public function getAge() 39 | { 40 | return $this->age; 41 | } 42 | 43 | /** 44 | * @param mixed $age 45 | */ 46 | public function setAge($age) 47 | { 48 | $this->age = $age; 49 | } 50 | 51 | public function direct($movieName) 52 | { 53 | return new Movie($this, $movieName, date('Y-m-d')); 54 | } 55 | 56 | public static function factory($name, $age) 57 | { 58 | return new Director($name, $age); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/TestClass/Foo.php: -------------------------------------------------------------------------------- 1 | director = $director; 35 | $this->actor = $actor; 36 | } 37 | 38 | /** 39 | * @param mixed $name 40 | */ 41 | public function setName($name) 42 | { 43 | $this->name = $name; 44 | } 45 | 46 | /** 47 | * @return mixed 48 | */ 49 | public function getName() 50 | { 51 | return $this->name; 52 | } 53 | 54 | /** 55 | * @param mixed $time 56 | */ 57 | public function setTime($time) 58 | { 59 | $this->time = $time; 60 | } 61 | 62 | /** 63 | * @return mixed 64 | */ 65 | public function getTime() 66 | { 67 | return $this->time; 68 | } 69 | 70 | /** 71 | * @return Director 72 | */ 73 | public function getDirector() 74 | { 75 | return $this->director; 76 | } 77 | 78 | /** 79 | * 设置女演员. 80 | * 81 | * @param ActorInterface $actress 82 | */ 83 | public function setActress(ActorInterface $actress) 84 | { 85 | $this->actress = $actress; 86 | } 87 | 88 | /** 89 | * @return ActorInterface 90 | */ 91 | public function getActor() 92 | { 93 | return $this->actor; 94 | } 95 | 96 | /** 97 | * @return ActorInterface 98 | */ 99 | public function getActress() 100 | { 101 | return $this->actress; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slince/di", 3 | "description": "A flexible dependency injection container", 4 | "keywords": ["di", "ioc", "container", "dependency injection", "PSR-11", "injection"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Tao", 9 | "email": "taosikai@yeah.net" 10 | } 11 | ], 12 | "minimum-stability": "stable", 13 | "require": { 14 | "php": ">=7.4", 15 | "dflydev/dot-access-data": "^3.0", 16 | "psr/container": "^1.0 || ^2.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "^7.5|^8.0|^9.0" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Slince\\Di\\": "" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./Tests/ 10 | 11 | 12 | 13 | 14 | 15 | ./ 16 | 17 | ./Tests 18 | ./vendor 19 | 20 | 21 | 22 | --------------------------------------------------------------------------------