├── src └── AutoDI │ ├── Exceptions │ ├── IncompleteServiceDefinition.php │ └── NoServiceRegistered.php │ ├── ClassList.php │ └── DI │ └── AutoDIExtension.php ├── composer.json ├── LICENSE └── README.md /src/AutoDI/Exceptions/IncompleteServiceDefinition.php: -------------------------------------------------------------------------------- 1 | classes = array_values($classes); 19 | } 20 | 21 | public function getMatching(string $classPattern): ClassList 22 | { 23 | $classes = preg_grep($this->buildRegex($classPattern), $this->classes); 24 | 25 | return new ClassList($classes); 26 | } 27 | 28 | /** 29 | * @return string 30 | */ 31 | private function buildRegex(string $classPattern): string 32 | { 33 | $replacements = [ 34 | '~\\*\\*~' => '(.*)', // ** for n-level wildcard 35 | '~(\\\\)~' => '\\\\\\\\', // \ as NS delimiter 36 | '~(? '\w+', // * for single NS level / class name wildcard 37 | '~\{((\w+,?)+)\}~' => '($1)', 38 | '~,~' => '|', // PHP 7-like group use 39 | ]; 40 | 41 | $regex = preg_replace( 42 | array_keys($replacements), 43 | array_values($replacements), 44 | $classPattern 45 | ); 46 | 47 | return "~^$regex$~"; 48 | } 49 | 50 | public function getClasses(): ClassList 51 | { 52 | $classes = array_filter($this->classes, function ($c) { 53 | $reflection = new \ReflectionClass($c); 54 | return ! $reflection->isTrait() && ! $reflection->isInterface() && ! $reflection->isAbstract(); 55 | }); 56 | 57 | return new ClassList($classes); 58 | } 59 | 60 | public function getInterfaces(): ClassList 61 | { 62 | $interfaces = array_filter($this->classes, function ($c) { 63 | return (new \ReflectionClass($c))->isInterface(); 64 | }); 65 | 66 | return new ClassList($interfaces); 67 | } 68 | 69 | public function getWithoutClasses(ClassList $list): ClassList 70 | { 71 | return new ClassList(array_diff($this->classes, $list->classes)); 72 | } 73 | 74 | /** 75 | * @return string[] 76 | */ 77 | public function toArray(): array 78 | { 79 | return $this->classes; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/AutoDI/DI/AutoDIExtension.php: -------------------------------------------------------------------------------- 1 | Expect::listOf(Expect::array()), 21 | 'errorOnNotMatchedDefinitions' => Expect::bool(true), 22 | 'registerOnConfiguration' => Expect::bool(false), 23 | 'directories' => Expect::listOf(Expect::string())->default([$this->getContainerBuilder()->parameters['appDir']]), 24 | 'defaults' => Expect::array(), 25 | 'tempDir' => Expect::string($this->getContainerBuilder()->parameters['tempDir']), 26 | ]); 27 | } 28 | 29 | public function beforeCompile(): void 30 | { 31 | if ( ! $this->shouldRegisterOnConfiguration()) { 32 | $this->registerServices(); 33 | } 34 | } 35 | 36 | public function loadConfiguration(): void 37 | { 38 | if ($this->shouldRegisterOnConfiguration()) { 39 | $this->registerServices(); 40 | } 41 | } 42 | 43 | private function shouldRegisterOnConfiguration(): bool 44 | { 45 | return (bool) $this->getConfig()->registerOnConfiguration; 46 | } 47 | 48 | private function registerServices(): void 49 | { 50 | $config = $this->getConfig(); 51 | 52 | $robotLoader = new RobotLoader(); 53 | 54 | foreach ($config->directories as $directory) { 55 | $robotLoader->addDirectory($directory); 56 | } 57 | 58 | $robotLoader->setTempDirectory($config->tempDir); 59 | $robotLoader->rebuild(); 60 | 61 | $classes = new ClassList( 62 | array_keys($robotLoader->getIndexedClasses()) 63 | ); 64 | 65 | $builder = $this->getContainerBuilder(); 66 | 67 | foreach ($config->services as $service) { 68 | [$field, $matchingClasses] = $this->getClasses($service, $classes); 69 | 70 | if (isset($service['exclude'])) { 71 | $excluded = $service['exclude']; 72 | $matchingClasses = $this->removeExcludedClasses($matchingClasses, is_string($excluded) ? [$excluded] : $excluded); 73 | unset($service['exclude']); 74 | } 75 | 76 | $matchingClasses = array_filter($matchingClasses->toArray(), function ($class) use ($builder) { 77 | return count($builder->findByType($class)) === 0; 78 | }); 79 | 80 | $service += $config->defaults; 81 | 82 | $services = array_map(function ($class) use ($service, $field) { 83 | $service[$field] = $class; 84 | return $service; 85 | }, $matchingClasses); 86 | 87 | if ($config->errorOnNotMatchedDefinitions && count($services) === 0) { 88 | throw NoServiceRegistered::byPattern($field); 89 | } 90 | 91 | $this->compiler->loadDefinitionsFromConfig($services); 92 | } 93 | } 94 | 95 | /** 96 | * @return array [definition field, Class list] 97 | */ 98 | private function getClasses(array $service, ClassList $classes): array 99 | { 100 | $types = [ 101 | 'class' => $classes->getClasses(), 102 | 'implement' => $classes->getInterfaces(), 103 | ]; 104 | 105 | if (count(array_intersect_key($service, $types)) !== 1) { 106 | throw IncompleteServiceDefinition::fromDefinition($service); 107 | } 108 | 109 | foreach($types as $field => $filteredClasses) { 110 | if(!isset($service[$field])) { 111 | continue; 112 | } 113 | 114 | /* @var $filteredClasses ClassList */ 115 | 116 | return [ 117 | $field, 118 | $filteredClasses->getMatching($service[$field]), 119 | ]; 120 | } 121 | 122 | throw new \RuntimeException('This should never happen'); 123 | } 124 | 125 | /** 126 | * @param string[] $exludedPatterns 127 | */ 128 | private function removeExcludedClasses(ClassList $classes, array $exludedPatterns): ClassList 129 | { 130 | return array_reduce($exludedPatterns, function(ClassList $c, $pattern) { 131 | return $c->getWithoutClasses($c->getMatching($pattern)); 132 | }, $classes); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Service auto registration and configuration for Nette DI 2 | 3 | [![Build Status](https://travis-ci.org/fmasa/auto-di.svg?branch=2.x)](https://travis-ci.org/fmasa/auto-di) 4 | [![Coverage Status](https://coveralls.io/repos/github/fmasa/auto-di/badge.svg?branch=2.x)](https://coveralls.io/github/fmasa/auto-di?branch=2.x) 5 | 6 | `fmasa\autoDI` is package intended to make registration and configuration 7 | of services easier. 8 | 9 | ## Future of extension 10 | In Nette 3.0, [SearchExtension](https://doc.nette.org/en/3.0/di-builtin-extensions#toc-searchextension) was introduced. 11 | It builds on the same concept as this extension (in which I stole the concept from Symfony DependencyInjection). 12 | Unfortunately there are still a few problems that need to be fixed in order to fully replace `fmasa/auto-di` (i.e. [nette/di#215](https://github.com/nette/di/issues/215)), but it's on the other hand part of Nette DI core and supports several features, that `fmasa/auto-di` doesn't (yet). 13 | These are ways to register services implementing specific interface or extending specific class. 14 | 15 | So long-term goal is to deprecate this extension in favor of SearchExtension, when (and if) SearchExtension reaches feature parity. 16 | Until then I plan to maintain and improve this package since I use it myself in many projects. 17 | 18 | ## Installation 19 | The best way to install fmasa/auto-di is using [Composer](https://getcomposer.org/): 20 | 21 | $ composer require fmasa/auto-di 22 | 23 | 24 | To enable auto registration register extension in your `config.neon`: 25 | 26 | ```yaml 27 | extensions: 28 | autoDI: Fmasa\AutoDI\DI\AutoDIExtension 29 | ``` 30 | 31 | ## Pattern based definition 32 | 33 | 34 | `autoDI` registers services defined by regex: 35 | 36 | ```yaml 37 | autoDI: 38 | services: 39 | - class: App\Model\**\*Repository 40 | ``` 41 | This registers every class under namespace `App\Model` which name ends with `Repository`: 42 | 43 | - App\Model\Eshop\UserRepository 44 | - App\Model\CMS\Comments\CommentsRepository 45 | 46 | There are several simple operators that can be used in patterns: 47 | 48 | - `*` matches class name, one namespace level or part of it (without \\) 49 | - `**` matches any part of namespace or classname (including \\) 50 | - `{Eshop,CMS}` options list, any item of list matches this pattern 51 | 52 | Apart from these, any PCRE regular expression can be used. 53 | 54 | ## Classes and generated factories 55 | 56 | Package supports both classes and [generated factories](https://doc.nette.org/en/2.4/di-usage#toc-component-factory). 57 | 58 | Classes are matched agains `class` field, factories againts `implement` field, 59 | which corresponds to way Nette use these fields. 60 | 61 | When using `class` field, all matching interfaces are skipped and vice versa. 62 | 63 | ```yaml 64 | autoDI: 65 | services: 66 | # Repositories 67 | - class: App\Model\**\*Repository 68 | 69 | # Component factories 70 | - implement: App\Components\very**\I*Factory 71 | ``` 72 | 73 | ## Tags, autowiring, ... 74 | 75 | Every option supported in DI (tags, inject, autowiring, ...) is supported with same syntax 76 | as normal service registration 77 | 78 | ```yaml 79 | autoDI: 80 | services: 81 | # Repositories 82 | - class: App\Model\Subscribers\** 83 | tags: [eventBus.subscriber] 84 | ``` 85 | 86 | The snippet above registers all classes in `App\Model\Subscribers` namespace 87 | with `eventBus.subscriber` tag. 88 | 89 | ## Exclude services 90 | 91 | Sometimes we wan't to exlude certain services from registration. For that we can use `exclude` field, 92 | that accepts pattern or list of patterns: 93 | 94 | ```yaml 95 | autoDI: 96 | services: 97 | - class: App\Model\** 98 | exclude: App\Model\{Entities,DTO}** 99 | ``` 100 | 101 | which is same as 102 | 103 | ```yaml 104 | autoDI: 105 | services: 106 | - class: App\Model\** 107 | exclude: 108 | - App\Model\Entities** 109 | - App\Model\DTO** 110 | ``` 111 | 112 | ## Already registered services 113 | 114 | When extension founds service, that is already registered 115 | (by `services` section, different extension or previous `autoDI` definition), **it's skipped**. 116 | 117 | This allows manual registration of specific services that need specific configuration. 118 | 119 | ## Defaults section 120 | 121 | To specify base configuration for all services registered via `autoDI`, `defaults` section 122 | can be used: 123 | 124 | ```yaml 125 | autoDI: 126 | defaults: 127 | tags: [ my.auto.service ] 128 | 129 | services: 130 | # these services will have tag my.auto.service 131 | - class: App\Model\Repositories\** 132 | 133 | # these services will have only tag eventBus.subscriber 134 | - class: app\Model\Subscribers\** 135 | tags: [ eventBus.subscriber ] 136 | ``` 137 | 138 | ## Configuring directories 139 | 140 | By default extension searches for services in `%appDir%`, but other directories can be specified: 141 | 142 | ```yaml 143 | autoDI: 144 | directories: 145 | - %appDir% 146 | - %appDir%/../vendor 147 | ``` 148 | 149 | ## Register services on configuration 150 | 151 | Compiler extensions such as AutoDIExtension manipulates the DI container 152 | in two phases (configuration loading and before compilation). 153 | By default this extension registers all services before compilation. 154 | This may not be optimal if you wan't to use this extension with other extensions 155 | such as decorator. 156 | 157 | You can enforce registration in configuration phase 158 | by setting `registerOnConfiguration` option to true. 159 | 160 | When no service is registered for configuration entry, either because no class/interface 161 | matches the pattern or all matched services were already registered in container, exception 162 | is thrown. This check can be disabled by setting `errorOnNotMatchedDefinitions` option to false. 163 | --------------------------------------------------------------------------------