├── LICENSE ├── README.md ├── bin └── zen ├── composer.json └── src ├── AbstractCompiledContainer.php ├── Attribute └── Inject.php ├── Config ├── AbstractCompilerConfig.php ├── AbstractContainerConfig.php ├── ContainerConfigInterface.php ├── EntryPoint │ ├── AbstractEntryPoint.php │ ├── ClassEntryPoint.php │ ├── EntryPointInterface.php │ ├── Psr4NamespaceEntryPoint.php │ └── WildcardEntryPoint.php ├── FileBasedDefinition │ ├── FileBasedDefinitionConfig.php │ └── FileBasedDefinitionConfigInterface.php ├── Hint │ ├── AbstractHint.php │ ├── ContextDependentDefinitionHint.php │ ├── DefinitionHint.php │ ├── DefinitionHintInterface.php │ ├── Psr4WildcardHint.php │ ├── WildcardHint.php │ └── WildcardHintInterface.php └── Preload │ ├── AbstractPreload.php │ ├── ClassPreload.php │ ├── PreloadConfig.php │ ├── PreloadConfigInterface.php │ ├── PreloadInterface.php │ ├── Psr4NamespacePreload.php │ └── WildcardPreload.php ├── Container ├── Builder │ ├── ContainerBuilderInterface.php │ └── FileSystemContainerBuilder.php ├── ContainerCompiler.php ├── ContainerDependencyResolver.php ├── Definition │ ├── AbstractDefinition.php │ ├── ClassDefinition.php │ ├── ContextDependentDefinition.php │ ├── DefinitionInterface.php │ ├── ReferenceDefinition.php │ └── SelfDefinition.php ├── DefinitionCompilation.php ├── DefinitionInstantiation.php ├── PreloadCompiler.php └── PreloadDependencyResolver.php ├── Exception ├── ContainerException.php └── NotFoundException.php ├── RuntimeContainer.php └── Utils ├── FileSystemUtil.php └── NamespaceUtil.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Woohoo Labs. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Woohoo Labs. Zen 2 | 3 | [![Latest Version on Packagist][ico-version]][link-version] 4 | [![Software License][ico-license]](LICENSE) 5 | [![Build Status][ico-build]][link-build] 6 | [![Coverage Status][ico-coverage]][link-coverage] 7 | [![Total Downloads][ico-downloads]][link-downloads] 8 | [![Gitter][ico-support]][link-support] 9 | 10 | **Woohoo Labs. Zen is a very fast and easy-to-use, PSR-11 (former Container-Interop) compliant DI Container.** 11 | 12 | ## Table of Contents 13 | 14 | * [Introduction](#introduction) 15 | * [Install](#install) 16 | * [Basic Usage](#basic-usage) 17 | * [Advanced Usage](#advanced-usage) 18 | * [Examples](#examples) 19 | * [Versioning](#versioning) 20 | * [Change Log](#change-log) 21 | * [Testing](#testing) 22 | * [Contributing](#contributing) 23 | * [Support](#support) 24 | * [Credits](#credits) 25 | * [License](#license) 26 | 27 | ## Introduction 28 | 29 | ### Rationale 30 | 31 | Although Dependency Injection is one of the most fundamental principles of Object Oriented Programming, it doesn't 32 | get as much attention as it should. To make things even worse, there are quite some misbeliefs around the topic which 33 | can prevent people from applying the theory correctly. 34 | 35 | Besides using Service Location, the biggest misbelief certainly is that Dependency Injection requires very complex tools 36 | called DI Containers. And we all deem to know that their performance is ridiculously low. Woohoo Labs. Zen was born after 37 | the realization of the fact that these fallacies were true indeed back in 2016. 38 | 39 | That's why I tried to create a DI container which makes configuration as explicit and convenient as possible, 40 | which enforces the correct usage of Dependency Injection while providing 41 | [outstanding performance](https://rawgit.com/kocsismate/php-di-container-benchmarks/master/var/benchmark.html) according 42 | to [my benchmarks](https://github.com/kocsismate/php-di-container-benchmarks). That's how Zen was born. Although 43 | I imagined a very simple container initially, with only the essential feature set, over the time, Zen managed to feature 44 | the most important capabilities of the most popular DI Containers currently available. 45 | 46 | Fortunately, since the birth of Zen a lot of progress have been made in the DI Container ecosystem: many containers almost 47 | doubled their performance, autowiring and compilation became more popular, but one thing didn't change: Zen is still 48 | one of the fastest PHP containers out there. 49 | 50 | ### Features 51 | 52 | - [PSR-11](https://www.php-fig.org/psr/psr-11/) (former Container-Interop) compliance 53 | - Supports compilation for [maximum performance](https://rawgit.com/kocsismate/php-di-container-benchmarks/master/var/benchmark.html) 54 | - Supports constructor and property injection 55 | - Supports the notion of scopes (Singleton and Prototype) 56 | - Supports autowiring 57 | - Supports scalar and context-dependent injection 58 | - Supports dynamic usage for development 59 | - Supports generating a [preload file](https://wiki.php.net/rfc/preload) 60 | 61 | ## Install 62 | 63 | The only thing you need is [Composer](https://getcomposer.org) before getting started. Then run the command below to get 64 | the latest version: 65 | 66 | ```bash 67 | $ composer require woohoolabs/zen 68 | ``` 69 | 70 | > Note: The tests and examples won't be downloaded by default. You have to use `composer require woohoolabs/zen --prefer-source` 71 | or clone the repository if you need them. 72 | 73 | Zen 3 requires PHP 8.0 at least, but you may use 2.8.0 for PHP 7.4 and Zen 2.7.2 for PHP 7.1+. 74 | 75 | ## Basic Usage 76 | 77 | ### Using the container 78 | 79 | As Zen is a PSR-11 compliant container, it supports the `$container->has()` and 80 | `$container->get()` methods as defined by [`ContainerInterface`](https://www.php-fig.org/psr/psr-11/). 81 | 82 | ### Types of injection 83 | 84 | Only constructor and property injection of objects and scalar types are supported by Zen. 85 | 86 | In order to use constructor injection, you have to declare the type of the parameters or add a `@param` PHPDoc tag for them. If a 87 | parameter has a default value then this value will be injected. Here is an example of a constructor with valid parameters: 88 | 89 | ```php 90 | /** 91 | * @param B $b 92 | */ 93 | public function __construct(A $a, $b, $c = true) 94 | { 95 | // ... 96 | } 97 | ``` 98 | 99 | In order to use property injection, you have to annotate your properties with `#[Inject]` (mind case-sensitivity!), and 100 | provide their type via either a type declaration or a `@var` PHPDoc tag, as shown below: 101 | 102 | ```php 103 | #[Inject] 104 | /** @var A */ 105 | private $a; 106 | 107 | #[Inject] 108 | private B $b; 109 | ``` 110 | 111 | As a rule of thumb, you should only rely on constructor injection, because using test doubles in your unit tests 112 | instead of your real dependencies becomes much easier this way. Property injection can be acceptable for those classes 113 | that aren't unit tested. I prefer this type of injection in my controllers, but nowhere else. 114 | 115 | ### Building the container 116 | 117 | Zen is a compiled DI Container which means that every time you update a dependency of a class, you have to recompile 118 | the container in order for it to reflect the changes. This is a major weakness of compiled containers during development, 119 | but that's why Zen also offers a dynamic container implementation which is introduced [later](#dynamic-container). 120 | 121 | Compilation is possible by running the following command from your project's root directory: 122 | 123 | ```bash 124 | $ ./vendor/bin/zen build CONTAINER_PATH COMPILER_CONFIG_CLASS_NAME 125 | ``` 126 | 127 | > Please make sure you escape the `COMPILER_CONFIG_CLASS_NAME` argument when using namespaces, like below: 128 | 129 | ```bash 130 | ./vendor/bin/zen build /var/www/app/Container/Container.php "App\\Container\\CompilerConfig" 131 | ``` 132 | 133 | This results in a new file `CONTAINER_PATH` (e.g.: "/var/www/app/Container/Container.php") which can be directly 134 | instantiated (assuming autoloading is properly set up) in your project. No other configuration is needed during runtime 135 | by default. 136 | 137 | ```php 138 | $container = new Container(); 139 | ``` 140 | 141 | In case of very big projects, you might run out of memory when building the container. You can circumvent this issue by manually 142 | setting the memory limit: 143 | 144 | ```bash 145 | ./vendor/bin/zen --memory-limit="128M" build /var/www/app/Container/Container.php "App\\Container\\CompilerConfig" 146 | ``` 147 | 148 | Besides via the CLI, you can also build the Container via PHP itself: 149 | 150 | ```php 151 | $builder = new FileSystemContainerBuilder(new CompilerConfig(), "/var/www/src/Container/CompiledContainer.php"); 152 | $builder->build(); 153 | ``` 154 | 155 | > It's up to you where you generate the container but please be aware that file system speed can affect the time consumption 156 | of the compilation as well as the performance of your application. On the other hand, it's much more convenient to put 157 | the container in a place where it is easily reachable as you might occasionally need to debug it. 158 | 159 | ### Configuring the compiler 160 | 161 | What about the `COMPILER_CONFIG_CLASS_NAME` argument? This must be the fully qualified name of a class which extends 162 | `AbstractCompilerConfig`. Let's see an [example](https://github.com/woohoolabs/zen/blob/master/examples/CompilerConfig.php)! 163 | 164 | ```php 165 | class CompilerConfig extends AbstractCompilerConfig 166 | { 167 | public function getContainerNamespace(): string 168 | { 169 | return "App\\Container"; 170 | } 171 | 172 | public function getContainerClassName(): string 173 | { 174 | return "Container"; 175 | } 176 | 177 | public function useConstructorInjection(): bool 178 | { 179 | return true; 180 | } 181 | 182 | public function usePropertyInjection(): bool 183 | { 184 | return true; 185 | } 186 | 187 | public function getContainerConfigs(): array 188 | { 189 | return [ 190 | new ContainerConfig(), 191 | ]; 192 | } 193 | } 194 | ``` 195 | 196 | By providing the prior configuration to the build command, an `App\Container\Container` class will be 197 | generated and the compiler will resolve constructor dependencies via type hinting and PHPDoc comments as well as property 198 | dependencies marked by annotations. 199 | 200 | ### Configuring the container 201 | 202 | We only mentioned so far how to configure the compiler, but we haven't talked about container configuration. This can 203 | be done by returning an array of `AbstractContainerConfig` child instances in the `getContainerConfigs()` 204 | method of the compiler config. Let's see an [example]((https://github.com/woohoolabs/zen/blob/master/examples/ContainerConfig.php)) 205 | for the container configuration too: 206 | 207 | ```php 208 | class ContainerConfig extends AbstractContainerConfig 209 | { 210 | protected function getEntryPoints(): array 211 | { 212 | return [ 213 | // Define all classes in a PSR-4 namespace as Entry Points 214 | Psr4NamespaceEntryPoint::singleton('WoohooLabs\Zen\Examples\Controller'), 215 | 216 | // Define all classes in a directory as Entry Points 217 | WildcardEntryPoint::singleton(__DIR__ . "/Controller"), 218 | 219 | // Define a class as Entry Point 220 | ClassEntryPoint::singleton(UserController::class), 221 | ]; 222 | } 223 | 224 | protected function getDefinitionHints(): array 225 | { 226 | return [ 227 | // Bind the Container class to the ContainerInterface (Singleton scope by default) 228 | ContainerInterface::class => Container::class, 229 | 230 | // Bind the Request class to the RequestInterface (Prototype scope) 231 | RequestInterface::class => DefinitionHint::prototype(Request::class), 232 | 233 | // Bind the Response class to the ResponseInterface (Singleton scope) 234 | ResponseInterface::class => DefinitionHint::singleton(Response::class), 235 | ]; 236 | } 237 | 238 | protected function getWildcardHints(): array 239 | { 240 | return [ 241 | // Bind all classes in the specified PSR-4 namespaces to each other based on patterns 242 | new Psr4WildcardHint( 243 | 'WoohooLabs\Zen\Examples\Domain\*RepositoryInterface', 244 | 'WoohooLabs\Zen\Examples\Infrastructure\Mysql*Repository' 245 | ), 246 | 247 | // Bind all classes in the specified directories to each other based on patterns 248 | new WildcardHint( 249 | __DIR__ . "/Domain", 250 | 'WoohooLabs\Zen\Examples\Domain\*RepositoryInterface', 251 | 'WoohooLabs\Zen\Examples\Infrastructure\Mysql*Repository' 252 | ), 253 | ]; 254 | } 255 | } 256 | ``` 257 | 258 | Configuring the container consist of the following two things: defining your Entry Points (in the `getEntryPoints()` 259 | method) and passing Hints for the compiler (via the `getDefinitionHints()` and `getWildcardHints()` methods). 260 | 261 | ### Entry Points 262 | 263 | Entry Points are such classes that are to be directly retrieved from the DI Container (for instance Controllers and 264 | Middleware usually fall in this category). This means that you can __only__ fetch Entry Points from the Container with 265 | the `$container->get()` method and nothing else. 266 | 267 | Entry Points are not only special because of this, but also because their dependencies are automatically discovered during 268 | the compilation phase resulting in the full object graph (this feature is usually called as "autowiring"). 269 | 270 | The following example shows a configuration which instructs the compiler to recursively search for all classes in the 271 | `Controller` directory and discover all of their dependencies. Please note that only concrete classes are included by default, 272 | and detection is done recursively. 273 | 274 | ```php 275 | protected function getEntryPoints(): array 276 | { 277 | return [ 278 | new WildcardEntryPoint(__DIR__ . "/Controller"), 279 | ]; 280 | } 281 | ``` 282 | 283 | If you use PSR-4, there is a more convenient and performant way to define multiple Entry Points at once: 284 | 285 | ```php 286 | protected function getEntryPoints(): array 287 | { 288 | return [ 289 | new Psr4NamespaceEntryPoint('Src\Controller'), 290 | ]; 291 | } 292 | ``` 293 | 294 | This way, you can define all classes in a specific PSR-4 namespace as Entry Point. Please note that only concrete 295 | classes are included by default and detection is done recursively. 296 | 297 | Last but not least, you are able to define Entry Points individually too: 298 | 299 | ```php 300 | protected function getEntryPoints(): array 301 | { 302 | return [ 303 | new ClassEntryPoint(UserController::class), 304 | ]; 305 | } 306 | ``` 307 | 308 | ### Hints 309 | 310 | Hints tell the compiler how to properly resolve a dependency. This can be necessary when you depend on an 311 | interface or an abstract class because they are obviously not instantiatable. With hints, you are able to bind 312 | implementations to your interfaces or concretions to your abstract classes. The following example binds the 313 | `Container` class to `ContainerInterface` (in fact, you don't have to bind these two classes together, because this 314 | very configuration is automatically set during compilation). 315 | 316 | ```php 317 | protected function getDefinitionHints(): array 318 | { 319 | return [ 320 | ContainerInterface::class => Container::class, 321 | ]; 322 | } 323 | ``` 324 | 325 | Wildcard Hints can be used when you want to bind your classes in masses. Basically, they recursively search for all your 326 | classes in a directory specified by the first parameter, and bind those classes together which can be matched by the 327 | provided patterns. The following example 328 | 329 | ```php 330 | protected function getWildcardHints(): array 331 | { 332 | return [ 333 | new WildcardHint( 334 | __DIR__ . "/Domain", 335 | 'WoohooLabs\Zen\Examples\Domain\*RepositoryInterface', 336 | 'WoohooLabs\Zen\Examples\Infrastructure\Mysql*Repository' 337 | ), 338 | ]; 339 | } 340 | ``` 341 | 342 | will bind 343 | 344 | `UserRepositoryInterface` to `MysqlUserRepository`. 345 | 346 | If you use PSR-4, there is another - more convenient and performant - way to define the above settings: 347 | 348 | ```php 349 | protected function getWildcardHints(): array 350 | { 351 | return [ 352 | new Psr4WildcardHint( 353 | 'WoohooLabs\Zen\Examples\Domain\*RepositoryInterface', 354 | 'WoohooLabs\Zen\Examples\Infrastructure\Mysql*Repository' 355 | ), 356 | ]; 357 | } 358 | ``` 359 | 360 | This does exactly the same thing as what `WildcardHint` did. 361 | 362 | > Note that currently, namespace detection is not recursive; you are only able to use the wildcard character in the class name part, 363 | but not in the namespace (so `WoohooLabs\Zen\Examples\*\UserRepositoryInterface` is invalid); and only `*` supported as 364 | a wildcard character. 365 | 366 | ### Scopes 367 | 368 | Zen is able to control the lifetime of your container entries via the notion of scopes. By default, all entries retrieved 369 | from the container have `Singleton` scope, meaning that they are only instantiated at the first retrieval, and the same 370 | instance will be returned on the subsequent fetches. `Singleton` scope works well for stateless objects. 371 | 372 | On the other hand, container entries of `Prototype` scope are instantiated at every retrieval, so that is makes it 373 | possible to store stateful objects in the container. You can hint a container entry as `Prototype` with the 374 | `DefinitionHint::prototype()` construct as follows: 375 | 376 | ```php 377 | protected function getDefinitionHints(): array 378 | { 379 | return [ 380 | ContainerInterface::class => DefinitionHint::prototype(Container::class), 381 | ]; 382 | } 383 | ``` 384 | 385 | You can use `WildcardHint::prototype()` to hint your Wildcard Hints the same way too. 386 | 387 | ## Advanced Usage 388 | 389 | ### Scalar injection 390 | 391 | Scalar injection makes it possible to pass scalar values to an object in the form of constructor arguments or properties. 392 | As of v2.5, Zen supports scalar injection natively. You can use [Hints](#hints) for this purpose as you can see in the 393 | following example: 394 | 395 | ```php 396 | protected function getDefinitionHints(): array 397 | { 398 | return [ 399 | UserRepositoryInterface::class => DefinitionHint::singleton(MySqlUserRepository::class) 400 | ->setParameter("mysqlUser", "root") 401 | ->setParameter("mysqlPassword", "root"), 402 | ->setParameter("mysqlPort", 3306), 403 | ->setProperty("mysqlModes", ["ONLY_FULL_GROUP_BY", "STRICT_TRANS_TABLES", "NO_ZERO_IN_DATE"]), 404 | ]; 405 | } 406 | ``` 407 | 408 | Here, we instructed the DI Container to pass MySQL connection details as constructor arguments to the `MySqlUserRepository` 409 | class. Also, we initialized the `MySqlUserRepository::$mysqlModes` property with an array. 410 | 411 | Alternatively, you can use the following technique to simulate scalar injection: extend the class whose constructor parameters 412 | contain scalar types, and provide the arguments in question via `parent::__construct()` in the constructor of the child class. 413 | Finally, add the appropriate [Hint](#hints) to the container configuration so that the child class should be used instead of 414 | the parent class. 415 | 416 | ### Context-dependent dependency injection 417 | 418 | Sometimes - usually for bigger projects - it can be useful to be able to inject different implementations of the same 419 | interface as dependency. Before Zen 2.4.0, you couldn't achieve this unless you used some trick (like extending the 420 | original interface and configuring the container accordingly). Now, context-dependent injection is supported out of the 421 | box by Zen! 422 | 423 | Imagine the following case: 424 | 425 | ```php 426 | class NewRelicHandler implements LoggerInterface {} 427 | 428 | class PhpConsoleHandler implements LoggerInterface {} 429 | 430 | class MailHandler implements LoggerInterface {} 431 | 432 | class ServiceA 433 | { 434 | public function __construct(LoggerInterface $logger) {} 435 | } 436 | 437 | class ServiceB 438 | { 439 | public function __construct(LoggerInterface $logger) {} 440 | } 441 | 442 | class ServiceC 443 | { 444 | public function __construct(LoggerInterface $logger) {} 445 | } 446 | ``` 447 | 448 | If you would like to use `NewRelicHandler` in `ServiceA`, but `PhpConsoleHandler` in `ServiceB` and `MailHandler` in any 449 | other classes (like `ServiceC`) then you have to configure the [definition hints](#hints) this way: 450 | 451 | ```php 452 | protected function getDefinitionHints(): array 453 | { 454 | return [ 455 | LoggerInterface::class => ContextDependentDefinitionHint::create() 456 | ->setClassContext( 457 | NewRelicHandler::class, 458 | [ 459 | ServiceA::class, 460 | ] 461 | ), 462 | ->setClassContext( 463 | new DefinitionHint(PhpConsoleHandler::class), 464 | [ 465 | ServiceB::class, 466 | ] 467 | ) 468 | ->setDefaultClass(MailHandler::class), 469 | ]; 470 | } 471 | ``` 472 | 473 | The code above can be read the following way: when the classes listed in the second parameter of the `setClassContext()` methods 474 | depend on the class/interface in the key of the specified array item (`ServiceA` depends on `LoggerInterface` in the example), 475 | then the class/[definition hint](#hints) in the first parameter will be resolved by the container. If any other class depends 476 | on it, then the class/[definition hint](#hints) in the first parameter of the `setDefaultClass()` method will be resolved. 477 | 478 | > Note that if you don't set a default implementation (either via the `setDefaultClass()` method or via constructor parameter) 479 | then a `ContainerException` will be thrown if the interface is injected as a dependency of any class other than the listed 480 | ones in the second parameter of the `setClassContext()` method calls. 481 | 482 | ### Generating a preload file 483 | 484 | Preloading is a [feature](https://wiki.php.net/rfc/preload) introduced in PHP 7.4 for optimizing performance by compiling 485 | PHP files and loading them into shared memory when PHP starts up using a dedicated preload file. 486 | 487 | According to an [initial benchmark](https://github.com/composer/composer/issues/7777#issuecomment-440268416), the best speedup 488 | can be achieved by only preloading the "hot" files: those ones which are used the most often. Another gotcha is that in order 489 | for preload to work, every class dependency (parent classes, interfaces, traits, property types, parameter types and return types) 490 | of a preloaded file must also be preloaded. It means, someone has to resolve these dependencies. And that's something 491 | Zen can definitely do! 492 | 493 | If you want to create a preload file, first, configure your [Compiler Configuration](#configuring-the-compiler) by adding 494 | the following method: 495 | 496 | ```php 497 | public function getPreloadConfig(): PreloadConfigInterface 498 | { 499 | return PreloadConfig::create() 500 | ->setPreloadedClasses( 501 | [ 502 | Psr4NamespacePreload::create('WoohooLabs\Zen\Examples\Domain'), 503 | ClassPreload::create('WoohooLabs\Zen\Examples\Utils\AnimalUtil'), 504 | ] 505 | ) 506 | ->setPreloadedFiles( 507 | [ 508 | __DIR__ . "/examples/Utils/UserUtil.php", 509 | ] 510 | ); 511 | } 512 | ``` 513 | 514 | This configuration indicates that we want to preload the following: 515 | - All classes and all their dependencies in the `WoohooLabs\Zen\Examples\Domain` namespace 516 | - The `WoohooLabs\Zen\Examples\Utils\AnimalUtil` class and all its dependencies 517 | - The `examples/Utils/UserUtil.php` file (dependency resolution isn't performed in case of files) 518 | 519 | By default, the PHP files in the preload file will be referenced absolutely. However, if you provide a base path for the 520 | `PreoadConfig` (either via its constructor, or via the `PreoadConfig::setRelativeBasePath()` method), file references will 521 | become relative. 522 | 523 | In order to create the preload file, you have two possibilities: 524 | 525 | 1. Build the preload file along with the container: 526 | ```bash 527 | ./vendor/bin/zen --preload="/var/www/examples/preload.php" build /var/www/examples/Container.php "WoohooLabs\\Zen\\Examples\\CompilerConfig" 528 | ``` 529 | 530 | This way, first the container is created as `/var/www/examples/Container.php`, then the preload file as `/var/www/examples/preload.php`. 531 | 532 | 2. Build the preload file separately: 533 | ```bash 534 | ./vendor/bin/zen preload /var/www/examples/preload.php "WoohooLabs\\Zen\\Examples\\CompilerConfig" 535 | ``` 536 | 537 | This way, only the preload file is created as `/var/www/examples/Container.php`. 538 | 539 | ### File-based definitions 540 | 541 | This is another optimization which was [inspired by Symfony](https://github.com/symfony/symfony/pull/23678): if you have 542 | hundreds or even thousands of entries in the compiled container, then you may be better off separating the content 543 | of the container into different files. 544 | 545 | There are two ways of enabling this feature: 546 | 547 | - Globally: Configure your [Compiler Configuration](#configuring-the-compiler) by adding this method: 548 | ```php 549 | public function getFileBasedDefinitionConfig(): FileBasedDefinitionConfigInterface 550 | { 551 | return FileBasedDefinitionConfig::enableGlobally("Definitions"); 552 | } 553 | ``` 554 | 555 | This way, all definitions will be in separate files. Note that the first parameter in the example above is the directory 556 | where the definitions are generated, relative to the container itself. This directory is automatically deleted and created 557 | during compilation, so be cautious with it. 558 | 559 | - Selectively: You can choose which Entry Points are to be separated into different files. 560 | ```php 561 | protected function getEntryPoints(): array 562 | { 563 | return [ 564 | Psr4WildcardEntryPoint::create('Src\Controller') 565 | ->fileBased(), 566 | 567 | WildcardEntryPoint::create(__DIR__ . "/Controller") 568 | ->fileBased(), 569 | 570 | ClassEntryPoint::create(Class10::class) 571 | ->disableFileBased(), 572 | ]; 573 | } 574 | ``` 575 | 576 | ### Dynamic container 577 | 578 | You probably don't want to recompile the container all the time during development. That's where a dynamic container 579 | helps you: 580 | 581 | ```php 582 | $container = new RuntimeContainer(new CompilerConfig()); 583 | ``` 584 | 585 | > Note that the dynamic container is only suitable for development purposes because it is much slower than the 586 | compiled one - however it is still faster than some of the most well-known DI Containers. 587 | 588 | ## Examples 589 | 590 | If you want to see how Zen works, have a look at the [examples](https://github.com/woohoolabs/zen/tree/master/examples) 591 | folder, where you can find an example configuration (`CompilerConfig`). If `docker-compose` and `make` is available 592 | on your system, then just run the following commands in order to build a container: 593 | 594 | ```bash 595 | make composer-install # Install the Composer dependencies 596 | make build # Build the container into the examples/Container.php 597 | ``` 598 | 599 | ## Versioning 600 | 601 | This library follows [SemVer v2.0.0](https://semver.org/). 602 | 603 | ## Change Log 604 | 605 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 606 | 607 | ## Testing 608 | 609 | Woohoo Labs. Zen has a PHPUnit test suite. To run the tests, run the following command from the project folder: 610 | 611 | ``` bash 612 | $ phpunit 613 | ``` 614 | 615 | Additionally, you may run `docker-compose up` or `make test` in order to execute the tests. 616 | 617 | ## Contributing 618 | 619 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 620 | 621 | ## Support 622 | 623 | Please see [SUPPORT](SUPPORT.md) for details. 624 | 625 | ## Credits 626 | 627 | - [Máté Kocsis][link-author] 628 | - [All Contributors][link-contributors] 629 | 630 | ## License 631 | 632 | The MIT License (MIT). Please see the [License File](LICENSE) for more information. 633 | 634 | [ico-version]: https://img.shields.io/packagist/v/woohoolabs/zen.svg 635 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg 636 | [ico-build]: https://github.com/woohoolabs/zen/actions/workflows/continuous-integration.yml/badge.svg 637 | [ico-coverage]: https://img.shields.io/codecov/c/github/woohoolabs/zen 638 | [ico-downloads]: https://img.shields.io/packagist/dt/woohoolabs/zen.svg 639 | [ico-support]: https://badges.gitter.im/woohoolabs/zen.svg 640 | 641 | [link-version]: https://packagist.org/packages/woohoolabs/zen 642 | [link-build]: https://github.com/woohoolabs/zen/actions 643 | [link-coverage]: https://codecov.io/gh/woohoolabs/zen 644 | [link-downloads]: https://packagist.org/packages/woohoolabs/zen 645 | [link-author]: https://github.com/kocsismate 646 | [link-contributors]: https://github.com/woohoolabs/zen/graphs/contributors 647 | [link-support]: https://gitter.im/woohoolabs/zen?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge 648 | -------------------------------------------------------------------------------- /bin/zen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ")) { 8 | fwrite( 9 | STDERR, 10 | "Woohoo Labs. Zen requires PHP 7.4 or later." . PHP_EOL 11 | ); 12 | 13 | die(1); 14 | } 15 | 16 | require getAutoloadPath(); 17 | 18 | $optind = 0; 19 | $options = getopt("m::p::", ["memory-limit::", "preload::"], $optind); 20 | 21 | $memoryLimit = $options["memory-limit"] ?? ($options["m"] ?? ""); 22 | if ($memoryLimit !== "") { 23 | ini_set("memory_limit", $memoryLimit); 24 | } 25 | 26 | if (isset($argv[$optind]) === false) { 27 | die(1); 28 | } 29 | 30 | if ($argv[$optind] === "build") { 31 | $containerFilePath = $argv[$optind + 1]; 32 | $compilerConfig = new $argv[$optind + 2](); 33 | $preloadFilePath = $options["preload"] ?? ($options["p"] ?? ""); 34 | 35 | $builder = new FileSystemContainerBuilder($compilerConfig, $containerFilePath, $preloadFilePath); 36 | $builder->build(); 37 | } 38 | 39 | if ($argv[$optind] === "preload") { 40 | $preloadFilePath = $argv[$optind + 1]; 41 | $compilerConfig = new $argv[$optind + 2](); 42 | 43 | $builder = new FileSystemContainerBuilder($compilerConfig, "", $preloadFilePath); 44 | $builder->build(); 45 | } 46 | 47 | function getAutoloadPath(): string 48 | { 49 | $paths = [ 50 | __DIR__ . "/../../../autoload.php", 51 | __DIR__ . "/../../vendor/autoload.php", 52 | __DIR__ . "/../vendor/autoload.php", 53 | ]; 54 | 55 | foreach ($paths as $file) { 56 | if (file_exists($file)) { 57 | return $file; 58 | } 59 | } 60 | 61 | fwrite(STDERR, "You need Composer to set up the project dependencies!" . PHP_EOL); 62 | die(1); 63 | } 64 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "woohoolabs/zen", 3 | "description": "Woohoo Labs. Zen DI Container and preload file generator", 4 | "type": "library", 5 | "keywords": ["Woohoo Labs.", "Zen", "DI", "DIC", "Ioc", "Dependency Injection Container", "PSR-11", "Preload", "Preload Generator"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Máté Kocsis", 10 | "email": "kocsismate@woohoolabs.com" 11 | } 12 | ], 13 | "support": { 14 | "issues": "https://github.com/woohoolabs/zen/issues", 15 | "source": "https://github.com/woohoolabs/zen" 16 | }, 17 | "require": { 18 | "php": "^8.0.0", 19 | "psr/container": "^1.1 || ^2.0", 20 | "php-di/phpdoc-reader": "^2.2.0" 21 | }, 22 | "require-dev": { 23 | "phpstan/phpstan": "^1.10.0", 24 | "phpstan/phpstan-strict-rules": "^1.5.0", 25 | "phpunit/phpunit": "^9.5.0", 26 | "squizlabs/php_codesniffer": "^3.6.0", 27 | "woohoolabs/coding-standard": "^2.3.0", 28 | "woohoolabs/releaser": "^1.2.0" 29 | }, 30 | "provide": { 31 | "psr/container-implementation": "1.0.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "WoohooLabs\\Zen\\": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "WoohooLabs\\Zen\\Examples\\": "examples/", 41 | "WoohooLabs\\Zen\\Tests\\": "tests/" 42 | } 43 | }, 44 | "bin": [ 45 | "bin/zen" 46 | ], 47 | "scripts": { 48 | "test": "phpunit", 49 | "phpstan": "phpstan analyse --level max src", 50 | "phpcs": "phpcs", 51 | "phpcbf": "phpcbf" 52 | }, 53 | "config": { 54 | "sort-packages": true, 55 | "platform-check": false, 56 | "allow-plugins": { 57 | "dealerdirect/phpcodesniffer-composer-installer": true 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/AbstractCompiledContainer.php: -------------------------------------------------------------------------------- 1 | */ 13 | protected array $singletonEntries = []; 14 | 15 | /** @param array $properties */ 16 | protected function setClassProperties(object $object, array $properties): object 17 | { 18 | Closure::bind( 19 | static function () use ($object, $properties): void { 20 | foreach ($properties as $name => $value) { 21 | $object->$name = $value; 22 | } 23 | }, 24 | null, 25 | $object 26 | )->__invoke(); 27 | 28 | return $object; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Attribute/Inject.php: -------------------------------------------------------------------------------- 1 | containerConfigs = $this->getContainerConfigs(); 33 | $this->setEntryPointMap(); 34 | $this->setDefinitionHints(); 35 | } 36 | 37 | abstract public function getContainerNamespace(): string; 38 | 39 | abstract public function getContainerClassName(): string; 40 | 41 | abstract public function useConstructorInjection(): bool; 42 | 43 | abstract public function usePropertyInjection(): bool; 44 | 45 | public function getPreloadConfig(): PreloadConfigInterface 46 | { 47 | return PreloadConfig::create(); 48 | } 49 | 50 | public function getFileBasedDefinitionConfig(): FileBasedDefinitionConfigInterface 51 | { 52 | return FileBasedDefinitionConfig::disabledGlobally(); 53 | } 54 | 55 | /** 56 | * @internal 57 | * 58 | * @return AbstractContainerConfig[] 59 | */ 60 | abstract public function getContainerConfigs(): array; 61 | 62 | /** 63 | * @internal 64 | */ 65 | public function getContainerHash(): string 66 | { 67 | return str_replace("\\", "__", $this->getContainerFqcn()); 68 | } 69 | 70 | /** 71 | * @internal 72 | */ 73 | public function getContainerFqcn(): string 74 | { 75 | $namespace = $this->getContainerNamespace() !== "" ? $this->getContainerNamespace() . "\\" : ""; 76 | 77 | return $namespace . $this->getContainerClassName(); 78 | } 79 | 80 | /** 81 | * @internal 82 | * 83 | * @return EntryPointInterface[] 84 | */ 85 | public function getEntryPointMap(): array 86 | { 87 | return $this->entryPoints; 88 | } 89 | 90 | /** 91 | * @internal 92 | * 93 | * @return PreloadInterface[] 94 | */ 95 | public function getPreloadMap(): array 96 | { 97 | $preloads = []; 98 | 99 | foreach ($this->getPreloadConfig()->getPreloadedClasses() as $preload) { 100 | foreach ($preload->getClassNames() as $id) { 101 | $preloads[$id] = $preload; 102 | } 103 | } 104 | 105 | return $preloads; 106 | } 107 | 108 | /** 109 | * @internal 110 | * 111 | * @return DefinitionHintInterface[] 112 | */ 113 | public function getDefinitionHints(): array 114 | { 115 | return $this->definitionHints; 116 | } 117 | 118 | /** 119 | * @internal 120 | */ 121 | protected function setEntryPointMap(): void 122 | { 123 | $this->entryPoints = [ 124 | ContainerInterface::class => new ClassEntryPoint(ContainerInterface::class), 125 | $this->getContainerFqcn() => new ClassEntryPoint($this->getContainerFqcn()), 126 | ]; 127 | 128 | foreach ($this->containerConfigs as $containerConfig) { 129 | foreach ($containerConfig->createEntryPoints() as $entryPoint) { 130 | foreach ($entryPoint->getClassNames() as $id) { 131 | // TODO This condition is only for ensuring backwards compatibility. It should be removed in Zen 3.0. 132 | if (array_key_exists($id, $this->entryPoints) === false) { 133 | $this->entryPoints[$id] = $entryPoint; 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | protected function setDefinitionHints(): void 141 | { 142 | $definitionHints = []; 143 | foreach ($this->containerConfigs as $containerConfig) { 144 | $definitionHints[] = $containerConfig->createDefinitionHints(); 145 | } 146 | 147 | $this->definitionHints = array_merge([], ...$definitionHints); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Config/AbstractContainerConfig.php: -------------------------------------------------------------------------------- 1 | setEntryPoints(); 28 | $this->setDefinitionHints(); 29 | } 30 | 31 | /** 32 | * @return EntryPointInterface[]|string[] 33 | */ 34 | abstract protected function getEntryPoints(): array; 35 | 36 | /** 37 | * @return DefinitionHintInterface[]|string[] 38 | */ 39 | abstract protected function getDefinitionHints(): array; 40 | 41 | /** 42 | * @return WildcardHintInterface[] 43 | */ 44 | abstract protected function getWildcardHints(): array; 45 | 46 | /** 47 | * @internal 48 | * 49 | * @return EntryPointInterface[] 50 | */ 51 | public function createEntryPoints(): array 52 | { 53 | return $this->entryPoints; 54 | } 55 | 56 | /** 57 | * @internal 58 | * 59 | * @return DefinitionHintInterface[] 60 | */ 61 | public function createDefinitionHints(): array 62 | { 63 | return $this->definitionHints; 64 | } 65 | 66 | /** 67 | * @internal 68 | */ 69 | protected function setEntryPoints(): void 70 | { 71 | $this->entryPoints = array_map( 72 | static function ($entryPoint): EntryPointInterface { 73 | if ($entryPoint instanceof EntryPointInterface) { 74 | return $entryPoint; 75 | } 76 | 77 | if (is_string($entryPoint)) { 78 | return new ClassEntryPoint($entryPoint); 79 | } 80 | 81 | throw new ContainerException( 82 | "An entry point must be either a string or instance of the EntryPointInterface (e.g.: ClassEntryPoint)!" 83 | ); 84 | }, 85 | $this->getEntryPoints() 86 | ); 87 | } 88 | 89 | /** 90 | * @internal 91 | */ 92 | protected function setDefinitionHints(): void 93 | { 94 | $this->definitionHints = array_map( 95 | static function ($definitionHint): DefinitionHintInterface { 96 | if ($definitionHint instanceof DefinitionHintInterface) { 97 | return $definitionHint; 98 | } 99 | 100 | if (is_string($definitionHint)) { 101 | return new DefinitionHint($definitionHint); 102 | } 103 | 104 | throw new ContainerException("A definition hint must be either a string or a DefinitionHint object!"); 105 | }, 106 | $this->getDefinitionHints() 107 | ); 108 | 109 | $wildcardDefinitionHints = []; 110 | foreach ($this->getWildcardHints() as $wildcardHint) { 111 | $wildcardDefinitionHints[] = $wildcardHint->getDefinitionHints(); 112 | } 113 | 114 | $this->definitionHints = array_merge($this->definitionHints, ...$wildcardDefinitionHints); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Config/ContainerConfigInterface.php: -------------------------------------------------------------------------------- 1 | fileBased = true; 16 | 17 | return $this; 18 | } 19 | 20 | public function disableFileBased(): static 21 | { 22 | $this->fileBased = false; 23 | 24 | return $this; 25 | } 26 | 27 | /** 28 | * @internal 29 | */ 30 | public function isFileBased(FileBasedDefinitionConfigInterface $fileBasedDefinitionConfig): bool 31 | { 32 | return $this->fileBased ?? $fileBasedDefinitionConfig->isGlobalFileBasedDefinitionEnabled(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Config/EntryPoint/ClassEntryPoint.php: -------------------------------------------------------------------------------- 1 | className = $className; 19 | } 20 | 21 | /** 22 | * @internal 23 | * 24 | * @return string[] 25 | */ 26 | public function getClassNames(): array 27 | { 28 | return [ 29 | $this->className, 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Config/EntryPoint/EntryPointInterface.php: -------------------------------------------------------------------------------- 1 | namespace = trim($namespace, "\\"); 25 | $this->recursive = $recursive; 26 | $this->onlyInstantiable = $onlyInstantiable; 27 | } 28 | 29 | /** 30 | * @internal 31 | * 32 | * @return string[] 33 | */ 34 | public function getClassNames(): array 35 | { 36 | return NamespaceUtil::getClassesInPsr4Namespace($this->namespace, $this->recursive, $this->onlyInstantiable); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Config/EntryPoint/WildcardEntryPoint.php: -------------------------------------------------------------------------------- 1 | directoryName = rtrim($directoryName, "\\/"); 24 | $this->onlyConcreteClasses = $onlyConcreteClasses; 25 | } 26 | 27 | /** 28 | * @internal 29 | * 30 | * @return string[] 31 | */ 32 | public function getClassNames(): array 33 | { 34 | return FileSystemUtil::getClassesInPath($this->directoryName, $this->onlyConcreteClasses); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Config/FileBasedDefinition/FileBasedDefinitionConfig.php: -------------------------------------------------------------------------------- 1 | */ 14 | private array $excludedDefinitions; 15 | 16 | public static function disabledGlobally(string $relativeDefinitionDirectory = ""): FileBasedDefinitionConfig 17 | { 18 | return new FileBasedDefinitionConfig(false, $relativeDefinitionDirectory); 19 | } 20 | 21 | public static function enabledGlobally(string $relativeDefinitionDirectory): FileBasedDefinitionConfig 22 | { 23 | return new FileBasedDefinitionConfig(true, $relativeDefinitionDirectory); 24 | } 25 | 26 | public static function create( 27 | bool $isGlobalFileBasedDefinitionsEnabled, 28 | string $relativeDefinitionDirectory = "" 29 | ): FileBasedDefinitionConfig { 30 | return new FileBasedDefinitionConfig($isGlobalFileBasedDefinitionsEnabled, $relativeDefinitionDirectory); 31 | } 32 | 33 | public function __construct(bool $isGlobalFileBasedDefinitionsEnabled, string $relativeDefinitionDirectory = "") 34 | { 35 | $this->isGlobalFileBasedDefinitionsEnabled = $isGlobalFileBasedDefinitionsEnabled; 36 | $this->excludedDefinitions = []; 37 | $this->setRelativeDefinitionDirectory($relativeDefinitionDirectory); 38 | } 39 | 40 | public function isGlobalFileBasedDefinitionEnabled(): bool 41 | { 42 | return $this->isGlobalFileBasedDefinitionsEnabled; 43 | } 44 | 45 | public function setRelativeDefinitionDirectory(string $relativeDefinitionDirectory): FileBasedDefinitionConfig 46 | { 47 | $this->relativeDefinitionDirectory = trim($relativeDefinitionDirectory, "\\/"); 48 | 49 | return $this; 50 | } 51 | 52 | public function getRelativeDefinitionDirectory(): string 53 | { 54 | return $this->relativeDefinitionDirectory; 55 | } 56 | 57 | /** 58 | * @param array $excludedDefinitions 59 | */ 60 | public function setExcludedDefinitions(array $excludedDefinitions): FileBasedDefinitionConfig 61 | { 62 | $this->excludedDefinitions = $excludedDefinitions; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * @return array 69 | */ 70 | public function getExcludedDefinitions(): array 71 | { 72 | return $this->excludedDefinitions; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Config/FileBasedDefinition/FileBasedDefinitionConfigInterface.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function getExcludedDefinitions(): array; 17 | } 18 | -------------------------------------------------------------------------------- /src/Config/Hint/AbstractHint.php: -------------------------------------------------------------------------------- 1 | singleton = $scope === "singleton"; 14 | } 15 | 16 | public function isSingleton(): bool 17 | { 18 | return $this->singleton; 19 | } 20 | 21 | public function setSingletonScope(): static 22 | { 23 | $this->singleton = true; 24 | 25 | return $this; 26 | } 27 | 28 | public function setPrototypeScope(): static 29 | { 30 | $this->singleton = false; 31 | 32 | return $this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Config/Hint/ContextDependentDefinitionHint.php: -------------------------------------------------------------------------------- 1 | defaultDefinitionHint = $this->createDefinitionHint($defaultDefinitionHint); 30 | } 31 | 32 | public function setDefaultClass(DefinitionHint|string $defaultDefinitionHint): ContextDependentDefinitionHint 33 | { 34 | $this->defaultDefinitionHint = $this->createDefinitionHint($defaultDefinitionHint); 35 | 36 | return $this; 37 | } 38 | 39 | /** 40 | * @param string[] $parentClasses 41 | */ 42 | public function setClassContext(DefinitionHint|string $definitionHint, array $parentClasses): ContextDependentDefinitionHint 43 | { 44 | $definitionHint = $this->createDefinitionHint($definitionHint); 45 | 46 | if ($definitionHint === null) { 47 | return $this; 48 | } 49 | 50 | foreach ($parentClasses as $parent) { 51 | $this->definitionHints[$parent] = $definitionHint; 52 | } 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * @internal 59 | * 60 | * @param EntryPointInterface[] $entryPoints 61 | * @param DefinitionHintInterface[] $definitionHints 62 | * @return DefinitionInterface[] 63 | */ 64 | public function toDefinitions(array $entryPoints, array $definitionHints, string $id, bool $isFileBased): array 65 | { 66 | $isEntryPoint = array_key_exists($id, $entryPoints); 67 | 68 | $defaultDefinition = null; 69 | if ($this->defaultDefinitionHint !== null) { 70 | $defaultDefinition = new ClassDefinition( 71 | $this->defaultDefinitionHint->getClassName(), 72 | $this->defaultDefinitionHint->isSingleton(), 73 | $isEntryPoint, 74 | $isFileBased 75 | ); 76 | } 77 | 78 | $definitions = []; 79 | foreach ($this->definitionHints as $parentId => $definitionHint) { 80 | $definitions[$parentId] = new ClassDefinition( 81 | $definitionHint->getClassName(), 82 | $definitionHint->isSingleton(), 83 | $isEntryPoint, 84 | $isFileBased 85 | ); 86 | } 87 | 88 | $result = [ 89 | $id => new ContextDependentDefinition($id, $defaultDefinition, $definitions), 90 | ]; 91 | 92 | if ($defaultDefinition !== null) { 93 | $result[$defaultDefinition->getClassName()] = $defaultDefinition; 94 | } 95 | 96 | $definitionHintDefinitions = []; 97 | foreach ($this->definitionHints as $definitionHint) { 98 | $definitionHintDefinitions[] = $definitionHint->toDefinitions( 99 | $entryPoints, 100 | $definitionHints, 101 | $definitionHint->getClassName(), 102 | $isFileBased 103 | ); 104 | } 105 | 106 | return array_merge($result, ...$definitionHintDefinitions); 107 | } 108 | 109 | private function createDefinitionHint(DefinitionHint|string|null $definitionHint): ?DefinitionHint 110 | { 111 | return is_string($definitionHint) ? new DefinitionHint($definitionHint) : $definitionHint; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Config/Hint/DefinitionHint.php: -------------------------------------------------------------------------------- 1 | |null> */ 22 | private array $parameters; 23 | /** @var array|null> */ 24 | private array $properties; 25 | 26 | public static function singleton(string $className): DefinitionHint 27 | { 28 | return new self($className, "singleton"); 29 | } 30 | 31 | public static function prototype(string $className): DefinitionHint 32 | { 33 | return new self($className, "prototype"); 34 | } 35 | 36 | public function __construct(string $className, string $scope = "singleton") 37 | { 38 | parent::__construct($scope); 39 | $this->className = $className; 40 | $this->parameters = []; 41 | $this->properties = []; 42 | } 43 | 44 | /** 45 | * @param string|int|float|bool|array|null $value 46 | */ 47 | public function setParameter(string $name, $value): DefinitionHint 48 | { 49 | if (is_scalar($value) === false && is_array($value) === false) { 50 | throw new ContainerException("Constructor parameter $name of $this->className must be a scalar or an array!"); 51 | } 52 | 53 | $this->parameters[$name] = $value; 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * @param string|int|float|bool|array|null $value 60 | */ 61 | public function setProperty(string $name, $value): DefinitionHint 62 | { 63 | if (is_scalar($value) === false && is_array($value) === false) { 64 | throw new ContainerException("Property '$this->className::\$$name' must be a scalar or an array!"); 65 | } 66 | 67 | $this->properties[$name] = $value; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * @internal 74 | * 75 | * @param EntryPointInterface[] $entryPoints 76 | * @param DefinitionHintInterface[] $definitionHints 77 | * @return DefinitionInterface[] 78 | */ 79 | public function toDefinitions(array $entryPoints, array $definitionHints, string $id, bool $isFileBased): array 80 | { 81 | $isEntryPoint = array_key_exists($id, $entryPoints); 82 | 83 | if ($this->className === $id) { 84 | return [ 85 | $id => new ClassDefinition( 86 | $this->className, 87 | $this->singleton, 88 | $isEntryPoint, 89 | $isFileBased, 90 | $this->parameters, 91 | $this->properties 92 | ), 93 | ]; 94 | } 95 | 96 | $result = [ 97 | $id => new ReferenceDefinition($id, $this->className, $this->singleton, $isEntryPoint, $isFileBased), 98 | ]; 99 | 100 | if (array_key_exists($this->className, $definitionHints)) { 101 | $definitions = $definitionHints[$this->className]->toDefinitions( 102 | $entryPoints, 103 | $definitionHints, 104 | $this->className, 105 | $isFileBased 106 | ); 107 | 108 | foreach ($definitions as $definition) { 109 | $definition->increaseReferenceCount($id, $this->singleton); 110 | } 111 | 112 | return array_merge($result, $definitions); 113 | } 114 | 115 | $classDefinition = new ClassDefinition( 116 | $this->className, 117 | $this->singleton, 118 | array_key_exists($this->className, $entryPoints), 119 | $isFileBased, 120 | $this->parameters, 121 | $this->properties 122 | ); 123 | $result[$this->className] = $classDefinition->increaseReferenceCount($id, $this->singleton); 124 | 125 | return $result; 126 | } 127 | 128 | /** 129 | * @internal 130 | */ 131 | public function getClassName(): string 132 | { 133 | return $this->className; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Config/Hint/DefinitionHintInterface.php: -------------------------------------------------------------------------------- 1 | sourcePattern = $sourcePattern; 38 | $this->targetPattern = $targetPattern; 39 | } 40 | 41 | /** 42 | * @internal 43 | * 44 | * @return DefinitionHintInterface[] 45 | */ 46 | public function getDefinitionHints(): array 47 | { 48 | $sourceNamespace = $this->getNamespace($this->sourcePattern); 49 | $targetNamespace = $this->getNamespace($this->targetPattern); 50 | 51 | $this->validateNamespace($this->sourcePattern, $sourceNamespace); 52 | $this->validateNamespace($this->targetPattern, $targetNamespace); 53 | 54 | $sourceRegex = "/" . str_replace([".", "\\", "*"], ["\\.", "\\\\", "(.*)"], $this->sourcePattern) . "/"; 55 | 56 | $definitionHints = []; 57 | foreach (NamespaceUtil::getClassesInPsr4Namespace($sourceNamespace, false, false) as $sourceClass) { 58 | $matches = []; 59 | preg_match_all($sourceRegex, $sourceClass, $matches); 60 | 61 | $targetClass = $this->targetPattern; 62 | foreach ($matches[1] as $match) { 63 | $result = preg_replace("/\*/", $match, $targetClass, 1); 64 | 65 | if ($result !== null) { 66 | $targetClass = $result; 67 | } 68 | } 69 | 70 | if (class_exists($targetClass) || interface_exists($targetClass)) { 71 | $definitionHints[$sourceClass] = new DefinitionHint($targetClass, $this->singleton ? "singleton" : "prototype"); 72 | } 73 | } 74 | 75 | return $definitionHints; 76 | } 77 | 78 | private function getNamespace(string $pattern): string 79 | { 80 | $namespaceLength = strrpos($pattern, "\\"); 81 | 82 | return $namespaceLength === false ? "" : substr($pattern, 0, $namespaceLength); 83 | } 84 | 85 | private function validateNamespace(string $pattern, string $namespace): void 86 | { 87 | if (strpos($namespace, "*") !== false) { 88 | throw new ContainerException("'$pattern' is an invalid pattern: the namespace part can't contain the asterisk character (*)!"); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Config/Hint/WildcardHint.php: -------------------------------------------------------------------------------- 1 | sourcePath = $sourcePath; 34 | $this->sourcePattern = $sourcePattern; 35 | $this->targetPattern = $targetPattern; 36 | } 37 | 38 | /** 39 | * @internal 40 | * 41 | * @return DefinitionHintInterface[] 42 | */ 43 | public function getDefinitionHints(): array 44 | { 45 | $sourceRegex = "/" . str_replace([".", "\\", "*"], ["\\.", "\\\\", "(.*)"], $this->sourcePattern) . "/"; 46 | 47 | $definitionHints = []; 48 | foreach (FileSystemUtil::getClassesInPath($this->sourcePath, false) as $sourceClass) { 49 | $matches = []; 50 | preg_match_all($sourceRegex, $sourceClass, $matches); 51 | 52 | $targetClass = $this->targetPattern; 53 | foreach ($matches[1] as $match) { 54 | $result = preg_replace("/\*/", $match, $targetClass, 1); 55 | 56 | if ($result !== null) { 57 | $targetClass = $result; 58 | } 59 | } 60 | 61 | if (class_exists($targetClass) === false) { 62 | continue; 63 | } 64 | 65 | $definitionHints[$sourceClass] = new DefinitionHint($targetClass, $this->singleton ? "singleton" : "prototype"); 66 | } 67 | 68 | return $definitionHints; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Config/Hint/WildcardHintInterface.php: -------------------------------------------------------------------------------- 1 | className = $className; 19 | } 20 | 21 | /** 22 | * @internal 23 | * 24 | * @return string[] 25 | */ 26 | public function getClassNames(): array 27 | { 28 | return [ 29 | $this->className, 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Config/Preload/PreloadConfig.php: -------------------------------------------------------------------------------- 1 | setRelativeBasePath($relativeBasePath); 25 | $this->preloadedClasses = []; 26 | $this->preloadedFiles = []; 27 | } 28 | 29 | public function getRelativeBasePath(): string 30 | { 31 | return $this->relativeBasePath; 32 | } 33 | 34 | public function setRelativeBasePath(string $relativeBasePath): PreloadConfig 35 | { 36 | $this->relativeBasePath = rtrim($relativeBasePath, "\\/"); 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * @return PreloadInterface[] 43 | */ 44 | public function getPreloadedClasses(): array 45 | { 46 | return $this->preloadedClasses; 47 | } 48 | 49 | /** 50 | * @param PreloadInterface[] $preloadedClasses 51 | */ 52 | public function setPreloadedClasses(array $preloadedClasses): PreloadConfig 53 | { 54 | $this->preloadedClasses = $preloadedClasses; 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * @param string[] $preloadedFiles 61 | */ 62 | public function setPreloadedFiles(array $preloadedFiles): PreloadConfig 63 | { 64 | $this->preloadedFiles = $preloadedFiles; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * @return string[] 71 | */ 72 | public function getPreloadedFiles(): array 73 | { 74 | return $this->preloadedFiles; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Config/Preload/PreloadConfigInterface.php: -------------------------------------------------------------------------------- 1 | namespace = trim($namespace, "\\"); 25 | $this->recursive = $recursive; 26 | $this->onlyInstantiable = $onlyInstantiable; 27 | } 28 | 29 | /** 30 | * @internal 31 | * 32 | * @return string[] 33 | */ 34 | public function getClassNames(): array 35 | { 36 | return NamespaceUtil::getClassesInPsr4Namespace($this->namespace, $this->recursive, $this->onlyInstantiable); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Config/Preload/WildcardPreload.php: -------------------------------------------------------------------------------- 1 | directoryName = rtrim($directoryName, "\\/"); 24 | $this->onlyConcreteClasses = $onlyConcreteClasses; 25 | } 26 | 27 | /** 28 | * @internal 29 | * 30 | * @return string[] 31 | */ 32 | public function getClassNames(): array 33 | { 34 | return FileSystemUtil::getClassesInPath($this->directoryName, $this->onlyConcreteClasses); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Container/Builder/ContainerBuilderInterface.php: -------------------------------------------------------------------------------- 1 | containerPath = $containerPath; 36 | $this->compilerConfig = $compilerConfig; 37 | $this->preloadFilePath = $preloadFilePath; 38 | } 39 | 40 | public function build(): void 41 | { 42 | $preloadedClasses = []; 43 | if ($this->preloadFilePath !== "") { 44 | $preloadedClasses = $this->buildPreloadFile(); 45 | } 46 | 47 | if ($this->containerPath !== "") { 48 | $this->buildContainer($preloadedClasses); 49 | } 50 | } 51 | 52 | /** 53 | * @return string[] 54 | */ 55 | public function buildPreloadFile(): array 56 | { 57 | $dependencyResolver = new PreloadDependencyResolver($this->compilerConfig); 58 | $classes = $dependencyResolver->resolvePreloads(); 59 | $compiler = new PreloadCompiler(); 60 | 61 | $compiledPreloadFile = $compiler->compile($this->compilerConfig, $classes); 62 | 63 | $result = file_put_contents($this->preloadFilePath, $compiledPreloadFile); 64 | if ($result === false) { 65 | throw new ContainerException("File '$this->preloadFilePath' cannot be written"); 66 | } 67 | 68 | return $classes; 69 | } 70 | 71 | /** 72 | * @param string[] $preloadedClasses 73 | */ 74 | public function buildContainer(array $preloadedClasses): void 75 | { 76 | $dependencyResolver = new ContainerDependencyResolver($this->compilerConfig); 77 | $compiler = new ContainerCompiler(); 78 | 79 | $compiledContainerFiles = $compiler->compile($this->compilerConfig, $dependencyResolver->resolveEntryPoints(), $preloadedClasses); 80 | 81 | if ($compiledContainerFiles["definitions"] !== []) { 82 | $definitionDirectory = $this->getDefinitionDirectory(); 83 | $this->deleteDirectory($definitionDirectory); 84 | $this->createDirectory($definitionDirectory); 85 | 86 | foreach ($compiledContainerFiles["definitions"] as $filename => $content) { 87 | $file = $definitionDirectory . DIRECTORY_SEPARATOR . $filename; 88 | $result = file_put_contents($file, $content); 89 | if ($result === false) { 90 | throw new ContainerException("File '$$file' cannot be written"); 91 | } 92 | } 93 | } 94 | 95 | $result = file_put_contents($this->containerPath, $compiledContainerFiles["container"]); 96 | if ($result === false) { 97 | throw new ContainerException("File '$this->containerPath' cannot be written"); 98 | } 99 | } 100 | 101 | private function deleteDirectory(string $directory): void 102 | { 103 | if (file_exists($directory) === false) { 104 | return; 105 | } 106 | 107 | $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS); 108 | $files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST); 109 | 110 | foreach ($files as $file) { 111 | assert($file instanceof SplFileInfo); 112 | if ($file->isDir()) { 113 | $result = rmdir($file->getRealPath()); 114 | if ($result === false) { 115 | throw new ContainerException("Directory '" . $file->getRealPath() . "' cannot be deleted"); 116 | } 117 | } else { 118 | $result = unlink($file->getRealPath()); 119 | if ($result === false) { 120 | throw new ContainerException("File '" . $file->getRealPath() . "' cannot be deleted"); 121 | } 122 | } 123 | } 124 | 125 | rmdir($directory); 126 | } 127 | 128 | /** 129 | * @throws ContainerException 130 | */ 131 | private function createDirectory(string $directory): void 132 | { 133 | if (file_exists($directory)) { 134 | return; 135 | } 136 | 137 | $result = mkdir($directory); 138 | if ($result === false) { 139 | throw new ContainerException("Directory '$directory' cannot be created"); 140 | } 141 | } 142 | 143 | private function getDefinitionDirectory(): string 144 | { 145 | $basePath = dirname($this->containerPath); 146 | $relativeDirectory = $this->compilerConfig->getFileBasedDefinitionConfig()->getRelativeDefinitionDirectory(); 147 | 148 | if ($relativeDirectory === "") { 149 | throw new ContainerException("Relative directory of file-based definitions cannot be empty"); 150 | } 151 | 152 | return $basePath . DIRECTORY_SEPARATOR . $relativeDirectory; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Container/ContainerCompiler.php: -------------------------------------------------------------------------------- 1 | } 21 | */ 22 | public function compile(AbstractCompilerConfig $compilerConfig, array $definitions, array $preloadedClasses): array 23 | { 24 | $fileBasedDefinitionConfig = $compilerConfig->getFileBasedDefinitionConfig(); 25 | $fileBasedDefinitionDirectory = $fileBasedDefinitionConfig->getRelativeDefinitionDirectory(); 26 | $definitionCompilation = new DefinitionCompilation($fileBasedDefinitionConfig, $definitions); 27 | 28 | $definitionFiles = []; 29 | 30 | $container = "getContainerNamespace() !== "") { 34 | $container .= "\nnamespace " . $compilerConfig->getContainerNamespace() . ";\n"; 35 | } 36 | $container .= "\nuse WoohooLabs\\Zen\\AbstractCompiledContainer;\n"; 37 | $container .= "use WoohooLabs\\Zen\\Exception\\NotFoundException;\n\n"; 38 | $container .= "class " . $compilerConfig->getContainerClassName() . " extends AbstractCompiledContainer\n"; 39 | $container .= "{\n"; 40 | 41 | $entryPointIds = array_keys($compilerConfig->getEntryPointMap()); 42 | 43 | // ContainerInterface::has() 44 | 45 | $container .= " /**\n"; 46 | $container .= " * @param string \$id\n"; 47 | $container .= " */\n"; 48 | $container .= " public function has(\$id): bool\n"; 49 | $container .= " {\n"; 50 | $container .= " return match (\$id) {\n"; 51 | 52 | $entryPointCount = count($entryPointIds); 53 | foreach ($entryPointIds as $i => $id) { 54 | if (array_key_exists($id, $definitions) === false) { 55 | continue; 56 | } 57 | 58 | $container .= " '$id'" . ($i === $entryPointCount - 1 ? " => true" : "") . ",\n"; 59 | } 60 | $container .= " default => false,\n"; 61 | $container .= " };\n"; 62 | $container .= " }\n\n"; 63 | 64 | // ContainerInterface::get() 65 | 66 | $container .= " /**\n"; 67 | $container .= " * @param string \$id\n"; 68 | $container .= " * @throws NotFoundException\n"; 69 | $container .= " */\n"; 70 | $container .= " public function get(\$id): mixed\n"; 71 | $container .= " {\n"; 72 | $container .= " return \$this->singletonEntries[\$id] ?? match (\$id) {\n"; 73 | 74 | foreach ($entryPointIds as $id) { 75 | if (array_key_exists($id, $definitions) === false) { 76 | continue; 77 | } 78 | 79 | $definition = $definitions[$id]; 80 | 81 | if ($definition->isDefinitionInlinable("")) { 82 | if ($definition->isFileBased("")) { 83 | $filename = $this->getHash($id) . ".php"; 84 | $container .= " '$id' => require __DIR__ . '/$fileBasedDefinitionDirectory/$filename',\n"; 85 | } else { 86 | $container .= " '$id' => " . $definition->compile( 87 | $definitionCompilation, 88 | "", 89 | 3, 90 | true, 91 | $preloadedClasses 92 | ); 93 | $container .= ",\n"; 94 | } 95 | } else { 96 | $methodName = $this->getHash($id); 97 | $container .= " '$id' => \$this->$methodName(),\n"; 98 | } 99 | } 100 | $container .= " default => throw new NotFoundException(\$id),\n"; 101 | $container .= " };\n"; 102 | $container .= " }\n"; 103 | 104 | // Entry Points 105 | foreach ($entryPointIds as $id) { 106 | if (array_key_exists($id, $definitions) === false) { 107 | continue; 108 | } 109 | 110 | $definition = $definitions[$id]; 111 | $filename = $this->getHash($id) . ".php"; 112 | 113 | if ($definition->isFileBased()) { 114 | $definitionFiles[$filename] = "compile($definitionCompilation, "", 0, false, $preloadedClasses); 116 | } 117 | 118 | if ($definition->isDefinitionInlinable("")) { 119 | continue; 120 | } 121 | 122 | $container .= "\n public function " . $this->getHash($id) . "()\n {\n"; 123 | if ($definition->isFileBased()) { 124 | $container .= " return require __DIR__ . '/$fileBasedDefinitionDirectory/$filename';\n"; 125 | } else { 126 | $container .= $definition->compile($definitionCompilation, "", 2, false, $preloadedClasses); 127 | } 128 | $container .= " }\n"; 129 | } 130 | 131 | $container .= "}\n"; 132 | 133 | return [ 134 | "container" => $container, 135 | "definitions" => $definitionFiles, 136 | ]; 137 | } 138 | 139 | private function getHash(string $id): string 140 | { 141 | return str_replace("\\", "__", $id); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Container/ContainerDependencyResolver.php: -------------------------------------------------------------------------------- 1 | typeHintReader = new PhpDocReader(); 48 | 49 | $this->compilerConfig = $compilerConfig; 50 | $this->useConstructorInjection = $compilerConfig->useConstructorInjection(); 51 | $this->usePropertyInjection = $compilerConfig->usePropertyInjection(); 52 | $this->entryPoints = $compilerConfig->getEntryPointMap(); 53 | $this->definitionHints = $compilerConfig->getDefinitionHints(); 54 | 55 | $this->fileBasedDefinitionConfig = $compilerConfig->getFileBasedDefinitionConfig(); 56 | $this->excludedFileBasedDefinitions = array_flip($this->fileBasedDefinitionConfig->getExcludedDefinitions()); 57 | } 58 | 59 | /** 60 | * @return DefinitionInterface[] 61 | */ 62 | public function resolveEntryPoints(): array 63 | { 64 | $this->resetDefinitions(); 65 | 66 | foreach ($this->entryPoints as $id => $entryPoint) { 67 | $this->resolve($id, "", $entryPoint, false); 68 | } 69 | 70 | return $this->definitions; 71 | } 72 | 73 | /** 74 | * @return DefinitionInterface[] 75 | * @throws NotFoundException 76 | */ 77 | public function resolveEntryPoint(string $id): array 78 | { 79 | $this->resetDefinitions(); 80 | 81 | if (array_key_exists($id, $this->entryPoints) === false) { 82 | throw new NotFoundException($id); 83 | } 84 | 85 | $this->resolve($id, "", $this->entryPoints[$id], true); 86 | 87 | return $this->definitions; 88 | } 89 | 90 | private function resolve(string $id, string $parentId, EntryPointInterface $parentEntryPoint, bool $runtime): void 91 | { 92 | if (array_key_exists($id, $this->definitions)) { 93 | if ($this->definitions[$id]->needsDependencyResolution()) { 94 | $this->resolveDependencies($id, $parentId, $parentEntryPoint, $runtime); 95 | } 96 | 97 | return; 98 | } 99 | 100 | $isFileBased = $runtime ? false : $this->isFileBased($id, $parentEntryPoint); 101 | 102 | if (array_key_exists($id, $this->definitionHints)) { 103 | $definitions = $this->definitionHints[$id]->toDefinitions( 104 | $this->entryPoints, 105 | $this->definitionHints, 106 | $id, 107 | $isFileBased 108 | ); 109 | 110 | foreach ($definitions as $definitionId => $definition) { 111 | /** @var DefinitionInterface $definition */ 112 | if (array_key_exists($definitionId, $this->definitions) === false) { 113 | $this->definitions[$definitionId] = $definition; 114 | $this->resolve($definitionId, $parentId, $parentEntryPoint, $runtime); 115 | } 116 | } 117 | 118 | return; 119 | } 120 | 121 | $this->definitions[$id] = new ClassDefinition($id, true, array_key_exists($id, $this->entryPoints), $isFileBased); 122 | $this->resolveDependencies($id, $parentId, $parentEntryPoint, $runtime); 123 | } 124 | 125 | /** 126 | * @throws ContainerException 127 | */ 128 | private function resolveDependencies(string $id, string $parentId, EntryPointInterface $parentEntryPoint, bool $runtime): void 129 | { 130 | $definition = $this->definitions[$id]; 131 | 132 | $definition->resolveDependencies(); 133 | 134 | if ($definition instanceof ClassDefinition === false) { 135 | return; 136 | } 137 | 138 | if ($this->useConstructorInjection) { 139 | $this->resolveConstructorArguments($id, $parentId, $definition, $parentEntryPoint, $runtime); 140 | } 141 | 142 | if ($this->usePropertyInjection) { 143 | $this->resolveProperties($id, $parentId, $definition, $parentEntryPoint, $runtime); 144 | } 145 | } 146 | 147 | /** 148 | * @throws ContainerException 149 | */ 150 | private function resolveConstructorArguments( 151 | string $id, 152 | string $parentId, 153 | ClassDefinition $definition, 154 | EntryPointInterface $parentEntryPoint, 155 | bool $runtime 156 | ): void { 157 | try { 158 | $reflectionClass = new ReflectionClass($id); 159 | } catch (ReflectionException $e) { 160 | throw new ContainerException("Cannot inject class: " . $id); 161 | } 162 | 163 | $constructor = $reflectionClass->getConstructor(); 164 | if ($constructor === null) { 165 | return; 166 | } 167 | 168 | $paramNames = []; 169 | foreach ($constructor->getParameters() as $parameter) { 170 | $paramName = $parameter->getName(); 171 | $paramNames[] = $paramName; 172 | 173 | if ($definition->isConstructorParameterOverridden($paramName)) { 174 | $definition->addConstructorArgumentFromOverride($paramName); 175 | continue; 176 | } 177 | 178 | if ($parameter->isOptional()) { 179 | $definition->addConstructorArgumentFromValue($parameter->getDefaultValue()); 180 | continue; 181 | } 182 | 183 | $parameterClass = null; 184 | $parameterType = $parameter->getType(); 185 | if ($parameterType === null) { 186 | $parameterClass = $this->typeHintReader->getParameterClass($parameter); 187 | } elseif ($parameterType instanceof ReflectionNamedType && $parameterType->isBuiltin() === false) { 188 | $parameterClass = $parameterType->getName(); 189 | } 190 | 191 | if ($parameterClass === null) { 192 | throw new ContainerException( 193 | "Type declaration or PHPDoc type hint for constructor parameter $paramName of " . 194 | "class {$definition->getClassName()} is missing or it is not a class!" 195 | ); 196 | } 197 | 198 | $definition->addConstructorArgumentFromClass($parameterClass); 199 | $this->resolve($parameterClass, $id, $parentEntryPoint, $runtime); 200 | $this->definitions[$parameterClass]->increaseReferenceCount($id, $definition->isSingleton($parentId)); 201 | } 202 | 203 | $invalidConstructorParameterOverrides = array_diff($definition->getOverriddenConstructorParameters(), $paramNames); 204 | if ($invalidConstructorParameterOverrides !== []) { 205 | throw new ContainerException( 206 | "Class {$definition->getClassName()} has the following overridden constructor parameters which don't exist: " . 207 | implode(", ", $invalidConstructorParameterOverrides) . "!" 208 | ); 209 | } 210 | } 211 | 212 | /** 213 | * @throws ContainerException 214 | */ 215 | private function resolveProperties( 216 | string $id, 217 | string $parentId, 218 | ClassDefinition $definition, 219 | EntryPointInterface $parentEntryPoint, 220 | bool $runtime 221 | ): void { 222 | $class = new ReflectionClass($id); 223 | 224 | $propertyNames = []; 225 | foreach ($class->getProperties() as $property) { 226 | $propertyName = $property->getName(); 227 | 228 | $propertyNames[] = $propertyName; 229 | 230 | if ($definition->isPropertyOverridden($propertyName)) { 231 | $definition->addPropertyFromOverride($propertyName); 232 | continue; 233 | } 234 | 235 | if ($property->getAttributes(Inject::class) === []) { 236 | continue; 237 | } 238 | 239 | if ($property->isStatic()) { 240 | throw new ContainerException( 241 | "Property {$class->getName()}::\$$propertyName is static and can't be injected upon!" 242 | ); 243 | } 244 | 245 | $propertyClass = null; 246 | $propertyType = $property->getType(); 247 | if ($propertyType === null) { 248 | $propertyClass = $this->typeHintReader->getPropertyClass($property); 249 | } elseif ($propertyType instanceof ReflectionNamedType && $propertyType->isBuiltin() === false) { 250 | $propertyClass = $propertyType->getName(); 251 | } 252 | 253 | if ($propertyClass === null) { 254 | throw new ContainerException( 255 | "Type declaration or PHPDoc type hint for property $id::\$$propertyName is missing or it is not a class!" 256 | ); 257 | } 258 | 259 | $definition->addPropertyFromClass($propertyName, $propertyClass); 260 | $this->resolve($propertyClass, $id, $parentEntryPoint, $runtime); 261 | $this->definitions[$propertyClass]->increaseReferenceCount($id, $definition->isSingleton($parentId)); 262 | } 263 | 264 | $invalidPropertyOverrides = array_diff($definition->getOverriddenProperties(), $propertyNames); 265 | if ($invalidPropertyOverrides !== []) { 266 | throw new ContainerException( 267 | "Class $id has the following overridden properties which don't exist: " . 268 | implode(", ", $invalidPropertyOverrides) . "!" 269 | ); 270 | } 271 | } 272 | 273 | private function isFileBased(string $id, EntryPointInterface $parentEntryPoint): bool 274 | { 275 | if (array_key_exists($id, $this->excludedFileBasedDefinitions)) { 276 | return false; 277 | } 278 | 279 | return $parentEntryPoint->isFileBased($this->fileBasedDefinitionConfig); 280 | } 281 | 282 | private function resetDefinitions(): void 283 | { 284 | $this->definitions = [ 285 | ContainerInterface::class => ReferenceDefinition::singleton( 286 | ContainerInterface::class, 287 | $this->compilerConfig->getContainerFqcn(), 288 | true 289 | ), 290 | $this->compilerConfig->getContainerFqcn() => new SelfDefinition($this->compilerConfig->getContainerFqcn()), 291 | ]; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/Container/Definition/AbstractDefinition.php: -------------------------------------------------------------------------------- 1 | id = $id; 35 | $this->hash = $this->hash($id); 36 | $this->singleton = $isSingleton; 37 | $this->entryPoint = $isEntryPoint; 38 | $this->fileBased = $isFileBased; 39 | $this->singletonReferenceCount = $singletonReferenceCount; 40 | $this->prototypeReferenceCount = $prototypeReferenceCount; 41 | } 42 | 43 | public function getId(string $parentId = ""): string 44 | { 45 | return $this->id; 46 | } 47 | 48 | public function getHash(string $parentId = ""): string 49 | { 50 | return $this->hash; 51 | } 52 | 53 | public function isSingleton(string $parentId = ""): bool 54 | { 55 | return $this->singleton; 56 | } 57 | 58 | public function isEntryPoint(string $parentId = ""): bool 59 | { 60 | return $this->entryPoint; 61 | } 62 | 63 | public function isFileBased(string $parentId = ""): bool 64 | { 65 | return $this->fileBased; 66 | } 67 | 68 | public function increaseReferenceCount(string $parentId, bool $isParentSingleton): DefinitionInterface 69 | { 70 | if ($isParentSingleton) { 71 | $this->singletonReferenceCount++; 72 | } else { 73 | $this->prototypeReferenceCount++; 74 | } 75 | 76 | return $this; 77 | } 78 | 79 | public function getSingletonReferenceCount(): int 80 | { 81 | return $this->singletonReferenceCount; 82 | } 83 | 84 | public function getPrototypeReferenceCount(): int 85 | { 86 | return $this->prototypeReferenceCount; 87 | } 88 | 89 | public function isDefinitionInlinable(string $parentId = ""): bool 90 | { 91 | return false; 92 | } 93 | 94 | public function isSingletonCheckEliminable(string $parentId = ""): bool 95 | { 96 | if ($this->singleton === false) { 97 | return true; 98 | } 99 | 100 | return $this->entryPoint === false && $this->singletonReferenceCount <= 1 && $this->prototypeReferenceCount === 0; 101 | } 102 | 103 | /** 104 | * @param string[] $preloadedClasses 105 | */ 106 | protected function compileEntryReference( 107 | DefinitionInterface $definition, 108 | DefinitionCompilation $compilation, 109 | int $indentationLevelWhenInlined, 110 | array $preloadedClasses 111 | ): string { 112 | if ($definition->isEntryPoint($this->id) === false) { 113 | return $this->compileInlinedEntry($definition, $compilation, $indentationLevelWhenInlined, $preloadedClasses); 114 | } 115 | 116 | return $this->compileReferencedEntry($definition, $compilation->getFileBasedDefinitionConfig()); 117 | } 118 | 119 | /** 120 | * @param string[] $preloadedClasses 121 | */ 122 | private function compileInlinedEntry( 123 | DefinitionInterface $definition, 124 | DefinitionCompilation $compilation, 125 | int $indentationLevelWhenInlined, 126 | array $preloadedClasses 127 | ): string { 128 | $id = $definition->getId($this->id); 129 | 130 | $code = ""; 131 | 132 | if ($definition->isSingletonCheckEliminable($this->id) === false) { 133 | $code .= "\$this->singletonEntries['$id'] ?? "; 134 | } 135 | 136 | $code .= $definition->compile($compilation, $this->id, $indentationLevelWhenInlined, true, $preloadedClasses); 137 | 138 | return $code; 139 | } 140 | 141 | private function compileReferencedEntry( 142 | DefinitionInterface $definition, 143 | FileBasedDefinitionConfigInterface $fileBasedDefinitionConfig 144 | ): string { 145 | $id = $definition->getId($this->id); 146 | $hash = $definition->getHash($this->id); 147 | $isFileBased = $definition->isFileBased($this->id); 148 | 149 | if ($isFileBased) { 150 | $path = "__DIR__ . '/"; 151 | if ($this->isFileBased($this->id) === false) { 152 | $path .= $fileBasedDefinitionConfig->getRelativeDefinitionDirectory() . "/"; 153 | } 154 | $path .= "$hash.php'"; 155 | 156 | if ($definition->isSingletonCheckEliminable($this->id) === false) { 157 | return "\$this->singletonEntries['$id'] ?? require $path"; 158 | } 159 | 160 | return "require $path"; 161 | } 162 | 163 | if ($definition->isSingletonCheckEliminable($this->id) === false) { 164 | return "\$this->singletonEntries['$id'] ?? \$this->$hash()"; 165 | } 166 | 167 | return "\$this->$hash()"; 168 | } 169 | 170 | protected function hash(string $id): string 171 | { 172 | return str_replace("\\", "__", $id); 173 | } 174 | 175 | protected function indent(int $indentationLevel): string 176 | { 177 | return str_repeat(" ", $indentationLevel * 4); 178 | } 179 | 180 | /** 181 | * @param array $relatedClasses 182 | */ 183 | protected function collectParentClasses(string $id, array &$relatedClasses): void 184 | { 185 | try { 186 | $class = new ReflectionClass($id); 187 | } catch (ReflectionException $exception) { 188 | return; 189 | } 190 | 191 | while ($parent = $class->getParentClass()) { 192 | $name = $parent->getName(); 193 | 194 | $relatedClasses[$name] = $name; 195 | foreach ($class->getInterfaceNames() as $interface) { 196 | if (array_key_exists($interface, $relatedClasses)) { 197 | unset($relatedClasses[$interface]); 198 | } 199 | $relatedClasses[$interface] = $interface; 200 | } 201 | 202 | $class = $parent; 203 | } 204 | 205 | foreach ($class->getInterfaceNames() as $interface) { 206 | if (array_key_exists($interface, $relatedClasses)) { 207 | unset($relatedClasses[$interface]); 208 | } 209 | $relatedClasses[$interface] = $interface; 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Container/Definition/ClassDefinition.php: -------------------------------------------------------------------------------- 1 | */ 18 | private array $constructorArguments; 19 | /** @var array */ 20 | private array $properties; 21 | private bool $needsDependencyResolution; 22 | /** @var array|null> */ 23 | private array $overriddenConstructorParameters; 24 | /** @var array|null> */ 25 | private array $overriddenProperties; 26 | 27 | /** 28 | * @param array|null> $overriddenConstructorParameters 29 | * @param array|null> $overriddenProperties 30 | */ 31 | public static function singleton( 32 | string $className, 33 | bool $isEntryPoint = false, 34 | bool $isFileBased = false, 35 | array $overriddenConstructorParameters = [], 36 | array $overriddenProperties = [], 37 | int $singletonReferenceCount = 0, 38 | int $prototypeReferenceCount = 0 39 | ): ClassDefinition { 40 | return new self( 41 | $className, 42 | true, 43 | $isEntryPoint, 44 | $isFileBased, 45 | $overriddenConstructorParameters, 46 | $overriddenProperties, 47 | $singletonReferenceCount, 48 | $prototypeReferenceCount 49 | ); 50 | } 51 | 52 | /** 53 | * @param array|null> $overriddenConstructorParameters 54 | * @param array|null> $overriddenProperties 55 | */ 56 | public static function prototype( 57 | string $className, 58 | bool $isEntryPoint = false, 59 | bool $isFileBased = false, 60 | array $overriddenConstructorParameters = [], 61 | array $overriddenProperties = [], 62 | int $singletonReferenceCount = 0, 63 | int $prototypeReferenceCount = 0 64 | ): ClassDefinition { 65 | return new self( 66 | $className, 67 | false, 68 | $isEntryPoint, 69 | $isFileBased, 70 | $overriddenConstructorParameters, 71 | $overriddenProperties, 72 | $singletonReferenceCount, 73 | $prototypeReferenceCount 74 | ); 75 | } 76 | 77 | /** 78 | * @param array|null> $overriddenConstructorParameters 79 | * @param array|null> $overriddenProperties 80 | */ 81 | public function __construct( 82 | string $className, 83 | bool $isSingleton = true, 84 | bool $isEntryPoint = false, 85 | bool $isFileBased = false, 86 | array $overriddenConstructorParameters = [], 87 | array $overriddenProperties = [], 88 | int $singletonReferenceCount = 0, 89 | int $prototypeReferenceCount = 0 90 | ) { 91 | parent::__construct( 92 | $className, 93 | $isSingleton, 94 | $isEntryPoint, 95 | $isFileBased, 96 | $singletonReferenceCount, 97 | $prototypeReferenceCount 98 | ); 99 | $this->constructorArguments = []; 100 | $this->properties = []; 101 | $this->needsDependencyResolution = true; 102 | $this->overriddenConstructorParameters = $overriddenConstructorParameters; 103 | $this->overriddenProperties = $overriddenProperties; 104 | } 105 | 106 | public function getClassName(): string 107 | { 108 | return $this->id; 109 | } 110 | 111 | public function addConstructorArgumentFromClass(string $className): ClassDefinition 112 | { 113 | $this->constructorArguments[] = ["class" => $className]; 114 | 115 | return $this; 116 | } 117 | 118 | public function addConstructorArgumentFromValue(mixed $value): ClassDefinition 119 | { 120 | $this->constructorArguments[] = ["value" => $value]; 121 | 122 | return $this; 123 | } 124 | 125 | public function addConstructorArgumentFromOverride(string $name): ClassDefinition 126 | { 127 | $this->constructorArguments[] = ["value" => $this->overriddenConstructorParameters[$name] ?? null]; 128 | 129 | return $this; 130 | } 131 | 132 | public function addPropertyFromClass(string $name, string $className): ClassDefinition 133 | { 134 | $this->properties[$name] = ["class" => $className]; 135 | 136 | return $this; 137 | } 138 | 139 | public function addPropertyFromOverride(string $name): ClassDefinition 140 | { 141 | $this->properties[$name] = ["value" => $this->overriddenProperties[$name] ?? null]; 142 | 143 | return $this; 144 | } 145 | 146 | public function needsDependencyResolution(): bool 147 | { 148 | return $this->needsDependencyResolution; 149 | } 150 | 151 | public function resolveDependencies(): DefinitionInterface 152 | { 153 | $this->needsDependencyResolution = false; 154 | 155 | return $this; 156 | } 157 | 158 | public function isDefinitionInlinable(string $parentId = ""): bool 159 | { 160 | if ($this->isFileBased($parentId)) { 161 | return true; 162 | } 163 | 164 | if ($this->getSingletonReferenceCount() >= 1 || $this->getPrototypeReferenceCount() >= 1) { 165 | return false; 166 | } 167 | 168 | return true; 169 | } 170 | 171 | public function isConstructorParameterOverridden(string $name): bool 172 | { 173 | return array_key_exists($name, $this->overriddenConstructorParameters); 174 | } 175 | 176 | /** 177 | * @return array|null> 178 | */ 179 | public function getOverriddenConstructorParameters(): array 180 | { 181 | return array_keys($this->overriddenConstructorParameters); 182 | } 183 | 184 | public function isPropertyOverridden(string $name): bool 185 | { 186 | return array_key_exists($name, $this->overriddenProperties); 187 | } 188 | 189 | /** 190 | * @return array|null> 191 | */ 192 | public function getOverriddenProperties(): array 193 | { 194 | return array_keys($this->overriddenProperties); 195 | } 196 | 197 | /** 198 | * @return string[] 199 | */ 200 | public function getClassDependencies(): array 201 | { 202 | $dependencies = []; 203 | 204 | foreach ($this->constructorArguments as $constructorArgument) { 205 | if (array_key_exists("class", $constructorArgument)) { 206 | $dependencies[] = $constructorArgument["class"]; 207 | } 208 | } 209 | 210 | foreach ($this->properties as $property) { 211 | if (array_key_exists("class", $property)) { 212 | $dependencies[] = $property["class"]; 213 | } 214 | } 215 | 216 | return $dependencies; 217 | } 218 | 219 | /** 220 | * @param DefinitionInstantiation $instantiation 221 | * @param string $parentId 222 | */ 223 | public function instantiate($instantiation, $parentId): mixed 224 | { 225 | if ($this->singleton === false) { 226 | return $this->instantiateClass($instantiation); 227 | } 228 | 229 | return $instantiation->singletonEntries[$this->id] ?? $instantiation->singletonEntries[$this->id] = $this->instantiateClass($instantiation); 230 | } 231 | 232 | /** 233 | * @param string[] $preloadedClasses 234 | */ 235 | public function compile( 236 | DefinitionCompilation $compilation, 237 | string $parentId, 238 | int $indentationLevel, 239 | bool $inline = false, 240 | array $preloadedClasses = [] 241 | ): string { 242 | $indent = $this->indent($indentationLevel); 243 | $tab = $this->indent(1); 244 | $hasProperties = $this->properties !== []; 245 | $hasConstructorArguments = $this->constructorArguments !== []; 246 | 247 | $code = ""; 248 | 249 | if ($inline === false) { 250 | $code .= "{$indent}return "; 251 | } 252 | 253 | if ($this->isSingletonCheckEliminable($parentId) === false) { 254 | $code .= "\$this->singletonEntries['{$this->id}'] = "; 255 | } 256 | 257 | if ($hasProperties) { 258 | $code .= "\$this->setClassProperties(\n"; 259 | $code .= "{$indent}{$tab}"; 260 | } 261 | 262 | $code .= "new \\" . $this->getClassName() . "("; 263 | if ($hasConstructorArguments === false) { 264 | $code .= ")"; 265 | } 266 | 267 | $constructorIndentationLevel = $indentationLevel + ($hasProperties ? 1 : 0); 268 | $constructorIndent = $this->indent($constructorIndentationLevel); 269 | 270 | foreach ($this->constructorArguments as $constructorArgument) { 271 | if (array_key_exists("class", $constructorArgument)) { 272 | $definition = $compilation->getDefinition($constructorArgument["class"]); 273 | 274 | $code .= "\n{$constructorIndent}{$tab}" . $this->compileEntryReference( 275 | $definition, 276 | $compilation, 277 | $constructorIndentationLevel + 1, 278 | $preloadedClasses 279 | ) . ","; 280 | } elseif (array_key_exists("value", $constructorArgument)) { 281 | $code .= "\n{$constructorIndent}{$tab}" . $this->serializeValue($constructorArgument["value"]) . ","; 282 | } 283 | } 284 | 285 | if ($hasConstructorArguments) { 286 | $code .= "\n{$constructorIndent})"; 287 | } 288 | 289 | if ($hasProperties) { 290 | $code .= ",\n"; 291 | $code .= "{$indent}{$tab}[\n"; 292 | foreach ($this->properties as $propertyName => $property) { 293 | if (array_key_exists("class", $property)) { 294 | $definition = $compilation->getDefinition($property["class"]); 295 | 296 | $code .= "{$indent}{$tab}{$tab}'$propertyName' => " . $this->compileEntryReference( 297 | $definition, 298 | $compilation, 299 | $indentationLevel + 2, 300 | $preloadedClasses 301 | ) . ",\n"; 302 | } elseif (array_key_exists("value", $property)) { 303 | $code .= "{$indent}{$tab}{$tab}'$propertyName' => " . $this->serializeValue($property["value"]) . ",\n"; 304 | } 305 | } 306 | $code .= "{$indent}{$tab}]\n"; 307 | $code .= "{$indent})"; 308 | } 309 | 310 | if ($inline === false) { 311 | $code .= ";\n"; 312 | } 313 | 314 | return $code; 315 | } 316 | 317 | private function instantiateClass(DefinitionInstantiation $instantiation): mixed 318 | { 319 | $arguments = []; 320 | foreach ($this->constructorArguments as $argument) { 321 | if (array_key_exists("class", $argument)) { 322 | $arguments[] = $instantiation->definitions[$argument["class"]]->instantiate($instantiation, $this->id); 323 | } elseif (array_key_exists("value", $argument)) { 324 | $arguments[] = $argument["value"]; 325 | } 326 | } 327 | 328 | $className = $this->id; 329 | $object = new $className(...$arguments); 330 | 331 | if ($this->properties !== []) { 332 | $properties = $this->properties; 333 | Closure::bind( 334 | static function () use ($instantiation, $className, $object, $properties): void { 335 | foreach ($properties as $name => $property) { 336 | if (array_key_exists("class", $property)) { 337 | $object->$name = $instantiation->definitions[$property["class"]]->instantiate($instantiation, $className); 338 | } elseif (array_key_exists("value", $property)) { 339 | $object->$name = $property["value"]; 340 | } 341 | } 342 | }, 343 | null, 344 | $object 345 | )->__invoke(); 346 | } 347 | 348 | return $object; 349 | } 350 | 351 | private function serializeValue(mixed $value): string 352 | { 353 | return var_export($value, true); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/Container/Definition/ContextDependentDefinition.php: -------------------------------------------------------------------------------- 1 | referrerId = $referrerId; 26 | $this->defaultDefinition = $defaultDefinition; 27 | $this->definitions = $contextDependentDefinitions; 28 | } 29 | 30 | public function getId(string $parentId = ""): string 31 | { 32 | return $this->getDefinition($parentId)->getId($parentId); 33 | } 34 | 35 | public function getHash(string $parentId = ""): string 36 | { 37 | return $this->getDefinition($parentId)->getHash($parentId); 38 | } 39 | 40 | public function isSingleton(string $parentId = ""): bool 41 | { 42 | return $this->getDefinition($parentId)->isSingleton($parentId); 43 | } 44 | 45 | public function isEntryPoint(string $parentId = ""): bool 46 | { 47 | return $this->getDefinition($parentId)->isEntryPoint($parentId); 48 | } 49 | 50 | public function isFileBased(string $parentId = ""): bool 51 | { 52 | return $this->getDefinition($parentId)->isFileBased($parentId); 53 | } 54 | 55 | public function increaseReferenceCount(string $parentId, bool $isSingletonParent): DefinitionInterface 56 | { 57 | return $this->getDefinition($parentId)->increaseReferenceCount($parentId, $isSingletonParent); 58 | } 59 | 60 | public function isDefinitionInlinable(string $parentId = ""): bool 61 | { 62 | return false; 63 | } 64 | 65 | public function isSingletonCheckEliminable(string $parentId = ""): bool 66 | { 67 | return $this->getDefinition($parentId)->isSingletonCheckEliminable($parentId); 68 | } 69 | 70 | public function needsDependencyResolution(): bool 71 | { 72 | return false; 73 | } 74 | 75 | public function resolveDependencies(): DefinitionInterface 76 | { 77 | return $this; 78 | } 79 | 80 | /** 81 | * @return string[] 82 | */ 83 | public function getClassDependencies(): array 84 | { 85 | return [ 86 | ]; 87 | } 88 | 89 | /** 90 | * @param DefinitionInstantiation $instantiation 91 | * @param string $parentId 92 | */ 93 | public function instantiate($instantiation, $parentId): mixed 94 | { 95 | return $this->getDefinition($parentId)->instantiate($instantiation, $this->referrerId); 96 | } 97 | 98 | /** 99 | * @param string[] $preloadedClasses 100 | */ 101 | public function compile( 102 | DefinitionCompilation $compilation, 103 | string $parentId, 104 | int $indentationLevel, 105 | bool $inline = false, 106 | array $preloadedClasses = [] 107 | ): string { 108 | return $this->getDefinition($parentId)->compile($compilation, $parentId, $indentationLevel, $inline, $preloadedClasses); 109 | } 110 | 111 | private function getDefinition(string $parentId): DefinitionInterface 112 | { 113 | if (array_key_exists($parentId, $this->definitions)) { 114 | return $this->definitions[$parentId]; 115 | } 116 | 117 | if ($this->defaultDefinition !== null) { 118 | return $this->defaultDefinition; 119 | } 120 | 121 | throw new ContainerException( 122 | "The Context-Dependent definition with the '{$this->referrerId}' ID can't be injected for the '{$parentId}' class!" 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Container/Definition/DefinitionInterface.php: -------------------------------------------------------------------------------- 1 | referencedId = $referencedId; 70 | } 71 | 72 | public function isDefinitionInlinable(string $parentId = ""): bool 73 | { 74 | if ($this->isFileBased($parentId)) { 75 | return true; 76 | } 77 | 78 | if ($this->getSingletonReferenceCount() >= 1 || $this->getPrototypeReferenceCount() >= 1) { 79 | return false; 80 | } 81 | 82 | return true; 83 | } 84 | 85 | public function needsDependencyResolution(): bool 86 | { 87 | return false; 88 | } 89 | 90 | public function resolveDependencies(): DefinitionInterface 91 | { 92 | return $this; 93 | } 94 | 95 | /** 96 | * @return string[] 97 | */ 98 | public function getClassDependencies(): array 99 | { 100 | return [ 101 | $this->referencedId, 102 | ]; 103 | } 104 | 105 | /** 106 | * @param DefinitionInstantiation $instantiation 107 | * @param string $parentId 108 | */ 109 | public function instantiate($instantiation, $parentId): mixed 110 | { 111 | if ($this->singleton === false) { 112 | return $instantiation->definitions[$this->referencedId]->instantiate($instantiation, $this->id); 113 | } 114 | 115 | return $instantiation->singletonEntries[$this->id] ?? $instantiation->singletonEntries[$this->id] = 116 | $instantiation->definitions[$this->referencedId]->instantiate($instantiation, $this->id); 117 | } 118 | 119 | /** 120 | * @param string[] $preloadedClasses 121 | */ 122 | public function compile( 123 | DefinitionCompilation $compilation, 124 | string $parentId, 125 | int $indentationLevel, 126 | bool $inline = false, 127 | array $preloadedClasses = [] 128 | ): string { 129 | $indent = $this->indent($indentationLevel); 130 | 131 | $code = ""; 132 | 133 | if ($inline === false) { 134 | $code .= "{$indent}return "; 135 | } 136 | 137 | if ($this->isSingletonCheckEliminable($parentId) === false) { 138 | $code .= "\$this->singletonEntries['{$this->id}'] = "; 139 | } 140 | 141 | $definition = $compilation->getDefinition($this->referencedId); 142 | 143 | $code .= $this->compileEntryReference($definition, $compilation, $indentationLevel, $preloadedClasses); 144 | 145 | if ($inline === false) { 146 | $code .= ";\n"; 147 | } 148 | 149 | return $code; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Container/Definition/SelfDefinition.php: -------------------------------------------------------------------------------- 1 | container; 52 | } 53 | 54 | /** 55 | * @param string[] $preloadedClasses 56 | */ 57 | public function compile( 58 | DefinitionCompilation $compilation, 59 | string $parentId, 60 | int $indentationLevel, 61 | bool $inline = false, 62 | array $preloadedClasses = [] 63 | ): string { 64 | $indent = $this->indent($indentationLevel); 65 | 66 | if ($inline) { 67 | return "\$this"; 68 | } 69 | 70 | return "{$indent}return \$this;\n"; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Container/DefinitionCompilation.php: -------------------------------------------------------------------------------- 1 | fileBasedDefinitionConfig = $fileBasedDefinitionConfig; 24 | $this->definitions = $definitions; 25 | } 26 | 27 | public function getFileBasedDefinitionConfig(): FileBasedDefinitionConfigInterface 28 | { 29 | return $this->fileBasedDefinitionConfig; 30 | } 31 | 32 | /** 33 | * @return DefinitionInterface[] 34 | */ 35 | public function getDefinitions(): array 36 | { 37 | return $this->definitions; 38 | } 39 | 40 | public function getDefinition(string $id): DefinitionInterface 41 | { 42 | return $this->definitions[$id]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Container/DefinitionInstantiation.php: -------------------------------------------------------------------------------- 1 | */ 16 | public array $singletonEntries = []; 17 | 18 | public function __construct(RuntimeContainer $container) 19 | { 20 | $this->container = $container; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Container/PreloadCompiler.php: -------------------------------------------------------------------------------- 1 | getPreloadConfig()->getRelativeBasePath(); 22 | $preloadedFiles = $compilerConfig->getPreloadConfig()->getPreloadedFiles(); 23 | $preloadedClassFiles = array_values($preloadedClassFiles); 24 | $files = array_unique(array_merge($preloadedFiles, $preloadedClassFiles)); 25 | 26 | $preloader = "typeHintReader = new PhpDocReader(); 29 | 30 | $this->preloads = $compilerConfig->getPreloadMap(); 31 | } 32 | 33 | /** 34 | * @return string[] 35 | */ 36 | public function resolvePreloads(): array 37 | { 38 | $this->resetClasses(); 39 | 40 | foreach ($this->preloads as $id => $preload) { 41 | $this->resolve($id); 42 | } 43 | 44 | return $this->classes; 45 | } 46 | 47 | /** 48 | * @param string $id 49 | */ 50 | private function resolve($id): void 51 | { 52 | if (array_key_exists($id, $this->classes)) { 53 | return; 54 | } 55 | 56 | try { 57 | $reflectionClass = new ReflectionClass($id); 58 | 59 | if ($reflectionClass->isInternal()) { 60 | return; 61 | } 62 | 63 | if (in_array($reflectionClass->getName(), ["self", "static", "parent"], true)) { 64 | return; 65 | } 66 | 67 | $filename = $reflectionClass->getFileName(); 68 | $this->classes[$id] = $filename !== false ? $filename : ""; 69 | $this->resolveParents($reflectionClass); 70 | $this->resolveTraits($reflectionClass); 71 | $this->resolveConstructorArguments($reflectionClass); 72 | $this->resolveProperties($reflectionClass); 73 | $this->resolveMethods($reflectionClass); 74 | } catch (ReflectionException $exception) { 75 | } 76 | } 77 | 78 | /** 79 | * @param ReflectionClass $reflectionClass 80 | */ 81 | private function resolveParents($reflectionClass): void 82 | { 83 | foreach ($reflectionClass->getInterfaceNames() as $interface) { 84 | $this->resolve($interface); 85 | } 86 | 87 | $parent = $reflectionClass->getParentClass(); 88 | if ($parent === false) { 89 | return; 90 | } 91 | 92 | $this->resolve($parent->getName()); 93 | } 94 | 95 | /** 96 | * @param ReflectionClass $reflectionClass 97 | */ 98 | private function resolveTraits($reflectionClass): void 99 | { 100 | foreach ($reflectionClass->getTraitNames() as $trait) { 101 | $this->resolve($trait); 102 | } 103 | } 104 | 105 | /** 106 | * @param ReflectionClass $reflectionClass 107 | */ 108 | private function resolveConstructorArguments($reflectionClass): void 109 | { 110 | $constructor = $reflectionClass->getConstructor(); 111 | if ($constructor === null) { 112 | return; 113 | } 114 | 115 | foreach ($constructor->getParameters() as $param) { 116 | $paramClass = $this->typeHintReader->getParameterClass($param); 117 | if ($paramClass === null) { 118 | continue; 119 | } 120 | 121 | $this->resolve($paramClass); 122 | } 123 | } 124 | 125 | /** 126 | * @param ReflectionClass $reflectionClass 127 | */ 128 | private function resolveProperties($reflectionClass): void 129 | { 130 | foreach ($reflectionClass->getProperties() as $property) { 131 | $propertyClass = null; 132 | $propertyType = $property->getType(); 133 | 134 | if ($propertyType === null) { 135 | $propertyClass = $this->typeHintReader->getPropertyClass($property); 136 | if ($propertyClass !== null) { 137 | $this->resolve($propertyClass); 138 | } 139 | } elseif ($propertyType instanceof ReflectionNamedType && $propertyType->isBuiltin() === false) { 140 | $this->resolve($propertyType->getName()); 141 | } elseif ($propertyType instanceof ReflectionUnionType) { 142 | foreach ($propertyType->getTypes() as $type) { 143 | if ($type instanceof ReflectionNamedType && $type->isBuiltin() === false) { 144 | $this->resolve($type->getName()); 145 | } 146 | } 147 | } 148 | } 149 | } 150 | 151 | /** 152 | * @param ReflectionClass $reflectionClass 153 | */ 154 | private function resolveMethods($reflectionClass): void 155 | { 156 | foreach ($reflectionClass->getMethods() as $method) { 157 | foreach ($method->getParameters() as $parameter) { 158 | $type = $parameter->getType(); 159 | if ($type === null) { 160 | $class = $this->typeHintReader->getParameterClass($parameter); 161 | if ($class !== null) { 162 | $this->resolve($class); 163 | } 164 | } elseif ($type instanceof ReflectionNamedType && $type->isBuiltin() === false) { 165 | $this->resolve($type->getName()); 166 | } elseif ($type instanceof ReflectionUnionType) { 167 | foreach ($type->getTypes() as $subType) { 168 | if ($subType instanceof ReflectionNamedType && $subType->isBuiltin() === false) { 169 | $this->resolve($subType->getName()); 170 | } 171 | } 172 | } 173 | } 174 | 175 | $returnType = $method->getReturnType(); 176 | if ($returnType instanceof ReflectionNamedType && $returnType->isBuiltin() === false) { 177 | $this->resolve($returnType->getName()); 178 | } elseif ($returnType instanceof ReflectionUnionType) { 179 | foreach ($returnType->getTypes() as $subType) { 180 | if ($subType instanceof ReflectionNamedType && $subType->isBuiltin() === false) { 181 | $this->resolve($subType->getName()); 182 | } 183 | } 184 | } 185 | } 186 | } 187 | 188 | private function resetClasses(): void 189 | { 190 | $this->classes = []; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Exception/ContainerException.php: -------------------------------------------------------------------------------- 1 | dependencyResolver = new ContainerDependencyResolver($compilerConfig); 25 | $this->instantiation = new DefinitionInstantiation($this); 26 | } 27 | 28 | public function has(string $id): bool 29 | { 30 | if (array_key_exists($id, $this->instantiation->definitions)) { 31 | return $this->instantiation->definitions[$id]->isEntryPoint(); 32 | } 33 | 34 | try { 35 | $this->resolve($id); 36 | } catch (NotFoundException $exception) { 37 | return false; 38 | } 39 | 40 | return true; 41 | } 42 | 43 | /** 44 | * @throws NotFoundException 45 | */ 46 | public function get(string $id): mixed 47 | { 48 | return $this->instantiation->singletonEntries[$id] ?? ($this->instantiation->definitions[$id] ?? $this->resolve($id))->instantiate($this->instantiation, ""); 49 | } 50 | 51 | /** 52 | * @throws NotFoundException 53 | */ 54 | private function resolve(string $id): DefinitionInterface 55 | { 56 | $this->instantiation->definitions = array_merge( 57 | $this->instantiation->definitions, 58 | $this->dependencyResolver->resolveEntryPoint($id) 59 | ); 60 | 61 | return $this->instantiation->definitions[$id]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Utils/FileSystemUtil.php: -------------------------------------------------------------------------------- 1 | getFileName(); 50 | } catch (Throwable $e) { 51 | $pathCache[$key] = ""; 52 | 53 | return ""; 54 | } 55 | 56 | if ($filename === false) { 57 | $pathCache[$key] = ""; 58 | 59 | return ""; 60 | } 61 | 62 | // Make the filename relative to the root directory 63 | $filename = self::getRelativeFilename($basePath, $filename); 64 | 65 | $pathCache[$key] = $filename; 66 | 67 | return $filename; 68 | } 69 | 70 | public static function getRelativeFilename(string $basePath, string $filename): string 71 | { 72 | if ($basePath !== "" && strpos($filename, $basePath) === 0) { 73 | $filename = ltrim(substr($filename, strlen($basePath)), "\\/"); 74 | } 75 | 76 | return $filename; 77 | } 78 | 79 | /** 80 | * @return string[] 81 | */ 82 | public static function getClassesInPath(string $path, bool $onlyConcreteClasses): array 83 | { 84 | $result = []; 85 | 86 | foreach (self::getPhpFilesInPath($path) as $filePath) { 87 | foreach (self::getClassesInFile($filePath, $onlyConcreteClasses) as $namespace => $classes) { 88 | foreach ($classes as $class) { 89 | $result[] = is_string($namespace) ? $namespace . "\\" . $class : $class; 90 | } 91 | } 92 | } 93 | 94 | return $result; 95 | } 96 | 97 | /** 98 | * @return string[] 99 | */ 100 | public static function getPhpFilesInPath(string $path, bool $recursive = true, bool $caseInsensitive = true): array 101 | { 102 | if ($recursive) { 103 | $directoryIterator = new RecursiveDirectoryIterator($path); 104 | $iterator = new RecursiveIteratorIterator($directoryIterator); 105 | } else { 106 | $directoryIterator = new DirectoryIterator($path); 107 | $iterator = new IteratorIterator($directoryIterator); 108 | } 109 | 110 | $result = []; 111 | foreach ($iterator as $file) { 112 | assert($file instanceof SplFileInfo); 113 | $path = $file->getPathname(); 114 | 115 | if (isset($path[4]) && substr_compare($path, ".php", -4, null, $caseInsensitive) === 0) { 116 | $result[] = $path; 117 | } 118 | } 119 | 120 | return $result; 121 | } 122 | 123 | /** 124 | * @return string[][] 125 | */ 126 | private static function getClassesInFile(string $filePath, bool $onlyConcreteClasses): array 127 | { 128 | $classes = []; 129 | $namespace = 0; 130 | $content = file_get_contents($filePath); 131 | if ($content === false) { 132 | return []; 133 | } 134 | 135 | $tokens = token_get_all($content); 136 | $count = count($tokens); 137 | $dlm = false; 138 | 139 | for ($i = 2; $i < $count; $i++) { 140 | if (self::isNamespace($tokens, $i, $dlm)) { 141 | if ($dlm === false) { 142 | $namespace = 0; 143 | } 144 | 145 | if (isset($tokens[$i][1])) { 146 | $namespace = $namespace !== 0 ? $namespace . "\\" . $tokens[$i][1] : $tokens[$i][1]; 147 | $dlm = true; 148 | } 149 | } elseif ($dlm && ($tokens[$i][0] !== T_NS_SEPARATOR) && ($tokens[$i][0] !== T_STRING)) { 150 | $dlm = false; 151 | } 152 | 153 | if (self::isRequiredClass($tokens, $i, $onlyConcreteClasses)) { 154 | $className = $tokens[$i][1]; 155 | if (array_key_exists($namespace, $classes) === false) { 156 | $classes[$namespace] = []; 157 | } 158 | $classes[$namespace][] = $className; 159 | } 160 | } 161 | 162 | return $classes; 163 | } 164 | 165 | /** 166 | * @param array|string> $tokens 167 | */ 168 | private static function isNamespace(array $tokens, int $position, bool $dlm): bool 169 | { 170 | return (isset($tokens[$position - 2][1]) && $tokens[$position - 2][1] === "namespace") || 171 | ($dlm && $tokens[$position - 1][0] === T_NS_SEPARATOR && $tokens[$position][0] === T_STRING); 172 | } 173 | 174 | /** 175 | * @param array|string> $tokens 176 | */ 177 | private static function isRequiredClass(array $tokens, int $position, bool $onlyConcreteClasses): bool 178 | { 179 | if ($onlyConcreteClasses) { 180 | return self::isClass($tokens, $position, [T_CLASS], true); 181 | } 182 | 183 | return self::isClass($tokens, $position, [T_CLASS, T_INTERFACE], false); 184 | } 185 | 186 | /** 187 | * @param array|string> $tokens 188 | * @param array $allowedClassTokens 189 | */ 190 | private static function isClass( 191 | array $tokens, 192 | int $position, 193 | array $allowedClassTokens, 194 | bool $onlyConcreteClasses 195 | ): bool { 196 | $type = $tokens[$position - 4][0] ?? null; 197 | $class = $tokens[$position - 2][0]; 198 | $whitespace = $tokens[$position - 1][0]; 199 | $name = $tokens[$position][0]; 200 | 201 | $result = in_array($class, $allowedClassTokens, true) && $whitespace === T_WHITESPACE && $name === T_STRING; 202 | 203 | return $result && ($onlyConcreteClasses === false || $type !== T_ABSTRACT); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/Utils/NamespaceUtil.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public static function getClassesInPsr4Namespace(string $namespace, bool $recursive, bool $onlyInstantiable): array 25 | { 26 | /** @var array>|null $psr4Prefixes */ 27 | static $psr4Prefixes = null; 28 | if ($psr4Prefixes === null) { 29 | $psr4Prefixes = require self::getPsr4AutoloaderPath(); 30 | } 31 | 32 | $namespacePrefix = self::getBestMatchingPsr4Prefix($psr4Prefixes, $namespace); 33 | if ($namespacePrefix === "") { 34 | return []; 35 | } 36 | 37 | $namespacePostfix = substr($namespace, strlen($namespacePrefix)); 38 | $prefixPaths = $psr4Prefixes[$namespacePrefix]; 39 | 40 | $classNames = []; 41 | foreach ($prefixPaths as $prefixPath) { 42 | $postfixPath = str_replace("\\", DIRECTORY_SEPARATOR, $namespacePostfix); 43 | $path = $prefixPath . DIRECTORY_SEPARATOR . ($postfixPath !== "" ? $postfixPath . DIRECTORY_SEPARATOR : ""); 44 | $pathLength = strlen($path); 45 | 46 | foreach (FileSystemUtil::getPhpFilesInPath($path, $recursive) as $file) { 47 | $fileName = str_replace(".php", "", substr($file, $pathLength)); 48 | $className = str_replace(DIRECTORY_SEPARATOR, "\\", $fileName); 49 | $fqClassName = $namespacePrefix . $namespacePostfix . "\\" . $className; 50 | 51 | if ($onlyInstantiable) { 52 | try { 53 | $reflectionClass = new ReflectionClass($fqClassName); 54 | if ($reflectionClass->isInstantiable()) { 55 | $classNames[] = $fqClassName; 56 | } 57 | } catch (ReflectionException $exception) { 58 | continue; 59 | } 60 | } else { 61 | $classNames[] = $fqClassName; 62 | } 63 | } 64 | } 65 | 66 | return $classNames; 67 | } 68 | 69 | private static function getPsr4AutoloaderPath(): string 70 | { 71 | static $autoloaderPath = null; 72 | if ($autoloaderPath !== null) { 73 | return (string) $autoloaderPath; 74 | } 75 | 76 | $paths = [ 77 | __DIR__ . "/../../../../composer/autoload_psr4.php", 78 | __DIR__ . "/../../vendor/composer/autoload_psr4.php", 79 | __DIR__ . "/../vendor/composer/autoload_psr4.php", 80 | ]; 81 | 82 | foreach ($paths as $file) { 83 | if (file_exists($file)) { 84 | return $autoloaderPath = $file; 85 | } 86 | } 87 | 88 | throw new ContainerException("PSR-4 autoloader file can not be found!"); 89 | } 90 | 91 | /** 92 | * @param array> $psr4Prefixes 93 | */ 94 | private static function getBestMatchingPsr4Prefix(array $psr4Prefixes, string $namespace): string 95 | { 96 | $maxLength = 0; 97 | $maxPrefix = ""; 98 | 99 | foreach ($psr4Prefixes as $prefix => $path) { 100 | $prefixLength = strlen($prefix); 101 | 102 | if ($prefixLength > $maxLength && substr_compare($namespace, $prefix, 0, $prefixLength) === 0) { 103 | $maxPrefix = $prefix; 104 | $maxLength = $prefixLength; 105 | } 106 | } 107 | 108 | return $maxPrefix; 109 | } 110 | } 111 | --------------------------------------------------------------------------------