Code Monkey home page Code Monkey logo

di's Introduction

Yii

Yii Dependency Injection


Latest Stable Version Total Downloads Build status Scrutinizer Code Quality Code Coverage Mutation testing badge static analysis type-coverage

PSR-11 compatible dependency injection container that's able to instantiate and configure classes resolving dependencies.

Features

  • PSR-11 compatible.
  • Supports property injection, constructor injection and method injection.
  • Detects circular references.
  • Accepts array definitions. You can use it with mergeable configs.
  • Provides optional autoload fallback for classes without explicit definition.
  • Allows delegated lookup and has a composite container.
  • Supports aliasing.
  • Supports service providers.
  • Has state resetter for long-running workers serving many requests, such as RoadRunner or Swoole.
  • Supports container delegates.

Requirements

  • PHP 8.0 or higher.
  • Multibyte String PHP extension.

Installation

You could install the package with composer:

composer require yiisoft/di

Using the container

Usage of the DI container is simple: You first initialize it with an array of definitions. The array keys are usually interface names. It will then use these definitions to create an object whenever the application requests that type. This happens, for example, when fetching a type directly from the container somewhere in the application. But objects are also created implicitly if a definition has a dependency to another definition.

Usually one uses a single container for the whole application. It's often configured either in the entry script such as index.php or a configuration file:

use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withDefinitions($definitions);

$container = new Container($config);

You could store the definitions in a .php file that returns an array:

return [
    EngineInterface::class => EngineMarkOne::class,
    'full_definition' => [
        'class' => EngineMarkOne::class,
        '__construct()' => [42], 
        '$propertyName' => 'value',
        'setX()' => [42],
    ],
    'closure' => fn (SomeFactory $factory) => $factory->create('args'),
    'static_call_preferred' => fn () => MyFactory::create('args'),
    'static_call_supported' => [MyFactory::class, 'create'],
    'object' => new MyClass(),
];

You can define an object in several ways:

  • In the simple case, an interface definition maps an id to a particular class.
  • A full definition describes how to instantiate a class in more detail:
    • class has the name of the class to instantiate.
    • __construct() holds an array of constructor arguments.
    • The rest of the config are property values (prefixed with $) and method calls, postfixed with (). They're set/called in the order they appear in the array.
  • Closures are useful if instantiation is tricky and can better done in code. When using these, arguments are auto-wired by type. ContainerInterface could be used to get current container instance.
  • If it's even more complicated, it's a good idea to move such a code into a factory and reference it as a static call.
  • While it's usually not a good idea, you can also set an already instantiated object into the container.

See yiisoft/definitions for more information.

After you configure the container, you can obtain a service can via get():

/** @var \Yiisoft\Di\Container $container */
$object = $container->get('interface_name');

Note, however, that it's bad practice using a container directly. It's much better to rely on auto-wiring as provided by the Injector available from the yiisoft/injector package.

Using aliases

The DI container supports aliases via the Yiisoft\Definitions\Reference class. This way you can retrieve objects by a more handy name:

use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withDefinitions([
        EngineInterface::class => EngineMarkOne::class,
        'engine_one' => EngineInterface::class,
    ]);

$container = new Container($config);
$object = $container->get('engine_one');

Composite containers

A composite container combines many containers in a single container. When using this approach, you should fetch objects only from the composite container.

use Yiisoft\Di\CompositeContainer;
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$composite = new CompositeContainer();

$carConfig = ContainerConfig::create()
    ->withDefinitions([
        EngineInterface::class => EngineMarkOne::class,
        CarInterface::class => Car::class
    ]);
$carContainer = new Container($carConfig);

$bikeConfig = ContainerConfig::create()
    ->withDefinitions([
        BikeInterface::class => Bike::class
    ]);

$bikeContainer = new Container($bikeConfig);
$composite->attach($carContainer);
$composite->attach($bikeContainer);

// Returns an instance of a `Car` class.
$car = $composite->get(CarInterface::class);
// Returns an instance of a `Bike` class.
$bike = $composite->get(BikeInterface::class);

Note, that containers attached earlier override dependencies of containers attached later.

use Yiisoft\Di\CompositeContainer;
use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$carConfig = ContainerConfig::create()
    ->withDefinitions([
        EngineInterface::class => EngineMarkOne::class,
        CarInterface::class => Car::class
    ]);

$carContainer = new Container($carConfig);

$composite = new CompositeContainer();
$composite->attach($carContainer);

// Returns an instance of a `Car` class.
$car = $composite->get(CarInterface::class);
// Returns an instance of a `EngineMarkOne` class.
$engine = $car->getEngine();

$engineConfig = ContainerConfig::create()
    ->withDefinitions([
        EngineInterface::class => EngineMarkTwo::class,
    ]);

$engineContainer = new Container($engineConfig);

