简体   繁体   中英

Yii2 dependency injection, configuration and inheritance

Let's say I have a configuration like this (snippet from official guide ):

$config = [
    // ...
    'container' => [
        'definitions' => [
            'yii\widgets\LinkPager' => ['maxButtonCount' => 5],
        ],
    ],
    // ...
];

I create a class named FancyLinkPager:

class FancyLinkPager extends \yii\widgets\LinkPager
{
    // ...
}

When I create an object of class FancyLinkPager like so (please ignore the $pagination object, it's here for correctness sake):

$pagination     = \Yii::createObject(Pagination::class);
$linkPager      = \Yii::createObject(['class' => LinkPager::class, 'pagination' => $pagination]);
$fancyLinkPager = \Yii::createObject(['class' => FancyLinkPager::class, 'pagination' => $pagination]);
$linkPager->maxButtonCount; // 5 as configured
$fancyLinkPager->maxButtonCount; // 10 as LinkPager's default

My problem is that I wished $fancyLinkPager->maxButtonCount to be 5 as configured. I know I can add another line in the configuration or adjust it to specify my custom class, but it's not solution for me because:

  1. I want to keep the code DRY
  2. This is an oversimplified example of my needs - in real world you don't expect to have multiple LinkPager's child classes, but it is highly possible for other objects

My question is: is there any framework-supported way of achieving this? The solutions I came up with are:

  1. Hack a custom __construct in FancyLinkPager (or another intermediate class or trait) so that it would look into App's configuration and call Yii::configure on the instance, but I don't find a good way to do it in generic way
  2. Inject a dependency into FancyLinkPager with "setup" object, like LinkPagerSettings and configure that class in container section of my configuration, but it would make some trouble to work with vanilla LinkPager instances as well

Maybe the only real solution would be to create my own implementation of yii\\di\\Container that allows for inheriting configuration from parent classes but before I dive into this I would like to know if I haven't overlooked something.

Finally I came up with my own implementation of DI container introducing new configurable property:

public $inheritableDefinitions = [];

Full class code:

<?php

namespace app\di;

/**
 * @inheritdoc
 */
class Container extends \yii\di\Container
{
    /**
     * @var array inheritable object configurations indexed by class name
     */
    public $inheritableDefinitions = [];

    /**
     * @inheritdoc
     *
     * @param string $class
     * @param array $params
     * @param array $config
     *
     * @return object the newly created instance of the specified class
     */
    protected function build($class, $params, $config) {
        $config = $this->mergeInheritedConfiguration($class, $config);

        return parent::build($class, $params, $config);
    }

    /**
     * Merges configuration arrays of parent classes into configuration of newly created instance of the specified class.
     * Properties defined in child class (via configuration or property declarations) will not get overridden.
     *
     * @param string $class
     * @param array $config
     *
     * @return array
     */
    protected function mergeInheritedConfiguration($class, $config) {
        if (empty($this->inheritableDefinitions)) {
            return $config;
        }

        $inheritedConfig = [];
        /** @var \ReflectionClass $reflection */
        list($reflection) = $this->getDependencies($class);

        foreach ($this->inheritableDefinitions as $parentClass => $parentConfig) {
            if ($class === $parentClass) {
                $inheritedConfig = array_merge($inheritedConfig, $parentConfig);
            } else if (is_subclass_of($class, $parentClass)) {
                /** @var \ReflectionClass $parentReflection */
                list($parentReflection) = $this->getDependencies($parentClass);

                // The "@" is necessary because of possible (and wanted) array to string conversions
                $notInheritableProperties = @array_diff_assoc($reflection->getDefaultProperties(),
                    $parentReflection->getDefaultProperties());

                // We don't want to override properties defined specifically in child class
                $parentConfig    = array_diff_key($parentConfig, $notInheritableProperties);
                $inheritedConfig = array_merge($inheritedConfig, $parentConfig);
            }
        }

        return array_merge($inheritedConfig, $config);
    }
}

This is how it can be used to accomplish customization of LinkPager described in the question:

'container'  => [
    'inheritableDefinitions' => [
        'yii\widgets\LinkPager'  => ['maxButtonCount' => 5],
    ],
],

Now if I create a class FancyLinkPager that extends yii\\widgets\\LinkPager the DI container will merge the default configuration:

$pagination     = \Yii::createObject(Pagination::class);
$linkPager      = \Yii::createObject(['class' => LinkPager::class, 'pagination' => $pagination]);
$fancyLinkPager = \Yii::createObject(['class' => FancyLinkPager::class, 'pagination' => $pagination]);
$linkPager->maxButtonCount; // 5 as configured
$fancyLinkPager->maxButtonCount; // 5 as configured - hurrah!

I have also taken into account qiangxue's comment about explicit setting of default property values in class definition, so if we declare the FancyLinkPager class as such:

class FancyLinkPager extends LinkPager
{
    public $maxButtonCount = 18;
}

the property setting will be respected:

$linkPager->maxButtonCount; // 5 as configured
$fancyLinkPager->maxButtonCount; // 18 as declared

To swap default DI container in your application you have to explicitly set Yii::$container somewhere in an entry script:

Yii::$container = new \app\di\Container();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM