Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Woohoo Labs. Zen is a very fast and simple, PSR-11 compliant DI Container & preload file generator.

License

NotificationsYou must be signed in to change notification settings

woohoolabs/zen

Repository files navigation

Latest Version on PackagistSoftware LicenseBuild StatusCoverage StatusTotal DownloadsGitter

Woohoo Labs. Zen is a very fast and easy-to-use, PSR-11 (former Container-Interop) compliant DI Container.

Table of Contents

Introduction

Rationale

Although Dependency Injection is one of the most fundamental principles of Object Oriented Programming, it doesn'tget as much attention as it should. To make things even worse, there are quite some misbeliefs around the topic whichcan prevent people from applying the theory correctly.

Besides using Service Location, the biggest misbelief certainly is that Dependency Injection requires very complex toolscalled DI Containers. And we all deem to know that their performance is ridiculously low. Woohoo Labs. Zen was born afterthe realization of the fact that these fallacies were true indeed back in 2016.

That's why I tried to create a DI container which makes configuration as explicit and convenient as possible,which enforces the correct usage of Dependency Injection while providingoutstanding performance accordingtomy benchmarks. That's how Zen was born. AlthoughI imagined a very simple container initially, with only the essential feature set, over the time, Zen managed to featurethe most important capabilities of the most popular DI Containers currently available.

Fortunately, since the birth of Zen a lot of progress have been made in the DI Container ecosystem: many containers almostdoubled their performance, autowiring and compilation became more popular, but one thing didn't change: Zen is stillone of the fastest PHP containers out there.

Features

  • PSR-11 (former Container-Interop) compliance
  • Supports compilation formaximum performance
  • Supports constructor and property injection
  • Supports the notion of scopes (Singleton and Prototype)
  • Supports autowiring
  • Supports scalar and context-dependent injection
  • Supports dynamic usage for development
  • Supports generating apreload file

Install

The only thing you need isComposer before getting started. Then run the command below to getthe latest version:

$ composer require woohoolabs/zen

Note: The tests and examples won't be downloaded by default. You have to usecomposer require woohoolabs/zen --prefer-sourceor clone the repository if you need them.

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+.

Basic Usage

Using the container

As Zen is a PSR-11 compliant container, it supports the$container->has() and$container->get() methods as defined byContainerInterface.

Types of injection

Only constructor and property injection of objects and scalar types are supported by Zen.

In order to use constructor injection, you have to declare the type of the parameters or add a@param PHPDoc tag for them. If aparameter has a default value then this value will be injected. Here is an example of a constructor with valid parameters:

/** * @param B $b */publicfunction__construct(A$a,$b,$c =true){// ...}

In order to use property injection, you have to annotate your properties with#[Inject] (mind case-sensitivity!), andprovide their type via either a type declaration or a@var PHPDoc tag, as shown below:

#[Inject]/** @var A */private$a;#[Inject]private B$b;

As a rule of thumb, you should only rely on constructor injection, because using test doubles in your unit testsinstead of your real dependencies becomes much easier this way. Property injection can be acceptable for those classesthat aren't unit tested. I prefer this type of injection in my controllers, but nowhere else.

Building the container

Zen is a compiled DI Container which means that every time you update a dependency of a class, you have to recompilethe container in order for it to reflect the changes. This is a major weakness of compiled containers during development,but that's why Zen also offers a dynamic container implementation which is introducedlater.

Compilation is possible by running the following command from your project's root directory:

$ ./vendor/bin/zen build CONTAINER_PATH COMPILER_CONFIG_CLASS_NAME

Please make sure you escape theCOMPILER_CONFIG_CLASS_NAME argument when using namespaces, like below:

./vendor/bin/zen build /var/www/app/Container/Container.php"App\\Container\\CompilerConfig"

This results in a new fileCONTAINER_PATH (e.g.: "/var/www/app/Container/Container.php") which can be directlyinstantiated (assuming autoloading is properly set up) in your project. No other configuration is needed during runtimeby default.

$container =newContainer();

In case of very big projects, you might run out of memory when building the container. You can circumvent this issue by manuallysetting the memory limit:

./vendor/bin/zen --memory-limit="128M" build /var/www/app/Container/Container.php"App\\Container\\CompilerConfig"

Besides via the CLI, you can also build the Container via PHP itself:

$builder =newFileSystemContainerBuilder(newCompilerConfig(),"/var/www/src/Container/CompiledContainer.php");$builder->build();

It's up to you where you generate the container but please be aware that file system speed can affect the time consumptionof the compilation as well as the performance of your application. On the other hand, it's much more convenient to putthe container in a place where it is easily reachable as you might occasionally need to debug it.

Configuring the compiler

What about theCOMPILER_CONFIG_CLASS_NAME argument? This must be the fully qualified name of a class which extendsAbstractCompilerConfig. Let's see anexample!

class CompilerConfigextends AbstractCompilerConfig{publicfunctiongetContainerNamespace():string    {return"App\\Container";    }publicfunctiongetContainerClassName():string    {return"Container";    }publicfunctionuseConstructorInjection():bool    {returntrue;    }publicfunctionusePropertyInjection():bool    {returntrue;    }publicfunctiongetContainerConfigs():array    {return [newContainerConfig(),        ];    }}

By providing the prior configuration to the build command, anApp\Container\Container class will begenerated and the compiler will resolve constructor dependencies via type hinting and PHPDoc comments as well as propertydependencies marked by annotations.

Configuring the container

We only mentioned so far how to configure the compiler, but we haven't talked about container configuration. This canbe done by returning an array ofAbstractContainerConfig child instances in thegetContainerConfigs()method of the compiler config. Let's see an examplefor the container configuration too:

class ContainerConfigextends AbstractContainerConfig{protectedfunctiongetEntryPoints():array    {return [// Define all classes in a PSR-4 namespace as Entry Points            Psr4NamespaceEntryPoint::singleton('WoohooLabs\Zen\Examples\Controller'),// Define all classes in a directory as Entry Points            WildcardEntryPoint::singleton(__DIR__ ."/Controller"),// Define a class as Entry Point            ClassEntryPoint::singleton(UserController::class),        ];    }protectedfunctiongetDefinitionHints():array    {return [// Bind the Container class to the ContainerInterface (Singleton scope by default)            ContainerInterface::class => Container::class,// Bind the Request class to the RequestInterface (Prototype scope)            RequestInterface::class => DefinitionHint::prototype(Request::class),// Bind the Response class to the ResponseInterface (Singleton scope)            ResponseInterface::class => DefinitionHint::singleton(Response::class),        ];    }protectedfunctiongetWildcardHints():array    {return [// Bind all classes in the specified PSR-4 namespaces to each other based on patternsnewPsr4WildcardHint('WoohooLabs\Zen\Examples\Domain\*RepositoryInterface','WoohooLabs\Zen\Examples\Infrastructure\Mysql*Repository'            ),// Bind all classes in the specified directories to each other based on patternsnewWildcardHint(__DIR__ ."/Domain",'WoohooLabs\Zen\Examples\Domain\*RepositoryInterface','WoohooLabs\Zen\Examples\Infrastructure\Mysql*Repository'            ),        ];    }}

Configuring the container consist of the following two things: defining your Entry Points (in thegetEntryPoints()method) and passing Hints for the compiler (via thegetDefinitionHints() andgetWildcardHints() methods).

Entry Points

Entry Points are such classes that are to be directly retrieved from the DI Container (for instance Controllers andMiddleware usually fall in this category). This means that you canonly fetch Entry Points from the Container withthe$container->get() method and nothing else.

Entry Points are not only special because of this, but also because their dependencies are automatically discovered duringthe compilation phase resulting in the full object graph (this feature is usually called as "autowiring").

The following example shows a configuration which instructs the compiler to recursively search for all classes in theController directory and discover all of their dependencies. Please note that only concrete classes are included by default,and detection is done recursively.

protectedfunctiongetEntryPoints():array{return [newWildcardEntryPoint(__DIR__ ."/Controller"),    ];}

If you use PSR-4, there is a more convenient and performant way to define multiple Entry Points at once:

protectedfunctiongetEntryPoints():array{return [newPsr4NamespaceEntryPoint('Src\Controller'),    ];}

This way, you can define all classes in a specific PSR-4 namespace as Entry Point. Please note that only concreteclasses are included by default and detection is done recursively.

Last but not least, you are able to define Entry Points individually too:

protectedfunctiongetEntryPoints():array{return [newClassEntryPoint(UserController::class),    ];}

Hints

Hints tell the compiler how to properly resolve a dependency. This can be necessary when you depend on aninterface or an abstract class because they are obviously not instantiatable. With hints, you are able to bindimplementations to your interfaces or concretions to your abstract classes. The following example binds theContainer class toContainerInterface (in fact, you don't have to bind these two classes together, because thisvery configuration is automatically set during compilation).

protectedfunctiongetDefinitionHints():array{return [        ContainerInterface::class => Container::class,    ];}

Wildcard Hints can be used when you want to bind your classes in masses. Basically, they recursively search for all yourclasses in a directory specified by the first parameter, and bind those classes together which can be matched by theprovided patterns. The following example

protectedfunctiongetWildcardHints():array{return [newWildcardHint(__DIR__ ."/Domain",'WoohooLabs\Zen\Examples\Domain\*RepositoryInterface','WoohooLabs\Zen\Examples\Infrastructure\Mysql*Repository'        ),    ];}

will bind

UserRepositoryInterface toMysqlUserRepository.

If you use PSR-4, there is another - more convenient and performant - way to define the above settings:

protectedfunctiongetWildcardHints():array{return [newPsr4WildcardHint('WoohooLabs\Zen\Examples\Domain\*RepositoryInterface','WoohooLabs\Zen\Examples\Infrastructure\Mysql*Repository'        ),    ];}

This does exactly the same thing as whatWildcardHint did.

Note that currently, namespace detection is not recursive; you are only able to use the wildcard character in the class name part,but not in the namespace (soWoohooLabs\Zen\Examples\*\UserRepositoryInterface is invalid); and only* supported asa wildcard character.

Scopes

Zen is able to control the lifetime of your container entries via the notion of scopes. By default, all entries retrievedfrom the container haveSingleton scope, meaning that they are only instantiated at the first retrieval, and the sameinstance will be returned on the subsequent fetches.Singleton scope works well for stateless objects.

On the other hand, container entries ofPrototype scope are instantiated at every retrieval, so that is makes itpossible to store stateful objects in the container. You can hint a container entry asPrototype with theDefinitionHint::prototype() construct as follows:

protectedfunctiongetDefinitionHints():array{return [        ContainerInterface::class => DefinitionHint::prototype(Container::class),    ];}

You can useWildcardHint::prototype() to hint your Wildcard Hints the same way too.

Advanced Usage

Scalar injection

Scalar injection makes it possible to pass scalar values to an object in the form of constructor arguments or properties.As of v2.5, Zen supports scalar injection natively. You can useHints for this purpose as you can see in thefollowing example:

protectedfunction getDefinitionHints():array{return [        UserRepositoryInterface::class => DefinitionHint::singleton(MySqlUserRepository::class)            ->setParameter("mysqlUser","root")            ->setParameter("mysqlPassword","root"),            ->setParameter("mysqlPort",3306),            ->setProperty("mysqlModes", ["ONLY_FULL_GROUP_BY","STRICT_TRANS_TABLES","NO_ZERO_IN_DATE"]),    ];}

Here, we instructed the DI Container to pass MySQL connection details as constructor arguments to theMySqlUserRepositoryclass. Also, we initialized theMySqlUserRepository::$mysqlModes property with an array.

Alternatively, you can use the following technique to simulate scalar injection: extend the class whose constructor parameterscontain scalar types, and provide the arguments in question viaparent::__construct() in the constructor of the child class.Finally, add the appropriateHint to the container configuration so that the child class should be used instead ofthe parent class.

Context-dependent dependency injection

Sometimes - usually for bigger projects - it can be useful to be able to inject different implementations of the sameinterface as dependency. Before Zen 2.4.0, you couldn't achieve this unless you used some trick (like extending theoriginal interface and configuring the container accordingly). Now, context-dependent injection is supported out of thebox by Zen!

Imagine the following case:

class NewRelicHandlerimplements LoggerInterface {}class PhpConsoleHandlerimplements LoggerInterface {}class MailHandlerimplements LoggerInterface {}class ServiceA{publicfunction__construct(LoggerInterface$logger) {}}class ServiceB{publicfunction__construct(LoggerInterface$logger) {}}class ServiceC{publicfunction__construct(LoggerInterface$logger) {}}

If you would like to useNewRelicHandler inServiceA, butPhpConsoleHandler inServiceB andMailHandler in anyother classes (likeServiceC) then you have to configure thedefinition hints this way:

protectedfunction getDefinitionHints():array{return [        LoggerInterface::class => ContextDependentDefinitionHint::create()            ->setClassContext(                NewRelicHandler::class,                [                    ServiceA::class,                ]            ),            ->setClassContext(newDefinitionHint(PhpConsoleHandler::class),                [                    ServiceB::class,                ]            )            ->setDefaultClass(MailHandler::class),    ];}

The code above can be read the following way: when the classes listed in the second parameter of thesetClassContext() methodsdepend on the class/interface in the key of the specified array item (ServiceA depends onLoggerInterface in the example),then the class/definition hint in the first parameter will be resolved by the container. If any other class dependson it, then the class/definition hint in the first parameter of thesetDefaultClass() method will be resolved.

Note that if you don't set a default implementation (either via thesetDefaultClass() method or via constructor parameter)then aContainerException will be thrown if the interface is injected as a dependency of any class other than the listedones in the second parameter of thesetClassContext() method calls.

Generating a preload file

Preloading is afeature introduced in PHP 7.4 for optimizing performance by compilingPHP files and loading them into shared memory when PHP starts up using a dedicated preload file.

According to aninitial benchmark, the best speedupcan be achieved by only preloading the "hot" files: those ones which are used the most often. Another gotcha is that in orderfor preload to work, every class dependency (parent classes, interfaces, traits, property types, parameter types and return types)of a preloaded file must also be preloaded. It means, someone has to resolve these dependencies. And that's somethingZen can definitely do!

If you want to create a preload file, first, configure yourCompiler Configuration by addingthe following method:

publicfunctiongetPreloadConfig():PreloadConfigInterface{return PreloadConfig::create()        ->setPreloadedClasses(            [                Psr4NamespacePreload::create('WoohooLabs\Zen\Examples\Domain'),                ClassPreload::create('WoohooLabs\Zen\Examples\Utils\AnimalUtil'),            ]        )        ->setPreloadedFiles(            [__DIR__ ."/examples/Utils/UserUtil.php",            ]        );}

This configuration indicates that we want to preload the following:

  • All classes and all their dependencies in theWoohooLabs\Zen\Examples\Domain namespace
  • TheWoohooLabs\Zen\Examples\Utils\AnimalUtil class and all its dependencies
  • Theexamples/Utils/UserUtil.php file (dependency resolution isn't performed in case of files)

By default, the PHP files in the preload file will be referenced absolutely. However, if you provide a base path for thePreoadConfig (either via its constructor, or via thePreoadConfig::setRelativeBasePath() method), file references willbecome relative.

In order to create the preload file, you have two possibilities:

  1. Build the preload file along with the container:
./vendor/bin/zen --preload="/var/www/examples/preload.php" build /var/www/examples/Container.php"WoohooLabs\\Zen\\Examples\\CompilerConfig"

This way, first the container is created as/var/www/examples/Container.php, then the preload file as/var/www/examples/preload.php.

  1. Build the preload file separately:
./vendor/bin/zen preload /var/www/examples/preload.php"WoohooLabs\\Zen\\Examples\\CompilerConfig"

This way, only the preload file is created as/var/www/examples/Container.php.

File-based definitions

This is another optimization which wasinspired by Symfony: if you havehundreds or even thousands of entries in the compiled container, then you may be better off separating the contentof the container into different files.

There are two ways of enabling this feature:

publicfunctiongetFileBasedDefinitionConfig():FileBasedDefinitionConfigInterface{return FileBasedDefinitionConfig::enableGlobally("Definitions");}

This way, all definitions will be in separate files. Note that the first parameter in the example above is the directorywhere the definitions are generated, relative to the container itself. This directory is automatically deleted and createdduring compilation, so be cautious with it.

  • Selectively: You can choose which Entry Points are to be separated into different files.
protectedfunctiongetEntryPoints():array{return [        Psr4WildcardEntryPoint::create('Src\Controller')            ->fileBased(),        WildcardEntryPoint::create(__DIR__ ."/Controller")            ->fileBased(),        ClassEntryPoint::create(Class10::class)            ->disableFileBased(),    ];}

Dynamic container

You probably don't want to recompile the container all the time during development. That's where a dynamic containerhelps you:

$container =newRuntimeContainer(newCompilerConfig());

Note that the dynamic container is only suitable for development purposes because it is much slower than thecompiled one - however it is still faster than some of the most well-known DI Containers.

Examples

If you want to see how Zen works, have a look at theexamplesfolder, where you can find an example configuration (CompilerConfig). Ifdocker-compose andmake is availableon your system, then just run the following commands in order to build a container:

make composer-install# Install the Composer dependenciesmake build# Build the container into the examples/Container.php

Versioning

This library followsSemVer v2.0.0.

Change Log

Please seeCHANGELOG for more information what has changed recently.

Testing

Woohoo Labs. Zen has a PHPUnit test suite. To run the tests, run the following command from the project folder:

$ phpunit

Additionally, you may rundocker-compose up ormake test in order to execute the tests.

Contributing

Please seeCONTRIBUTING for details.

Support

Please seeSUPPORT for details.

Credits

License

The MIT License (MIT). Please see theLicense File for more information.

About

Woohoo Labs. Zen is a very fast and simple, PSR-11 compliant DI Container & preload file generator.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp