├── LICENSE ├── composer.json └── src ├── Decorator ├── DecorateDefinition.php └── Decorator.php ├── Exception ├── Logical │ └── ClassNotExistsException.php ├── LogicalException.php └── RuntimeException.php ├── Extension ├── CompilerExtension.php ├── ContainerAwareExtension.php ├── InjectValueExtension.php ├── MutableExtension.php ├── PassCompilerExtension.php └── ResourceExtension.php ├── Helper └── ExtensionDefinitionsHelper.php ├── IContainerAware.php ├── Pass └── AbstractPass.php └── TContainerAware.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Contributte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contributte/di", 3 | "description": "Extra contrib to nette/di", 4 | "keywords": [ 5 | "nette", 6 | "dependency", 7 | "inject" 8 | ], 9 | "type": "library", 10 | "license": "MIT", 11 | "homepage": "https://github.com/contributte/di", 12 | "authors": [ 13 | { 14 | "name": "Milan Felix Šulc", 15 | "homepage": "https://f3l1x.io" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.1", 20 | "nette/di": "^3.1.0", 21 | "nette/utils": "^3.2.8 || ^4.0" 22 | }, 23 | "require-dev": { 24 | "contributte/qa": "^0.4", 25 | "contributte/tester": "^0.3", 26 | "contributte/phpstan": "^0.2", 27 | "nette/robot-loader": "^3.4.2 || ^4.0", 28 | "nette/bootstrap": "^3.1.4" 29 | }, 30 | "conflict": { 31 | "nette/schema": "<1.1.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Contributte\\DI\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Tests\\": "tests" 41 | } 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true, 45 | "config": { 46 | "sort-packages": true, 47 | "allow-plugins": { 48 | "dealerdirect/phpcodesniffer-composer-installer": true 49 | } 50 | }, 51 | "extra": { 52 | "branch-alias": { 53 | "dev-master": "0.6.x-dev" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Decorator/DecorateDefinition.php: -------------------------------------------------------------------------------- 1 | definitions = $definitions; 23 | } 24 | 25 | /** 26 | * @param string|mixed[]|Definition|Reference|Statement $entity 27 | * @param mixed[] $args 28 | */ 29 | public function addSetup(string|array|Definition|Reference|Statement $entity, array $args = []): self 30 | { 31 | foreach ($this->definitions as $definition) { 32 | $definition->addSetup($entity, $args); 33 | } 34 | 35 | return $this; 36 | } 37 | 38 | /** 39 | * @param string[] $tags 40 | */ 41 | public function addTags(array $tags): self 42 | { 43 | $tags = Arrays::normalize($tags, true); 44 | foreach ($this->definitions as $definition) { 45 | $definition->setTags($definition->getTags() + $tags); 46 | } 47 | 48 | return $this; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/Decorator/Decorator.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 20 | $this->definitionsHelper = $definitionsHelper; 21 | } 22 | 23 | public static function of(ContainerBuilder $builder, ExtensionDefinitionsHelper $definitionsHelper): self 24 | { 25 | return new self($builder, $definitionsHelper); 26 | } 27 | 28 | public function decorate(string $type): DecorateDefinition 29 | { 30 | if (!class_exists($type)) { 31 | throw new ClassNotExistsException($type); 32 | } 33 | 34 | return new DecorateDefinition($this->findByType($type)); 35 | } 36 | 37 | /** 38 | * @return ServiceDefinition[] 39 | */ 40 | private function findByType(string $type): array 41 | { 42 | $definitions = $this->definitionsHelper->getServiceDefinitionsFromDefinitions($this->builder->getDefinitions()); 43 | 44 | return array_filter($definitions, static fn (ServiceDefinition $def): bool => $def->getType() !== null && is_a($def->getType(), $type, true)); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Exception/Logical/ClassNotExistsException.php: -------------------------------------------------------------------------------- 1 | helper === null) { 16 | $this->helper = new ExtensionDefinitionsHelper($this->compiler); 17 | } 18 | 19 | return $this->helper; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Extension/ContainerAwareExtension.php: -------------------------------------------------------------------------------- 1 | getContainerBuilder(); 18 | 19 | $definitionsHelper = new ExtensionDefinitionsHelper($this->compiler); 20 | $definitions = $definitionsHelper->getServiceDefinitionsFromDefinitions($builder->findByType(IContainerAware::class)); 21 | 22 | // Register as services 23 | foreach ($definitions as $definition) { 24 | $definition->addSetup('setContainer'); 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/Extension/InjectValueExtension.php: -------------------------------------------------------------------------------- 1 | Expect::bool(false), 27 | ]); 28 | } 29 | 30 | /** 31 | * Find all definitions and inject into @value 32 | */ 33 | public function beforeCompile(): void 34 | { 35 | $builder = $this->getContainerBuilder(); 36 | $config = $this->config; 37 | 38 | $definitions = $config->all 39 | ? $builder->getDefinitions() 40 | : array_map( 41 | [$builder, 'getDefinition'], 42 | array_keys($builder->findByTag(self::TAG_INJECT_VALUE)) 43 | ); 44 | 45 | $definitionsHelper = new ExtensionDefinitionsHelper($this->compiler); 46 | $definitions = $definitionsHelper->getServiceDefinitionsFromDefinitions($definitions); 47 | 48 | foreach ($definitions as $def) { 49 | // Inject @value into definition 50 | $this->inject($def); 51 | } 52 | } 53 | 54 | /** 55 | * Inject into @value property 56 | */ 57 | protected function inject(ServiceDefinition $def): void 58 | { 59 | $class = $def->getType(); 60 | 61 | // Class is not defined, skip it 62 | if ($class === null) { 63 | return; 64 | } 65 | 66 | foreach (get_class_vars($class) as $name => $var) { 67 | $rp = new ReflectionProperty($class, $name); 68 | 69 | // Try to match property by regex 70 | // https://regex101.com/r/D6gc21/1 71 | $match = Strings::match((string) $rp->getDocComment(), '#@value\((.+)\)#U'); 72 | 73 | // If there's no @value annotation or it's not in propel format, 74 | // then skip it 75 | if ($match === null) { 76 | continue; 77 | } 78 | 79 | // Hooray, we have a match! 80 | [, $content] = $match; 81 | 82 | // Expand content of @value and setup to definition 83 | $def->addSetup('$' . $name, [$this->expand($content)]); 84 | } 85 | } 86 | 87 | protected function expand(string $value): mixed 88 | { 89 | return Helpers::expand($value, $this->getContainerBuilder()->parameters); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/Extension/MutableExtension.php: -------------------------------------------------------------------------------- 1 | onLoad($this, $this->getContainerBuilder(), $this->getConfig()); 29 | } 30 | 31 | /** 32 | * Decorate services 33 | */ 34 | public function beforeCompile(): void 35 | { 36 | $this->onBefore($this, $this->getContainerBuilder(), $this->getConfig()); 37 | } 38 | 39 | public function afterCompile(ClassType $class): void 40 | { 41 | $this->onAfter($this, $class, $this->getConfig()); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Extension/PassCompilerExtension.php: -------------------------------------------------------------------------------- 1 | passes as $pass) { 22 | $pass->loadPassConfiguration(); 23 | } 24 | } 25 | 26 | /** 27 | * Decorate services 28 | */ 29 | public function beforeCompile(): void 30 | { 31 | // Trigger passes 32 | foreach ($this->passes as $pass) { 33 | $pass->beforePassCompile(); 34 | } 35 | } 36 | 37 | public function afterCompile(ClassType $class): void 38 | { 39 | // Trigger passes 40 | foreach ($this->passes as $pass) { 41 | $pass->afterPassCompile($class); 42 | } 43 | } 44 | 45 | protected function addPass(AbstractPass $pass): self 46 | { 47 | $this->passes[] = $pass; 48 | 49 | return $this; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/Extension/ResourceExtension.php: -------------------------------------------------------------------------------- 1 | */ 24 | private array $map = []; 25 | 26 | public function getConfigSchema(): Schema 27 | { 28 | return Expect::structure([ 29 | 'resources' => Expect::arrayOf( 30 | Expect::structure([ 31 | 'paths' => Expect::arrayOf('string'), 32 | 'excludes' => Expect::arrayOf('string'), 33 | 'decorator' => Expect::structure([ 34 | 'tags' => Expect::array(), 35 | 'setup' => Expect::listOf('callable|Nette\DI\Definitions\Statement|array:1'), 36 | 'autowired' => Expect::type('bool|string|array')->nullable(), 37 | 'inject' => Expect::bool()->nullable(), 38 | ]), 39 | ]) 40 | ), 41 | ]); 42 | } 43 | 44 | public function loadConfiguration(): void 45 | { 46 | $config = $this->config; 47 | 48 | foreach ($config->resources as $namespace => $resource) { 49 | if (substr($namespace, -1) !== '\\') { 50 | throw new RuntimeException(sprintf('Resource "%s" must end with /', $namespace)); 51 | } 52 | 53 | // Find classes of given resource 54 | $classes = $this->findClasses($namespace, $resource->paths, $resource->excludes); 55 | 56 | // Store found classes 57 | $this->map[] = [ 58 | 'namespace' => $namespace, 59 | 'resource' => $resource, 60 | 'classes' => $classes, 61 | ]; 62 | } 63 | } 64 | 65 | public function beforeCompile(): void 66 | { 67 | $builder = $this->getContainerBuilder(); 68 | 69 | foreach ($this->map as $config) { 70 | $classes = $config['classes']; 71 | sort($classes); 72 | $resource = $config['resource']; 73 | $namespace = $config['namespace']; 74 | 75 | // Register services of given resource 76 | $counter = 1; 77 | $name = preg_replace('#\W+#', '_', '.' . $namespace); 78 | foreach ($classes as $class) { 79 | // Check already registered classes 80 | if ($builder->getByType($class) !== null) { 81 | continue; 82 | } 83 | 84 | $def = $builder->addDefinition($this->prefix($name . '.' . $counter++)) 85 | ->setFactory($class) 86 | ->setType($class); 87 | 88 | $decorator = $resource->decorator; 89 | 90 | if ($decorator->tags !== []) { 91 | $def->setTags(Arrays::normalize($decorator->tags, true)); 92 | } 93 | 94 | if ($decorator->setup !== []) { 95 | foreach ($decorator->setup as $setup) { 96 | if (is_array($setup)) { 97 | $key = key($setup); 98 | $key = is_int($key) ? (string) $key : $key; 99 | $setup = new Statement($key, array_values($setup)); 100 | } 101 | 102 | $def->addSetup($setup); 103 | } 104 | } 105 | 106 | if ($decorator->autowired !== null) { 107 | $def->setAutowired($decorator->autowired); 108 | } 109 | 110 | if ($decorator->inject !== null) { 111 | $def->addTag(InjectExtension::TagInject, $decorator->inject); 112 | } 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * Find classes by given arguments 119 | * 120 | * @param string[] $dirs 121 | * @param string[] $excludes 122 | * @return string[] 123 | */ 124 | protected function findClasses(string $namespace, array $dirs, array $excludes = []): array 125 | { 126 | $loader = $this->createRobotLoader(); 127 | $loader->addDirectory(...$dirs); 128 | $loader->rebuild(); 129 | 130 | $indexed = $loader->getIndexedClasses(); 131 | $classes = []; 132 | foreach ($indexed as $class => $file) { 133 | // Different namespace 134 | if (!str_starts_with($class, $namespace)) { 135 | continue; 136 | } 137 | 138 | // Excluded namespace 139 | if (array_filter($excludes, static fn (string $exclude): bool => str_starts_with($class, $exclude)) !== []) { 140 | continue; 141 | } 142 | 143 | // Skip not existing class 144 | if (!class_exists($class)) { 145 | continue; 146 | } 147 | 148 | // Detect by reflection 149 | $ct = new ReflectionClass($class); 150 | 151 | // Skip abstract 152 | if ($ct->isAbstract()) { 153 | continue; 154 | } 155 | 156 | // All tests passed, it's our class 157 | $classes[] = $class; 158 | } 159 | 160 | return $classes; 161 | } 162 | 163 | protected function createRobotLoader(): RobotLoader 164 | { 165 | if (!class_exists(RobotLoader::class)) { 166 | throw new InvalidStateException('Install nette/robot-loader at first'); 167 | } 168 | 169 | return new RobotLoader(); 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/Helper/ExtensionDefinitionsHelper.php: -------------------------------------------------------------------------------- 1 | compiler = $compiler; 21 | } 22 | 23 | /** 24 | * @param Definition[] $definitions 25 | * @return ServiceDefinition[] 26 | */ 27 | public function getServiceDefinitionsFromDefinitions(array $definitions): array 28 | { 29 | $serviceDefinitions = []; 30 | $resolver = new Resolver($this->compiler->getContainerBuilder()); 31 | 32 | foreach ($definitions as $definition) { 33 | if ($definition instanceof ServiceDefinition) { 34 | $serviceDefinitions[] = $definition; 35 | } elseif ($definition instanceof FactoryDefinition) { 36 | $serviceDefinitions[] = $definition->getResultDefinition(); 37 | } elseif ($definition instanceof LocatorDefinition) { 38 | $references = $definition->getReferences(); 39 | foreach ($references as $reference) { 40 | // Check that reference is valid 41 | $reference = $resolver->normalizeReference($reference); 42 | // Get definition by reference 43 | $definition = $resolver->resolveReference($reference); 44 | // Only ServiceDefinition should be possible here 45 | assert($definition instanceof ServiceDefinition); 46 | $serviceDefinitions[] = $definition; 47 | } 48 | } else { 49 | // Definition is of type: 50 | // accessor - service definition exists independently 51 | // imported - runtime-created service, cannot work with 52 | // unknown 53 | continue; 54 | } 55 | } 56 | 57 | // Filter out duplicates - we cannot distinguish if service from LocatorDefinition is created by accessor or factory so duplicates are possible 58 | $serviceDefinitions = array_unique($serviceDefinitions, SORT_REGULAR); 59 | 60 | return $serviceDefinitions; 61 | } 62 | 63 | /** 64 | * @param string|mixed[]|Statement $config 65 | */ 66 | public function getDefinitionFromConfig(string|array|Statement $config, string $preferredPrefix): Definition|string 67 | { 68 | $builder = $this->compiler->getContainerBuilder(); 69 | 70 | // Definition is defined in ServicesExtension, try to get it 71 | if (is_string($config) && str_starts_with($config, '@')) { 72 | $definitionName = substr($config, 1); 73 | 74 | // Definition is already loaded (beforeCompile phase), return it 75 | if ($builder->hasDefinition($definitionName)) { 76 | return $builder->getDefinition($definitionName); 77 | } 78 | 79 | // Definition not loaded yet (loadConfiguration phase), return reference string 80 | return $config; 81 | } 82 | 83 | // Raw configuration given, create definition from it 84 | $this->compiler->loadDefinitionsFromConfig([$preferredPrefix => $config]); 85 | 86 | return $builder->getDefinition($preferredPrefix); 87 | } 88 | 89 | /** 90 | * Check if config is valid callable or callable syntax which may result in valid callable at runtime and returns an definition otherwise 91 | * 92 | * @param string|mixed[]|Statement $config 93 | */ 94 | public function getCallableFromConfig(string|array|Statement $config, string $preferredPrefix): mixed 95 | { 96 | if (is_callable($config)) { 97 | return $config; 98 | } 99 | 100 | // Might be valid callable at runtime 101 | if (is_array($config) && is_callable($config, true) && is_string($config[0]) && str_starts_with($config[0], '@')) { 102 | return $config; 103 | } 104 | 105 | return $this->getDefinitionFromConfig($config, $preferredPrefix); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/IContainerAware.php: -------------------------------------------------------------------------------- 1 | extension = $extension; 16 | } 17 | 18 | /** 19 | * Register services 20 | */ 21 | public function loadPassConfiguration(): void 22 | { 23 | // No-op 24 | } 25 | 26 | /** 27 | * Decorate services 28 | */ 29 | public function beforePassCompile(): void 30 | { 31 | // No-op 32 | } 33 | 34 | public function afterPassCompile(ClassType $class): void 35 | { 36 | // No-op 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/TContainerAware.php: -------------------------------------------------------------------------------- 1 | container = $container; 15 | } 16 | 17 | } 18 | --------------------------------------------------------------------------------