├── .readthedocs.yaml ├── LICENSE ├── composer.json ├── renovate.json └── src ├── Builder.php ├── Compiler.php ├── Compiler ├── DumpXmlContainer.php └── ParameterBag.php ├── CompilerPassListProvider.php ├── Config ├── ContainerConfiguration.php └── Package.php ├── ContainerBuilder.php ├── FileListProvider.php ├── Generator.php ├── Generators ├── Delegating.php ├── Php.php ├── Xml.php └── Yaml.php └── Testing └── MakeServicesPublic.php /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3" 7 | 8 | mkdocs: 9 | configuration: mkdocs.yml 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Luís Otávio Cobucci Oblonczyk 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lcobucci/di-builder", 3 | "description": "Dependency Injection Builder for PHP applications", 4 | "license": [ 5 | "BSD-3-Clause" 6 | ], 7 | "type": "library", 8 | "keywords": [ 9 | "dependency-injection" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Luís Cobucci", 14 | "email": "lcobucci@gmail.com" 15 | } 16 | ], 17 | "homepage": "https://github.com/lcobucci/di-builder", 18 | "require": { 19 | "php": "~8.3.0 || ~8.4.0", 20 | "symfony/config": "^7.1", 21 | "symfony/dependency-injection": "^7.1", 22 | "symfony/expression-language": "^7.1", 23 | "symfony/filesystem": "^7.1" 24 | }, 25 | "require-dev": { 26 | "infection/infection": "^0.29", 27 | "lcobucci/coding-standard": "^11.1", 28 | "mikey179/vfsstream": "^1.6.12", 29 | "phpstan/extension-installer": "^1.4", 30 | "phpstan/phpstan": "^2.0", 31 | "phpstan/phpstan-deprecation-rules": "^2.0", 32 | "phpstan/phpstan-phpunit": "^2.0", 33 | "phpstan/phpstan-strict-rules": "^2.0", 34 | "phpunit/phpunit": "^12.0", 35 | "squizlabs/php_codesniffer": "^3.10", 36 | "symfony/yaml": "^7.1" 37 | }, 38 | "replace": { 39 | "symfony/polyfill-php71": "*", 40 | "symfony/polyfill-php72": "*", 41 | "symfony/polyfill-php73": "*", 42 | "symfony/polyfill-php74": "*", 43 | "symfony/polyfill-php80": "*", 44 | "symfony/polyfill-php81": "*", 45 | "symfony/polyfill-php82": "*" 46 | }, 47 | "autoload": { 48 | "psr-4": { 49 | "Lcobucci\\DependencyInjection\\": "src" 50 | } 51 | }, 52 | "autoload-dev": { 53 | "psr-4": { 54 | "Lcobucci\\DependencyInjection\\": "test" 55 | } 56 | }, 57 | "config": { 58 | "allow-plugins": { 59 | "dealerdirect/phpcodesniffer-composer-installer": true, 60 | "infection/extension-installer": true, 61 | "ocramius/package-versions": true, 62 | "phpstan/extension-installer": true 63 | }, 64 | "preferred-install": "dist", 65 | "sort-packages": true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>lcobucci/.github:renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/Builder.php: -------------------------------------------------------------------------------- 1 | $className 34 | * @param mixed[] $constructArguments 35 | */ 36 | public function addDelayedPass( 37 | string $className, 38 | array $constructArguments = [], 39 | string $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, 40 | int $priority = self::DEFAULT_PRIORITY, 41 | ): Builder; 42 | 43 | /** 44 | * @param class-string $className 45 | * @param mixed[] $constructArguments 46 | */ 47 | public function addPackage(string $className, array $constructArguments = []): Builder; 48 | 49 | /** 50 | * Configures the application profile name to support profile-specific services 51 | */ 52 | public function setProfileName(string $profileName): Builder; 53 | 54 | /** 55 | * Configure the container to track file updates 56 | */ 57 | public function enableDebugging(): Builder; 58 | 59 | /** 60 | * Configures the dump directory 61 | */ 62 | public function setDumpDir(string $dir): Builder; 63 | 64 | /** 65 | * Adds a default parameter 66 | */ 67 | public function setParameter(string $name, mixed $value): Builder; 68 | 69 | /** 70 | * Adds a path to load the files 71 | */ 72 | public function addPath(string $path): Builder; 73 | 74 | /** 75 | * Configures the container's base class 76 | */ 77 | public function setBaseClass(string $class): Builder; 78 | 79 | /** 80 | * Creates the container with the given configuration 81 | */ 82 | public function getContainer(): ContainerInterface; 83 | 84 | /** 85 | * Creates a test container with the given configuration 86 | */ 87 | public function getTestContainer(): ContainerInterface; 88 | } 89 | -------------------------------------------------------------------------------- /src/Compiler.php: -------------------------------------------------------------------------------- 1 | isFresh()) { 33 | return; 34 | } 35 | 36 | $container = $generator->initializeContainer($config); 37 | 38 | $this->configurePassList($container, $config); 39 | $this->updateDump($container, $config, $dump); 40 | } 41 | 42 | private function configurePassList( 43 | SymfonyBuilder $container, 44 | ContainerConfiguration $config, 45 | ): void { 46 | foreach ($config->getPassList() as $passConfig) { 47 | [$pass, $type, $priority] = $passConfig + self::DEFAULT_PASS_CONFIG; 48 | 49 | if (! $pass instanceof CompilerPassInterface) { 50 | [$className, $constructArguments] = $pass; 51 | 52 | $pass = new $className(...$constructArguments); 53 | } 54 | 55 | $container->addCompilerPass($pass, $type, $priority); 56 | } 57 | } 58 | 59 | /** @throws RuntimeException */ 60 | private function updateDump( 61 | SymfonyBuilder $container, 62 | ContainerConfiguration $config, 63 | ConfigCache $dump, 64 | ): void { 65 | $container->compile(); 66 | 67 | $this->writeToFiles( 68 | $this->getContainerContent($container, $config, $dump), 69 | dirname($dump->getPath()) . '/', 70 | $dump, 71 | $container, 72 | ); 73 | } 74 | 75 | /** @return array */ 76 | private function getContainerContent( 77 | SymfonyBuilder $container, 78 | ContainerConfiguration $config, 79 | ConfigCache $dump, 80 | ): array { 81 | $options = $config->getDumpOptions(); 82 | $options['file'] = $dump->getPath(); 83 | $options['debug'] = $container->getParameter('app.devmode'); 84 | $options['as_files'] = true; 85 | 86 | $options['inline_factories'] = $options['debug'] === false; 87 | $options['inline_class_loader'] = $options['inline_factories']; 88 | 89 | $content = (new PhpDumper($container))->dump($options); 90 | assert(is_array($content) && ! array_is_list($content)); 91 | 92 | // @phpstan-ignore return.type 93 | return $content; 94 | } 95 | 96 | /** 97 | * @param string[] $content 98 | * 99 | * @throws RuntimeException 100 | */ 101 | private function writeToFiles( 102 | array $content, 103 | string $baseDir, 104 | ConfigCache $dump, 105 | SymfonyBuilder $container, 106 | ): void { 107 | $rootCode = array_pop($content); 108 | assert(is_string($rootCode)); 109 | 110 | $filesystem = new Filesystem(); 111 | 112 | foreach ($content as $file => $code) { 113 | $filesystem->dumpFile($baseDir . $file, $code); 114 | } 115 | 116 | $dump->write($rootCode, $container->getResources()); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Compiler/DumpXmlContainer.php: -------------------------------------------------------------------------------- 1 | getParameter('app.devmode') === false || $this->configCache->isFresh()) { 20 | return; 21 | } 22 | 23 | $this->configCache->write((new XmlDumper($container))->dump(), $container->getResources()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Compiler/ParameterBag.php: -------------------------------------------------------------------------------- 1 | $parameters */ 19 | public function __construct(private array $parameters = []) 20 | { 21 | } 22 | 23 | public function set(string $name, mixed $value): void 24 | { 25 | $this->parameters[$name] = $value; 26 | } 27 | 28 | public function get(string $name, mixed $default = null): mixed 29 | { 30 | return $this->parameters[$name] ?? $default; 31 | } 32 | 33 | public function process(ContainerBuilder $container): void 34 | { 35 | $container->getParameterBag()->add($this->parameters); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/CompilerPassListProvider.php: -------------------------------------------------------------------------------- 1 | */ 13 | public function getCompilerPasses(): DefaultGenerator; 14 | } 15 | -------------------------------------------------------------------------------- /src/Config/ContainerConfiguration.php: -------------------------------------------------------------------------------- 1 | , 1: mixed[]}, 1?: string, 2?: int}> $passList 42 | * @param string[] $paths 43 | * @param list, 1: mixed[]}> $packages 44 | * 45 | * phpcs:enable Generic.Files.LineLength 46 | */ 47 | public function __construct( 48 | private string $namespace, 49 | private array $files = [], 50 | private array $passList = [], 51 | private array $paths = [], 52 | private array $packages = [], 53 | ) { 54 | $this->dumpDir = sys_get_temp_dir(); 55 | } 56 | 57 | public function setProfileName(string $profileName): void 58 | { 59 | $this->profileName = $profileName; 60 | } 61 | 62 | public function profileName(): ?string 63 | { 64 | return $this->profileName; 65 | } 66 | 67 | /** @return Package[] */ 68 | public function getPackages(): array 69 | { 70 | if ($this->initializedPackages === null) { 71 | $this->initializedPackages = array_map( 72 | static function (array $data): Package { 73 | [$package, $arguments] = $data; 74 | 75 | return new $package(...$arguments); 76 | }, 77 | $this->packages, 78 | ); 79 | } 80 | 81 | return $this->initializedPackages; 82 | } 83 | 84 | /** 85 | * @param class-string $className 86 | * @param mixed[] $constructArguments 87 | */ 88 | public function addPackage(string $className, array $constructArguments = []): void 89 | { 90 | $this->packages[] = [$className, $constructArguments]; 91 | } 92 | 93 | /** @return Generator */ 94 | public function getFiles(): Generator 95 | { 96 | foreach ($this->filterPackages(FileListProvider::class) as $package) { 97 | yield from $package->getFiles(); 98 | } 99 | 100 | yield from $this->files; 101 | } 102 | 103 | /** 104 | * @template T 105 | * 106 | * @param class-string $packageType 107 | * 108 | * @return array 109 | */ 110 | private function filterPackages(string $packageType): array 111 | { 112 | return array_filter( 113 | $this->getPackages(), 114 | static function (Package $package) use ($packageType): bool { 115 | return $package instanceof $packageType; 116 | }, 117 | ); 118 | } 119 | 120 | public function addFile(string $file): void 121 | { 122 | $this->files[] = $file; 123 | } 124 | 125 | /** @return Generator, 1: mixed[]}, 1?: string, 2?: int}> */ 126 | public function getPassList(): Generator 127 | { 128 | foreach ($this->filterPackages(CompilerPassListProvider::class) as $package) { 129 | yield from $package->getCompilerPasses(); 130 | } 131 | 132 | yield from $this->passList; 133 | } 134 | 135 | public function addPass( 136 | CompilerPassInterface $pass, 137 | string $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, 138 | int $priority = Builder::DEFAULT_PRIORITY, 139 | ): void { 140 | $this->passList[] = [$pass, $type, $priority]; 141 | } 142 | 143 | /** 144 | * @param class-string $className 145 | * @param mixed[] $constructArguments 146 | */ 147 | public function addDelayedPass( 148 | string $className, 149 | array $constructArguments, 150 | string $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, 151 | int $priority = Builder::DEFAULT_PRIORITY, 152 | ): void { 153 | $this->passList[] = [[$className, $constructArguments], $type, $priority]; 154 | } 155 | 156 | /** @return string[] */ 157 | public function getPaths(): array 158 | { 159 | return $this->paths; 160 | } 161 | 162 | public function addPath(string $path): void 163 | { 164 | $this->paths[] = $path; 165 | } 166 | 167 | public function getBaseClass(): ?string 168 | { 169 | return $this->baseClass; 170 | } 171 | 172 | public function setBaseClass(string $baseClass): void 173 | { 174 | $this->baseClass = '\\' . ltrim($baseClass, '\\'); 175 | } 176 | 177 | public function withSubNamespace(string $namespace): self 178 | { 179 | $config = clone $this; 180 | $config->namespace .= '\\' . ltrim($namespace, '\\'); 181 | 182 | return $config; 183 | } 184 | 185 | public function getDumpDir(): string 186 | { 187 | return $this->dumpDir; 188 | } 189 | 190 | public function setDumpDir(string $dumpDir): void 191 | { 192 | $this->dumpDir = rtrim($dumpDir, DIRECTORY_SEPARATOR); 193 | } 194 | 195 | public function getDumpFile(): string 196 | { 197 | return $this->dumpDir . DIRECTORY_SEPARATOR 198 | . strtolower(str_replace('\\', '_', $this->namespace)) . DIRECTORY_SEPARATOR 199 | . ($this->profileName !== null ? $this->profileName . DIRECTORY_SEPARATOR : '') 200 | . self::CLASS_NAME . '.php'; 201 | } 202 | 203 | public function getClassName(): string 204 | { 205 | return $this->namespace . '\\' . self::CLASS_NAME; 206 | } 207 | 208 | /** @return array */ 209 | public function getDumpOptions(): array 210 | { 211 | $options = [ 212 | 'class' => self::CLASS_NAME, 213 | 'namespace' => ltrim($this->namespace, '\\'), 214 | ]; 215 | 216 | if ($this->baseClass !== null) { 217 | $options['base_class'] = $this->baseClass; 218 | } 219 | 220 | $options['hot_path_tag'] = 'container.hot_path'; 221 | 222 | return $options; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/Config/Package.php: -------------------------------------------------------------------------------- 1 | setDefaultConfiguration(); 26 | } 27 | 28 | /** @param class-string $builderClass */ 29 | public static function xml( 30 | string $configurationFile, 31 | string $namespace, 32 | string $builderClass = SymfonyBuilder::class, 33 | ): self { 34 | return new self( 35 | new ContainerConfiguration($namespace), 36 | new Generators\Xml($configurationFile, $builderClass), 37 | new ParameterBag(), 38 | ); 39 | } 40 | 41 | /** @param class-string $builderClass */ 42 | public static function php( 43 | string $configurationFile, 44 | string $namespace, 45 | string $builderClass = SymfonyBuilder::class, 46 | ): self { 47 | return new self( 48 | new ContainerConfiguration($namespace), 49 | new Generators\Php($configurationFile, $builderClass), 50 | new ParameterBag(), 51 | ); 52 | } 53 | 54 | /** @param class-string $builderClass */ 55 | public static function yaml( 56 | string $configurationFile, 57 | string $namespace, 58 | string $builderClass = SymfonyBuilder::class, 59 | ): self { 60 | return new self( 61 | new ContainerConfiguration($namespace), 62 | new Generators\Yaml($configurationFile, $builderClass), 63 | new ParameterBag(), 64 | ); 65 | } 66 | 67 | /** @param class-string $builderClass */ 68 | public static function delegating( 69 | string $configurationFile, 70 | string $namespace, 71 | string $builderClass = SymfonyBuilder::class, 72 | ): self { 73 | return new self( 74 | new ContainerConfiguration($namespace), 75 | new Generators\Delegating($configurationFile, $builderClass), 76 | new ParameterBag(), 77 | ); 78 | } 79 | 80 | /** 81 | * Configures the default parameters and appends the handler 82 | */ 83 | private function setDefaultConfiguration(): void 84 | { 85 | $this->parameterBag->set('app.devmode', false); 86 | 87 | $this->config->addPass($this->parameterBag); 88 | } 89 | 90 | public function addFile(string $file): Builder 91 | { 92 | $this->config->addFile($file); 93 | 94 | return $this; 95 | } 96 | 97 | public function addPass( 98 | CompilerPassInterface $pass, 99 | string $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, 100 | int $priority = self::DEFAULT_PRIORITY, 101 | ): Builder { 102 | $this->config->addPass($pass, $type, $priority); 103 | 104 | return $this; 105 | } 106 | 107 | /** @inheritDoc */ 108 | public function addDelayedPass( 109 | string $className, 110 | array $constructArguments = [], 111 | string $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, 112 | int $priority = self::DEFAULT_PRIORITY, 113 | ): Builder { 114 | $this->config->addDelayedPass($className, $constructArguments, $type, $priority); 115 | 116 | return $this; 117 | } 118 | 119 | /** @inheritDoc */ 120 | public function addPackage(string $className, array $constructArguments = []): Builder 121 | { 122 | $this->config->addPackage($className, $constructArguments); 123 | 124 | return $this; 125 | } 126 | 127 | public function setProfileName(string $profileName): Builder 128 | { 129 | $this->config->setProfileName($profileName); 130 | 131 | return $this; 132 | } 133 | 134 | public function enableDebugging(): Builder 135 | { 136 | $this->parameterBag->set('app.devmode', true); 137 | 138 | return $this; 139 | } 140 | 141 | public function setDumpDir(string $dir): Builder 142 | { 143 | $this->config->setDumpDir($dir); 144 | 145 | return $this; 146 | } 147 | 148 | public function setParameter(string $name, mixed $value): Builder 149 | { 150 | $this->parameterBag->set($name, $value); 151 | 152 | return $this; 153 | } 154 | 155 | public function addPath(string $path): Builder 156 | { 157 | $this->config->addPath($path); 158 | 159 | return $this; 160 | } 161 | 162 | public function setBaseClass(string $class): Builder 163 | { 164 | $this->config->setBaseClass($class); 165 | 166 | return $this; 167 | } 168 | 169 | public function getContainer(): ContainerInterface 170 | { 171 | $devMode = $this->parameterBag->get('app.devmode'); 172 | assert(is_bool($devMode)); 173 | 174 | return $this->generator->generate( 175 | $this->config, 176 | new ConfigCache($this->config->getDumpFile(), $devMode), 177 | ); 178 | } 179 | 180 | public function getTestContainer(): ContainerInterface 181 | { 182 | $config = $this->config->withSubNamespace('Tests'); 183 | $config->addPass(new MakeServicesPublic(), PassConfig::TYPE_BEFORE_REMOVING); 184 | 185 | return $this->generator->generate( 186 | $config, 187 | new ConfigCache($config->getDumpFile(), true), 188 | ); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/FileListProvider.php: -------------------------------------------------------------------------------- 1 | */ 12 | public function getFiles(): DefaultGenerator; 13 | } 14 | -------------------------------------------------------------------------------- /src/Generator.php: -------------------------------------------------------------------------------- 1 | $builderClass */ 22 | public function __construct( 23 | private string $configurationFile, 24 | private string $builderClass = SymfonyBuilder::class, 25 | ) { 26 | $this->compiler = new Compiler(); 27 | } 28 | 29 | /** 30 | * Loads the container 31 | */ 32 | public function generate( 33 | ContainerConfiguration $config, 34 | ConfigCache $dump, 35 | ): ContainerInterface { 36 | $this->compiler->compile($config, $dump, $this); 37 | 38 | return $this->loadContainer($config, $dump); 39 | } 40 | 41 | private function loadContainer(ContainerConfiguration $config, ConfigCache $dump): ContainerInterface 42 | { 43 | require_once $dump->getPath(); 44 | $className = $config->getClassName(); 45 | assert(is_a($className, ContainerInterface::class, true)); 46 | 47 | return new $className(); 48 | } 49 | 50 | public function initializeContainer(ContainerConfiguration $config): SymfonyBuilder 51 | { 52 | $container = new $this->builderClass(); 53 | $container->addResource(new FileResource($this->configurationFile)); 54 | 55 | $loader = $this->getLoader($container, $config->getPaths(), $config->profileName()); 56 | 57 | foreach ($config->getFiles() as $file) { 58 | $loader->load($file); 59 | } 60 | 61 | return $container; 62 | } 63 | 64 | /** @param string[] $paths */ 65 | abstract public function getLoader( 66 | SymfonyBuilder $container, 67 | array $paths, 68 | ?string $profileName = null, 69 | ): LoaderInterface; 70 | } 71 | -------------------------------------------------------------------------------- /src/Generators/Delegating.php: -------------------------------------------------------------------------------- 1 | getDefinitions() as $definition) { 18 | $definition->setPublic(true); 19 | } 20 | 21 | foreach ($container->getAliases() as $alias) { 22 | $alias->setPublic(true); 23 | } 24 | } 25 | } 26 | --------------------------------------------------------------------------------