$composite = new CompositeContainer();
$composite->attach($engineContainer);
$composite->attach($carContainer);

// Returns an instance of a `Car` class.
$car = $composite->get(CarInterface::class);
// Returns an instance of a `EngineMarkTwo` class.
$engine = $composite->get(EngineInterface::class);

Using service providers

A service provider is a special class that's responsible for providing complex services or groups of dependencies for the container and extensions of existing services.

A provider should extend from Yiisoft\Di\ServiceProviderInterface and must contain a getDefinitions() and getExtensions() methods. It should only provide services for the container and therefore should only contain code that's related to this task. It should never implement any business logic or other functionality such as environment bootstrap or applying changes to a database.

A typical service provider could look like:

use Yiisoft\Di\Container;
use Yiisoft\Di\ServiceProviderInterface;

class CarFactoryProvider extends ServiceProviderInterface
{
    public function getDefinitions(): array
    {
        return [
            CarFactory::class => [
                'class' => CarFactory::class,
                '$color' => 'red',
            ], 
            EngineInterface::class => SolarEngine::class,
            WheelInterface::class => [
                'class' => Wheel::class,
                '$color' => 'black',
            ],
            CarInterface::class => [
                'class' => BMW::class,
                '$model' => 'X5',
            ],
        ];    
    }
     
    public function getExtensions(): array
    {
        return [
            // Note that Garage should already be defined in a container 
            Garage::class => function(ContainerInterface $container, Garage $garage) {
                $car = $container
                    ->get(CarFactory::class)
                    ->create();
                $garage->setCar($car);
                
                return $garage;
            }
        ];
    } 
}

Here you created a service provider responsible for bootstrapping of a car factory with all its dependencies.

An extension is callable that returns a modified service object. In this case you get existing Garage service and put a car into the garage by calling the method setCar(). Thus, before applying this provider, you had an empty garage and with the help of the extension you fill it.

To add this service provider to a container, you can pass either its class or a configuration array in the extra config:

use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withProviders([CarFactoryProvider::class]);

$container = new Container($config);

When you add a service provider, DI calls its getDefinitions() and getExtensions() methods immediately and both services and their extensions get registered into the container.

Container tags

You can tag services in the following way:

use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withDefinitions([  
        BlueCarService::class => [
            'class' => BlueCarService::class,
            'tags' => ['car'], 
        ],
        RedCarService::class => [
            'definition' => fn () => new RedCarService(),
            'tags' => ['car'],
        ],
    ]);

$container = new Container($config);

Now you can get tagged services from the container in the following way:

$container->get(\Yiisoft\Di\Reference\TagReference::to('car'));

The result is an array that has two instances: BlueCarService and RedCarService.

Another way to tag services is setting tags via container constructor:

use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withDefinitions([  
        BlueCarService::class => [
            'class' => BlueCarService::class,
        ],
        RedCarService::class => fn () => new RedCarService(),
    ])
    ->withTags([
        // "car" tag has references to both blue and red cars
        'car' => [BlueCarService::class, RedCarService::class]
    ]);

$container = new Container($config);

Resetting services state

Despite stateful services isn't a great practice, these are often inevitable. When you build long-running applications with tools like Swoole or RoadRunner you should reset the state of such services every request. For this purpose you can use StateResetter with resetters callbacks:

$resetter = new StateResetter($container);
$resetter->setResetters([
    MyServiceInterface::class => function () {
        $this->reset(); // a method of MyServiceInterface
    },
]);

The callback has access to the private and protected properties of the service instance, so you can set initial state of the service efficiently without creating a new instance.

You should trigger the reset itself after each request-response cycle. For RoadRunner, it would look like the following:

while ($request = $psr7->acceptRequest()) {
    $response = $application->handle($request);
    $psr7->respond($response);
    $application->afterEmit($response);
    $container
        ->get(\Yiisoft\Di\StateResetter::class)
        ->reset();
    gc_collect_cycles();
}

Setting resetters in definitions

You define the reset state for each service by providing "reset" callback in the following way:

use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withDefinitions([
        EngineInterface::class => EngineMarkOne::class,
        EngineMarkOne::class => [
            'class' => EngineMarkOne::class,
            'setNumber()' => [42],
            'reset' => function () {
                $this->number = 42;
            },
        ],
    ]);

$container = new Container($config);

Note: resetters from definitions work only if you don't set StateResetter in definition or service providers.

Configuring StateResetter manually

To manually add resetters or in case you use Yii DI composite container with a third party container that doesn't support state reset natively, you could configure state resetter separately. The following example is PHP-DI:

MyServiceInterface::class => function () {
    // ...
},
StateResetter::class => function (ContainerInterface $container) {
    $resetter = new StateResetter($container);
    $resetter->setResetters([
        MyServiceInterface::class => function () {
            $this->reset(); // a method of MyServiceInterface
        },
    ]);
    return $resetter;
}

Specifying metadata for non-array definitions

To specify some metadata, such as in cases of "resetting services state" or "container tags," for non-array definitions, you could use the following syntax:

LogTarget::class => [
    'definition' => static function (LoggerInterface $logger) use ($params) {
        $target = ...
        return $target;
    },
    'reset' => function () use ($params) {
        ...
    },
],

Now you've explicitly moved the definition itself to "definition" key.

Delegates

Each delegate is a callable returning a container instance that's used in case DI can't find a service in a primary container:

function (ContainerInterface $container): ContainerInterface
{

}

To configure delegates use extra config:

use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withDelegates([
        function (ContainerInterface $container): ContainerInterface {
            // ...
        }
    ]);


$container = new Container($config);

Tuning for production

By default, the container validates definitions right when they're set. In the production environment, it makes sense to turn it off:

use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withValidate(false);

$container = new Container($config);

Strict mode

Container may work in a strict mode, that's when you should define everything in the container explicitly. To turn it on, use the following code:

use Yiisoft\Di\Container;
use Yiisoft\Di\ContainerConfig;

$config = ContainerConfig::create()
    ->withStrictMode(true);

$container = new Container($config);

Documentation

If you need help or have a question, the Yii Forum is a good place for that. You may also check out other Yii Community Resources.

License

The Yii Dependency Injection is free software. It is released under the terms of the BSD License. Please see LICENSE for more information.

Maintained by Yii Software.

Support the project

Open Collective

Follow updates

Official website Twitter Telegram Facebook Slack

di's People

Contributors

arhell avatar dependabot-preview[bot] avatar dependabot[bot] avatar devanych avatar fcaldarelli avatar hiqsol avatar klimov-paul avatar luizcmarin avatar machour avatar mikehaertl avatar mj4444ru avatar nex-otaku avatar prowwid avatar razonyang avatar rugabarbo avatar rustamwin avatar samdark avatar sammousa avatar sankaest avatar scrutinizer-auto-fixer avatar silverfire avatar stylecibot avatar terabytesoftw avatar thenotsoft avatar viktorprogger avatar vjik avatar vuongxuongminh avatar xepozz avatar yiiliveext avatar zhukovra avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

di's Issues

Optional reference

I'm working on a PR for this.

Our DI container / injector / factory should support optional dependencies:

$function = function(?OptionalDependency $dependency) {
...
}

In this scenario if we can instantiate the dependency we should, but if we cannot we should pass null.

Remove Initiable

Initiable should be removed as it's a Yii-specific behavior that we don't actually want in the Yii itself.

Typo in interface name

Hi,

Your lib is a really good package. I spoted a little typo aftezr the name change :

ServiceProviderInterace instead of ServiceProviderInterface

Singletons

Does this implementation support singletons declarations? (Like yii2 di)

Add `Reference::to()`

class Reference
{
    public static function to($id)
    {
	return new self($id);
    }
}

If you're ok with the idea I will make PR.

Incorrect work of FactoryProvider->has()

README.MD: # Using deferred service providers [link]

// returns false as provider wasn't registered
$container->has(EngineInterface::class); 

// returns SolarEngine, registered in the provider
$engine = $container->get(EngineInterface::class); 

// returns true as provider was registered when EngineInterface was requested from the container
$container->has(EngineInterface::class); 

PSR about has():

1.1.2 Reading from a container
has takes one unique parameter: an entry identifier, which MUST be a string. has MUST return true if an entry identifier is known to the container and false if it is not. If has($id) returns false, get($id) MUST throw a NotFoundExceptionInterface.

3.1. Psr\Container\ContainerInterface

    /**
     * Returns true if the container can return an entry for the given identifier.
     * Returns false otherwise.
     *
     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
     *
     * @param string $id Identifier of the entry to look for.
     *
     * @return bool
     */
    public function has($id);

Method get() must returns true if the container can return an entry for the given identifier.
In our case, it returns false until the first call to get()

Introduce DependencyResolver

Currently dependency resolution is done in AbstractContainer and in Injector.

Instead we should create a DependencyResolverInterface (and implementation):

interface DependencyResolverInterface {
    /**
     * @return Reference[]
     */
    public function resolveConstructor(string $class): array;

    /**
     * @return Reference[]
     */
    public function resolveCallable(callable $callable): array;

    /**
     * This is just an example, it doesn't add anything except syntax over resolveCallable.
     * @return Reference[]
     */
    public function resolveMethod($subject, string $method): array;

}

This approach will allow us to make the DI simpler to understand and simpler to test.

Exception: Serialization of 'Closure' is not allowed

What steps will reproduce the problem?

Run the tests for yiisoft/yii-swiftmailer

What is the expected result?

Tests to succeed

What do you get instead?

1) yiiunit\swiftmailer\MessageTest::testSerialize
Exception: Serialization of 'Closure' is not allowed

/Users/didou/yii3/all-packages/yii-swiftmailer-mine/tests/MessageTest.php:451
/Users/didou/yii3/all-packages/yii-swiftmailer-mine/vendor/phpunit/phpunit/phpunit:61

ERRORS!
Tests: 24, Assertions: 75, Errors: 1.

Additional info

I faced this in yiisoft/mailer-swiftmailer#4 and came to the conclusion that the problem was in yii-core/config/common.php where container is declared as a closure, making it impossible for any object embedding these definitions to be serialized.

Related issue in yiisoft/yii-swiftmailer : yiisoft/mailer-swiftmailer#7

[WIP] Proposal: DI configuration for application

This is a proposal for how we could configure DI in a Yii application.

$composite = new \yii\di\CompositeContextContainer();

// Create a container using a 3rd party library as well. This container should support delegated lookup
$thirdParty = new ThirdPartyContainer($thirdPartyConfig, $composite);
$composite->attach($thirdParty);

$appConfig = [
    'id' => 'app1',
    'di' => [
        EngineInterface::class => EngineMarkOne::class,
        'specificEngine' => EngineMarkTwo::class,
        ServiceXInterface::class => ConcreteX::class
    ],
    'components' => [
        // Expose a service from the DI container as a component so it can be used in the service locator pattern. This is not a recommended pattern.
        'serviceX' => Reference::to(ServiceXInterface::class),
        'carFactory' => [
            '__class' => CarFactory::class,
            '__construct()' => [
                Reference::to('specificEngine')
            ]
        ]
    ],
    'modules' => [
        'moduleA' => [
            '__class' => A\Module::class,
            'di' => [
                'specificEngine' => EngineMarkOne::class
            ],
            'components' => [
                'carFactory' => [
                    '__class' => CarFactory::class,
                    '__construct()' => [
                        Reference::to('specificEngine')
                    ]
                ]
            ]

        ]

    ]
];

// If not provided the application could instantiate its own composite container.
$app = new Application($appConfig, $composite);

The module then interprets the configuration;

// This is all pseudo-code / a PoC implementation
/**
 * @param string $uniqueId The full path to the module, for example `/moduleA/moduleB`.
 * @param mixed $config
 */
private function configure(string $uniqueId = '', array $config, CompositeContextContainer $root) {
    // Used for retrieval
    $this->container = $root;

    $container = new Container($appConfig['di'], $composite);
    $composite->attach($container);

    $moduleContainer = new Container($config['modules'], $composite);

    // This is so that our modules will get the correct root container injected.
    $moduleContainer->set(CompositeContextContainer::class, $root);
    $composite->attach($moduleContainer, $uniqueId . '/_modules');
    $this->componentContainer = new Container($config['components'], $composite);
    $composite->attach($this->componentContainer, $uniqueId . '/_components')
}

public function getModule(string $id) 
{
    return $this->container->getFromContext($id, $this->uniqueId . '/_modules');
}

public function getComponent(string $id) 
{
    return $this->container->getFromContext($id, $this->uniqueId . '/);
}

public function setComponent(string $id, $config) 
{
    $this->componentContainer->set($id, $config);
}

This will allow us to have module specific DI configurations; provide default configurations from extensions.
As in Yii2 the getters on module essentially implement the service locator pattern when used directly. In this case we use the DI container to back them.

Proposal: Way to use global

What steps will reproduce the problem?

Getting container globally

What is the expected result?

this is useful for functions
e.g: Container::getInstance()

What do you get instead?

Additional info

Q A
Version 1.0.?
PHP version
Operating system

[Feature Proposal] Add Service Providers

Laravel has a good feature called Service Providers.

Service providers give the ability to register complex dependencies in one place. Another good feature of service providers is deferring registration of dependencies to the time they are called.

I have implemented simple realization of service providers in my big ball of mud called yii2-di and can start working on implementing Service Providers in this package.

I suggest to have following imterface of service providers:

/**
 * Represents a component responsible for services registration in DI container.
 * The goal of service providers is to centralize and organize in one place
 * registration of services bound by any logic or services with complex dependencies.
 * For example you can have a service that requires several dependencies and those dependencies
 * also have their dependencies. You can simply organize registration of service and it's dependencies
 * in a single provider class except creating bootstrap file or configuration array for container. Pseudocode might look
 * like:
 * <pre>
 * class MyServiceProvider implements ServiceProvider {
 *    public function register() {
 *        $this->registerDependencies();
 *        $this->registerService();
 *    }
 *
 *    protected function registerDependencies() {
 *        $container = $this->container;
 *        $container->set('dependency1', SomeClass1::class);
 *        $container->set('dependency2', SomeClass2::class);
 *        $container->set('dependency3', [
 *            'class' => SomeClass3::class,
 *            'dependency' => $container->get('dependency1')
 *        ]);
 *        $container->set('dependency4', [
 *            'class' => SomeClass4::class,
 *            'dependency' => $container->get('dependency2')
 *        ]);
 *    }
 *
 *    protected function registerService() {
 *        $this->container->set('myService', function(DiContainer $container) {
 *            return $container->create([
 *                'class' => MyService::class,
 *                'dependency' => $container->get('dependency3'),
 *                'dependency2' => $container->get('dependency4'),
 *            ]);
 *        });
 *    }
 * }
 * </pre>
 *
 * @author Dmitry Kolodko <[email protected]>
 */
interface ServiceProvider {
    public function register();
}

and for service providers that should be delayed:

/**
 * Service provider that should be delayed to register till services are
 * actually required.
 *
 * @author Dmitry Kolodko <[email protected]>
 */
interface DelayedServiceProvider extends ServiceProvider {
    public function provides($classOrInterface);
}

Service Providers have a good Laravel documentation but if you would like to, I can write down more info and examples about Service Providers.

Automatic constructor dependencies

Currently DI supports specifically setting constructor arguments via the following syntax:

return [
    'full_definition' => [
        '__class' => EngineMarkOne::class,
        '__construct()' => [42], 
        'argName' => 'value',
        'setX()' => [42],
    ],
];

Consider the following scenario. I have a cache component.

class CoolCache {
    public function __construct(CacheInterface $handler) { ... }
}

return [
    'my_cool_cache' => [
        '__class' => CoolCache::class
    ]
];

The DI container will require us to define a concrete class for the CacheInterface, otherwise it cannot instantiate my cool cache component. Currently we could specify this as a constructor argument, but this is very cumbersome.
Alternatively we specify it globally:

return [
    'my_cool_cache' => [
        '__class' => CoolCache::class
    ],
    CacheInterface::class => SomeCacheHandler::class
];

This works great, the DI container will figure it out.

Now consider a more advanced situation where I have 2 caches, and I want them to use different handlers:

return [
    'my_cool_cache' => [
        '__class' => CoolCache::class
    ],
    'another_cool_cache' => [
        '__class' => CoolCache::class
    ],
    CacheInterface::class => SomeCacheHandler::class
];

We have a problem, we either cannot specify what we want, or we need to specify it too explicitly.

My proposal is add support for contextual configuration of the DI container:

return [
    'my_cool_cache' => [
        '__class' => CoolCache::class
    ],

    'another_cool_cache' => [
        '__class' => CoolCache::class

        '__overrides' => [
            CacheInterface::class => AnotherCacheHandler::class

        ]
    ],
    CacheInterface::class => SomeCacheHandler::class
];

The idea is simple yet powerful.
Basically when resolving my_cool_cache, we first load all overrides.
Then we proceed as normal.
Then we revert the overrides.

__overrides has the exact same syntax as the configuration array of the DI container.

Benefits:
- No need to specify argument names explicitly, we're just specifying how dependencies should be resolved IF they are needed.
- The implementing class can properly use constructor injection.
- The implementing class no longer needs to know about DI at all.

`has()` inconsistency with PSR for deferred service provider

What steps will reproduce the problem?

Code example from README Using deferred service providers:

$container->addProvider(CarFactoryProvider::class);

// returns false as provider wasn't registered
$container->has(EngineInterface::class); 

// returns SolarEngine, registered in the provider
$engine = $container->get(EngineInterface::class);

What is the expected result?

has() should return true

What do you get instead?

false

According to PSR-11:

If has($id) returns false, get($id) MUST throw a NotFoundExceptionInterface

I think has() should use provides() from deferred service provider to return proper result.

Fall back to autoloader when class is not in container

Currently there's a need to declare everything that is repetitive if we need to set a single class without an interface into container:

\App\ConsoleCommand\CreateUser::class => \App\ConsoleCommand\CreateUser::class,

In such cases we can try to fall back to class autoloading.

[Organization Proposal] Change namespaces to match PSR-4 and common style

In this comment @samdark said that this container isn't Yii-specific.

So, as I get it, this container should be framework-independent and aimed to be used by developers from different communities.

And this leads to a question: why namespace naming is Yii-specific?

Other first-class frameworks and libraries use upper camel case notation for namespaces so I suggest using upper camel case notation for namespaces in this repository too.

Example:
Yii\DI\Conatiner

Factories

One goal of a factory is to create a concrete implementation of an interface without knowing anything about the implementation.

For example, an application might want to create PSR7 responses without knowing anything about the underlying implementation in use.
This means that the create method of a factory should look roughly like this:

public function create($a, $b): SomeInterface;

The factory abstracts the how; we do not know if it passes $a and $b to a constructor, or uses setters, or even public properties.
The current implementation is not really a factory, but a wrapper around PHP __construct. All it does is provide a different syntax, while still binding to the concrete implementation.

Alternatively, the goal of a factory might be to hide the fact that the class it's creating has dependencies that we want to automatically inject.
This is the following pattern:

public function __construct(DepA $dependencyA) {
    $concretes = [];
    for ($i = 1; $i < 10; $i++) {
        $concretes[] = new SomeClass($dependencyA);
    }

}

Here our code is depending on a concrete implementation that it needs to instantiate.
A factory would allow us to clarify that our class doesn't directly need DepA, instead it just happens to use a class that needs it.
In this case, since we know the concrete class, it makes sense for the factory to:

  • Have a create function that supports all constructor parameters of the underlying class.
  • Have a createX function for all static constructors.

Regardless of which of these 2 cases is used, factories should only create instances of a single class or interface.

In Yii we have the convention to use array configuration and it might make sense to use similar configuration for the factory as we do for the DI container:

$factoryConfig = [
    Car::class => [
        '__construct()' => [
            EngineInterface::class => EngineMarkTwo::class
        ]
];

While this allows us to create one big factory for every configured (and even unconfigured?) class this isn't really the type of factory we should be injecting.

Instead we could create a FactoryFactory that, on demand, creates a factory for a class.
This could be done fully automated but this will require a lot of magic; either by creating classes on the fly using autoloading or by looking at variable names and deciding what to inject.

public function __construct(FactoryInterface $carFactory) {

}

The downside is that the interface will always be incomplete; it can't include multiple constructors and it can't provide the correct constructor arguments.

For the Yii core, there's just a few classes that we construct in this way, and in my opinion we should implement specific factories for those instead ActiveRecordFactory.
For all subclasses of ActiveRecord we know how to configure them and we can easily provide defaults. Note that currently we already have an AR specific factory in the form of: BaseActiveRecord::instantiate(), which is a factory.

@hiqsol I'm pinging you since you laid the groundwork for this library and I'd love your thoughts the reasoning behind the choices you made for the current implementation.

I'd like to ask everyone to state what their requirements / use cases are for the factory as part of a DI library.

Add `Container::create()` method

At the present state Container is almost useless in Yii Framework scope. It is unable to replace old Container from Yii 2.0.x.
In particular there is no way to create an object from array definition taking into account possible singleton definitions or predefined configurations.

Previously this task was implemented inside Container::get() method, which accepted constructor arguments and object config as additional parameters:

$foo = $container->get(Foo::class, ['constructor argument'], ['someField' => 'some-value']);

Accepting PSR for the container makes impossible to use get() for this purpose.

I suggest a separated method create() (or createObject()) should be introduced to incapsulate former object creation logic.
Method signature:

public function create(array $definition) : object

This method should check for registered definitions and use Container::build() to create final object.

Lazy services support

Discussion was started in: #57 (comment)

I'd not want the proxy manager as a hard dependency of DI, but I definitely would like to support it

It could be suggested in composer.json and then something like if class_exists then it works else silently works without it.

Syntax could be something like this:

    \my\ServiceInterface::class => [
        '__class' => \my\Service::class,
        '__lazy' => true,
    ],

Reference constructor

Currently Reference has 2 constructors, 1 static one and 1 non-static one.

I imagine the static one was added mostly for syntactic sugar:

$container->get(Reference::to('someService')); 
// vs
$container->get(new Reference('someService'));

If we decide to support this syntax then in my opinion we should make the __construct() constructor private.

Right way to create object

I need that object init() method is called, because it implements Initiable.

But I have found that init() method is called only if object is created with DI.

So I can create my \yii\web\UrlManager object in this way;

$manager = new UrlManager( $this->app, new UrlNormalizer() );

but init() method is not called.

To be sure that init() method is called, I have to write:

$manager = $this->app->createObject(['__class' => UrlManager::class], ['app' => $this->app, 'normalizer' => new UrlNormalizer() );

but this way to create object is very ugly and little intuitive.

Is there a more elegant way to create an object and ensure that init() method is called ?

If there are not other options to create an object, maybe it would be better that BaseObject had an its constructor that checked if child class implemented Initiable interface and finally call child init() method.

Drop aliases in favour of Reference

Will allow define aliases in config and it seems to me that implementation might get even simpler.

$container = new Container([
    EngineInterface::class => EngineMarkOne::class,
    'engine_one' => \yii\di\Reference::to(EngineInterface::class),
];

I am trying to modify the init method to the __construct method

$object = $reflection->newInstanceArgs($dependencies);

return static::configure($object, $config);

Because the assignment of the attribute is after __construct()
This will cause the settings in the __construct() to be used in the configuration file that cannot be applied to the object.
E.g

    public $db = 'db';
    public function __construct()
    {
        $this->db = Yii::ensureObject($this->db, Connection::class);
    }

* @deprecated Not recommended for use. Added only to support Yii 2.0 behavior.

Is there any plan for the development team?

Make factory not inherit from container

  1. Standalone factory can use composition to work with container i.e. require container instance in its constructor and then use PSR interface get() to obtain dependencies.
  2. Factory could be then extracted into yiisoft/factory. Then container would be truly interchangeable.

Closure wrongly resolving as a dependency

It seems that commit a9dff7f broke using closures as parameters to objects.

cc @hiqsol

What steps will reproduce the problem?

$dp = new \yii\data\ArrayDataProvider();
$dp->allModels = [
    ['id' => 1, 'name' => 'Item 1'],
    ['id' => 2, 'name' => 'Item 2'],
];

echo \yii\dataview\GridView::widget([
    'dataProvider' => $dp,
    'rowOptions' => function ($model) {
        return ['class' => 'foo-' . $model['id']];

    },
]);

What is the expected result?

For the grid view to have additional classes on rows.

What do you get instead?

Error: Cannot use object of type yii\di\Container as array in /Users/didou/yii3/myapp/src/views/site/index.php:25

Additional info

Printing the backtrace, I think that there's something wrong going on with the dependency injection during the widget initialization:

#0  yii\view\View->{closure}()
#1  call_user_func() called at [/Users/didou/yii3/myapp/vendor/yiisoft/di/src/AbstractContainer.php:464]
#2  yii\di\AbstractContainer->resolveDependencies() called at [/Users/didou/yii3/myapp/vendor/yiisoft/di/src/AbstractContainer.php:357]
#3  yii\di\AbstractContainer->buildFromConfig() called at [/Users/didou/yii3/myapp/vendor/yiisoft/di/src/AbstractContainer.php:163]
#4  yii\di\AbstractContainer->buildWithoutDefinition() called at [/Users/didou/yii3/myapp/vendor/yiisoft/di/src/AbstractContainer.php:132]
#5  yii\di\AbstractContainer->build() called at [/Users/didou/yii3/myapp/vendor/yiisoft/di/src/AbstractContainer.php:158]
#6  yii\di\AbstractContainer->buildWithoutDefinition() called at [/Users/didou/yii3/myapp/vendor/yiisoft/di/src/AbstractContainer.php:132]
#7  yii\di\AbstractContainer->build() called at [/Users/didou/yii3/myapp/vendor/yiisoft/di/src/Factory.php:48]
#8  yii\di\Factory->create() called at [/Users/didou/yii3/myapp/vendor/yiisoft/yii-core/src/helpers/BaseYii.php:69]
#9  yii\helpers\BaseYii::createObject() called at [/Users/didou/yii3/myapp/vendor/yiisoft/view/src/widgets/Widget.php:132]
#10 yii\widgets\Widget::widget() called at [/Users/didou/yii3/myapp/src/views/site/index.php:31]
#11 require(/Users/didou/yii3/myapp/src/views/site/index.php) called at [/Users/didou/yii3/myapp/vendor/yiisoft/view/src/view/View.php:363]

the provided callback, wrongly identified as a dependency, ends up being called from AbstractContainer line 459, and $model is passed the AbstractContainer instance.

Q A
Yii DI version 3.0
PHP version 7.1, 7.3
Operating system macOS Darwin Kernel Version 18.2.0

Refactor configuration array

At the moment we have configuration array like this:

    $config = [
        '__class' => MyClass::class,
        '__construct()' => [
            'param1' => 'value1',
            'param2' => 'value2',
        ],
        'prop' => 'value',
        'aMethod()' => ['arg1', 'arg2'],
    ];

We could redo it this way:

    $config = [
        '__class' => MyClass::class,
        'param1' => 'value1',
        'param2' => 'value2',
        '__configure' => [
            'prop' => 'value',
            'aMethod()' => ['arg1', 'arg2'],
        ],
    ];

The convention will be throw away all items starting with '__' and use everything else as constructor params (to allow adding other special configuration options later).

This way is more convenient for classes with normal constructors which is what we aim to.
I have actually working big application switched to Yii 3 and in practice, there are more constructor parameters then properties configuration.

Provide `ServiceLocatorTrait`

According to the PSR recommendations instance of Psr\Container\ContainerInterface should be used as 'Service Locator'. At its present state current yii\di\Container behaves exacly like a 'service locator' for me instead of DI container.

Obviously accepting PSR-11 demands old yii\di\ServiceLocator to match Psr\Container\ContainerInterface. I suggets we may implement ServiceLocatorTrait inside this package providing general implementation for it to be used back at the core.

Interface should not have a constructor

Currently ServiceProviderInterface has 2 functions:

    public function __construct(ContainerInterface $container);
    public function register(): void;

Interfaces generally should not have constructors.
In this case I propose changing it to this:

public function register(ContainerInterface $container): void;

This has several advantages:

  • I could create the container after the service provider.
  • I could serialize my service provider.

I don't have any need for these advantages, but I think the code is cleaner and more correct this way.

Idea how to reproduce Yii 2.0 container behavior

Explaining short and rough but I hope you'll catch the idea.

\yii\di\Container class could be extended with $isSingleton property (better name is welcome).
When isSingleton=true then container will work as is in this implementation and return same instance for same ID.
When isSingleton=false then container will work as factory and create new instance every time for same ID.

Then it is possible to create singleton container having non-singleton container as its parent.
So former definitions config goes to non-singleton container and singletons config goes to singleton container. Then this singleton container can be used in Yii::createObject function to reproduce Yii 2.0 behavior: it will return singleton when found and create new object according to non-singleton definition otherwise.

Simplify contributing

Right now in TravisCI build you add PHP Codesniffer package and check sources for compatibility with PSR 2 but it does not reflect on the development environment and produce "WTF Effect" once you create PR as locally your tests pass and everything seems to be ok.

I suggest to do following:

  • add PHP Codesniffer to dev dependencies
  • update readme with required steps to do before creating PR that include running
  • update .travis.yml to not require quizlabs/php_codesniffer

Another thing I noticed in .travis.yml is condition if [ $TRAVIS_PHP_VERSION = '5.6' ]; but matrix include only 7.1 and 7.2. Maybe this condition should be removed?

Number instead Numer in tests?

What steps will reproduce the problem?

Clone Repository
Run tests

What is the expected result?

What do you get instead?

Additional info

Q A
Version 1.0.?
PHP version
Operating system

Shouldn't it be called getNumber? Or is there a reason for calling it getNumer?

./tests/Unit/ContainerTest.php:298: $this->assertSame($number, $engine->getNumer());
./tests/Support/EngineMarkTwo.php:33: public function getNumer(): int
./tests/Support/EngineMarkOne.php:33: public function getNumer(): int
./tests/Support/EngineInterface.php:22: public function getNumer(): int;

Proposal: Extension containers

Extensions will be able to use this DI implementation, or any other container implementing delegated lookup.

The syntax could look like something like this:

public function getContainer(ContainerInterface $container): ContainerInterface
{
    return $myContainer = new Container([
        'some' => 'config',
        'config' => Config::class
    ], $container);

}

Note that the extension container must support the delegated lookup feature, this make sure that when looking for dependencies, new lookups will use the application container (thus allowing the user to override extension settings).

The calling code is then responsible for making sure all extension containers are used, for example using code similar to this:

$compositeContainer = new CompositeContainer();
$compositeContainer->attach(new Container($applicationConfig, $compositeContainer);
foreach($extensions as $extension) {
    $compositeContainer->attach($extension->getContainer($compositeContainer));
}

Note that while in this example we are passing the child container a reference to its parent, we are actually always passing in the root container, which in this case is the parent container.

How to achieve multi-connection effect of yii2 through di

Because the configuration is covered
Di always reference to redis2

    'queue1' => [
        '__class' => \yii\queue\redis\Queue::class,
    ],
    \yii\db\redis\Connection::class => \yii\di\Reference::to('redis1'),
    'redis1' => [
        '__class' => \yii\db\redis\Connection::class,
        'hostname' => 'redis1.com',
        'database' => 1,
    ],

    'queue2' => [
        '__class' => \yii\queue\redis\Queue::class,
    ],
    \yii\db\redis\Connection::class => \yii\di\Reference::to('redis2'),
    'redis2' => [
        '__class' => \yii\db\redis\Connection::class,
        'hostname' => 'redis2.com',
        'database' => 2,
    ],
    /**
     * @inheritdoc
     */
    public function __construct(SerializerInterface $serializer, Connection $redis)
    {
        parent::__construct($serializer);
        $this->redis = $redis; <- Never reids2 Connection
    }

[Feature Proposal] Add Decorators

Through container, we can set any default values, attach behaviors, event handlers etc.

But if there is a bootstrap code that used in different container definitions, add behavior or state to individual objects at run-time or if you want to apply several transformations to an object returned from the container, you need to add a lot of additional code with a meaning that won't be clean to understand sometimes and that won't be really handy.

I suggest adding support for decorators in the container.

Specification

Each decorator has a method decorate with one argument - object that should be decorated. No other methods are required, so the interface looks like:

/**
 * Represents decorator of any objects.
 *
 * @see https://sourcemaking.com/design_patterns/decorator
 */
interface ObjectDecorator {
    public function decorate($object);
}

Decorator can be assigned to a class by its name or identifier in the container, using method addDecorator of the container:

$container->addDecorator(Calculator::class, EngineeringDecorator::class);
$container->addDecorator(Calculator::class, ScienceDecorator::class);

or through configuration:

'container' => [
    'decorators' => [
        Calculator::class =>  [
            EngineeringDecorator::class,
            ScienceDecorator::class,
        ]
    ]
]

Each decorator is a singleton and should be created only one time when a target object is requested from the container.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.