Yii2 单例模式错误示范


单例模式错误示范


正文

一次在项目中要新增RabbitMQ队列服务器,原来有一个,就想着用同一个类就可以了,只是加一下配置,但调试时发现新队列服务器并没有接收到数据。 仔细查看下,发现使用的RabbitMQ队列服务的component是单例模式,后面的参数配置并没有起作用。现在怎么解决这个问题呢?

一种方案是,可以复制一份RabbitMQ队列服务的component,然后给这个复制组件配置参数实现功能。但这样这两个组件有大量相同的代码, 不符合主流开发思想。

怎么解决这个问题呢?想到了工厂模式(严格来说是建造者模式,也不能说是桥接模式), 传入不同的参数,实例化类为不同的对象。但使用的时候怎么实例化是个问题:需要把参数传进去,每一次使用都要实例化一次。 这么想来还是要用单例模式:一次实例化,多次使用。但又要是可配置单例,可以说是工厂模式加单例模式的复合体。

想来想去,突然想到了Yii2中数据库的配置,可以配置多个数据库,但使用的类是一个类。我们这里也可以使用这种方式。查询后, 发现Yii2使用了容器和依赖注入、还有服务定位,对component组件类有要求: 要继承自yii\base\Component,要实现init()方法,再就是配置参数要用public申明。

项目加载阶段会把组件及其配置都加载好,使用到这个组件时,如Yii::$app->queue,会调用get()实例化这个组件对象, 以后不会再重复实例化,会直接使用这个实例化好的对象。

看一下配置:

"components" => [
    'queue' => [
        "class" =>  'common\components\LRabbitQueue',
        'credentials'   => [
            'host' => '192.168.0.1',
            'port' => '5672',
            'login' => 'mqadmin',
            'password' => 'mqadmin'
        ]
    ],
    'queueNew' => [
        "class" =>  'common\components\LRabbitQueue',
        'credentials'   => [
            'host' => '192.168.0.2',
            'port' => '5672',
            'login' => 'mqadmin',
            'password' => 'mqadmin'
        ]
    ],
],

原来的component类:

<?php
namespace common\components;

use Yii;
use yii\base\Component;

class LRabbitQueue extends Component
{
    const LOG_PREFIX = 'common.components.LRabbitQueue.';
    private $connection;
    public $credentials;

    
    public function init()
    {

    }

    /**
     * 初始化连接
     */
    public function initConnection()
    {
        if (!$this->connection || !is_resource($this->connection)) {
            // 这里的Yii::$app->queue->credentials固定死了配置,是导致新组件不可用的原因
            $this->connection = new \AMQPConnection(Yii::$app->queue->credentials);
        }
        if (!$this->connection->isConnected()) {
            $this->connect();
        }
    }

    public function connect($tryNum=3) {
        try {
            $this->connection->connect();
        }
        catch(\Exception $e) {
            if (--$tryNum) {
                Yii::warning("msg[".$e->getMessage()."]", self::LOG_PREFIX . __FUNCTION__);
                sleep(1);
                $this->connect($tryNum);
            } else {
                Yii::error("msg[".$e->getMessage()."]", self::LOG_PREFIX . __FUNCTION__);
                throw $e;
            }
        }

    }
    
    public function getConnection() {
        $this->initConnection();
        return $this->connection;
    }

    /**
     * 日志进队
     * @param $message
     * @param $exchange
     * @param $routing
     * @return bool
     */
    public function produce($message, $exchange, $routing)
    {
        $message = json_encode($message);

        $channel = new \AMQPChannel($this->getConnection());
        $ex = new \AMQPExchange($channel);
        $ex->setName($exchange);
        $ex->setType(AMQP_EX_TYPE_DIRECT);
        $ex->setFlags(AMQP_DURABLE);

        $ex->declareExchange();

        if (!$ex->publish($message, $routing, 1, ['delivery_mode' => 2])) {
            return false;
        }

        return true;
    }

    public function batchProduce($messageList, $exchange, $routing)
    {
        $channel = new \AMQPChannel($this->getConnection());
        $ex = new \AMQPExchange($channel);
        $ex->setName($exchange);
        $ex->setType(AMQP_EX_TYPE_DIRECT);
        $ex->setFlags(AMQP_DURABLE);

        $ex->declareExchange();

        foreach ($messageList as $message) {
            $messageJson = json_encode($message);

            if (!$ex->publish($messageJson, $routing, 1, ['delivery_mode' => 2])) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param $message
     * @param $exchange
     * @param $routing
     * @param $ttl    消息生存时间(1000 = 1s)
     * @return bool
     * @created by Jhu
     * 通用延迟消费进队方法,消息持久化
     */
    public function produceTtl($message, $exchange, $routing, $ttl)
    {

        $message = json_encode($message);

        $channel = new \AMQPChannel($this->getConnection());
        $ex = new \AMQPExchange($channel);
        $ex->setName($exchange);
        $ex->setType(AMQP_EX_TYPE_DIRECT);
        $ex->setFlags(AMQP_DURABLE);

        $ex->declareExchange();

        $argument = array(
            'delivery_mode' => 2,
            'expiration' => $ttl
        );

        if (!$ex->publish($message, $routing, 1, $argument)) {
            return false;
        }

        return true;
    }
}

修改后的新component类:

<?php
namespace common\components;

use Yii;
use yii\base\Component;

class LRabbitQueue extends Component
{
    const LOG_PREFIX = 'common.components.LRabbitQueue.';
    private $connection;
    public $credentials;

    public function init()
    {
        parent::init();
        $this->connection = new \AMQPConnection($this->credentials);
    }

    public function connect($tryNum=3) {
        try {
            $this->connection->connect();
        }
        catch(\Exception $e) {
            if (--$tryNum) {
                Yii::warning("msg[".$e->getMessage()."]", self::LOG_PREFIX . __FUNCTION__);
                sleep(1);
                $this->connect($tryNum);
            } else {
                Yii::error("msg[".$e->getMessage()."]", self::LOG_PREFIX . __FUNCTION__);
                throw $e;
            }
        }
    }
    
    public function getConnection()
    {
        if (!$this->connection->isConnected()) {
            $this->connect();
        }
        return $this->connection;
    }

    /**
     * 日志进队
     * @param $message
     * @param $exchange
     * @param $routing
     * @return bool
     */
    public function produce($message, $exchange, $routing)
    {
        $message = json_encode($message);

        $channel = new \AMQPChannel($this->getConnection());
        $ex = new \AMQPExchange($channel);
        $ex->setName($exchange);
        $ex->setType(AMQP_EX_TYPE_DIRECT);
        $ex->setFlags(AMQP_DURABLE);

        $ex->declareExchange();

        if (!$ex->publish($message, $routing, 1, ['delivery_mode' => 2])) {
            return false;
        }

        return true;
    }

    public function batchProduce($messageList, $exchange, $routing)
    {
        $channel = new \AMQPChannel($this->getConnection());
        $ex = new \AMQPExchange($channel);
        $ex->setName($exchange);
        $ex->setType(AMQP_EX_TYPE_DIRECT);
        $ex->setFlags(AMQP_DURABLE);

        $ex->declareExchange();

        foreach ($messageList as $message) {
            $messageJson = json_encode($message);

            if (!$ex->publish($messageJson, $routing, 1, ['delivery_mode' => 2])) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param $message
     * @param $exchange
     * @param $routing
     * @param $ttl    消息生存时间(1000 = 1s)
     * @return bool
     * @created by Jhu
     * 通用延迟消费进队方法,消息持久化
     */
    public function produceTtl($message, $exchange, $routing, $ttl)
    {

        $message = json_encode($message);

        $channel = new \AMQPChannel($this->getConnection());
        $ex = new \AMQPExchange($channel);
        $ex->setName($exchange);
        $ex->setType(AMQP_EX_TYPE_DIRECT);
        $ex->setFlags(AMQP_DURABLE);

        $ex->declareExchange();

        $argument = array(
            'delivery_mode' => 2,
            'expiration' => $ttl
        );

        if (!$ex->publish($message, $routing, 1, $argument)) {
            return false;
        }

        return true;
    }
}

Yii2中要用到单例时都可以考虑使用服务定位的方式实现,方便以后维护。

这里$credentials这个参数并没有看到赋值,为什么可以访问?这是因为这个组件类继承自yii\base\Component, 而yii\base\Component又继承自yii\base\Objectyii\base\Object中有:

<?php
public function __construct($config = [])
{
    if (!empty($config)) {
        Yii::configure($this, $config);
    }
    $this->init();
}

Yii::configure源码:

<?php
public static function configure($object, $properties)
{
    foreach ($properties as $name => $value) {
            $object->$name = $value;
    }

    return $object;
}

你可能又发现一个疑问,$config这个参数怎么取到的?

这是因为在项目入口文件index.php中有:

<?php
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');

require __DIR__ . '/../../vendor/autoload.php';
require __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
require __DIR__ . '/../../common/config/bootstrap.php';
require __DIR__ . '/../config/bootstrap.php';

$config = yii\helpers\ArrayHelper::merge(
    require __DIR__ . '/../../common/config/main.php',
    require __DIR__ . '/../../common/config/main-local.php',
    require __DIR__ . '/../config/main.php',
    require __DIR__ . '/../config/main-local.php'
);

(new yii\web\Application($config))->run();

$config 这个数组会被 new yii\web\Application($config)yii\base\Application->__construct($config) -> yii\base\Component::__construct($config)yii\base\Object::__construct($config) -> Yii::configure($this, $config) 所调用:

public static function configure($object, $properties)
{
    foreach ($properties as $name => $value) {
        $object->$name = $value;
    }

    return $object;
}

错误追踪

可以看出Yii::configure($this, $config)中的$this肯定不是yii\web\Application对象,而是我们上面说到的组件,如queueNew等(当然这是错误的)。

图:Yii2 核心类继承关系

我们追踪下过程,new yii\web\Application($config)其实调用的是yii\base\Application->__construct($config)这个构造方法:

<?php
/**
 * Constructor.
 * @param array $config name-value pairs that will be used to initialize the object properties.
 * Note that the configuration must contain both [[id]] and [[basePath]].
 * @throws InvalidConfigException if either [[id]] or [[basePath]] configuration is missing.
 */
public function __construct($config = [])
{
    Yii::$app = $this;
    static::setInstance($this);

    $this->state = self::STATE_BEGIN;

    $this->preInit($config);

    $this->registerErrorHandler($config);

    Component::__construct($config);
}

这里的Yii::$app = $this;是说,Yii::$app才是yii\web\Application对象。

Component::__construct($config);调用的其实是父类yii\base\Object::__construct($config)方法:

<?php
/**
 * Constructor.
 * The default implementation does two things:
 *
 * - Initializes the object with the given configuration `$config`.
 * - Call [[init()]].
 *
 * If this method is overridden in a child class, it is recommended that
 *
 * - the last parameter of the constructor is a configuration array, like `$config` here.
 * - call the parent implementation at the end of the constructor.
 *
 * @param array $config name-value pairs that will be used to initialize the object properties
 */
public function __construct($config = [])
{
    if (!empty($config)) {
        Yii::configure($this, $config);
    }
    $this->init();
}

追到这发现Yii::configure($this, $config)中的$this又是yii\web\Application对象, 也是Yii::configure($object, $properties)中的$object->$name = $value;$object,而这个$name则是配置参数中的components$valuecomponents的属性值(包含queuequeueNew等组件)。

看来玄机因该在yii\base\Object::__construct($config)$this->init();上了。 这个$this->init();调用的是yii\base\Applicationinit()

<?php
/**
 * @inheritdoc
 */
public function init()
{
    $this->state = self::STATE_INIT;
    $this->bootstrap();
}

$this->bootstrap();调用的是yii\web\Applicationbootstrap()

<?php
/**
 * @inheritdoc
 */
protected function bootstrap()
{
    $request = $this->getRequest();
    Yii::setAlias('@webroot', dirname($request->getScriptFile()));
    Yii::setAlias('@web', $request->getBaseUrl());

    parent::bootstrap();
}

parent::bootstrap();调用的是yii\base\Applicationbootstrap()

<?php
/**
 * Initializes extensions and executes bootstrap components.
 * This method is called by [[init()]] after the application has been fully configured.
 * If you override this method, make sure you also call the parent implementation.
 */
protected function bootstrap()
{
    if ($this->extensions === null) {
        $file = Yii::getAlias('@vendor/yiisoft/extensions.php');
        $this->extensions = is_file($file) ? include($file) : [];
    }
    foreach ($this->extensions as $extension) {
        if (!empty($extension['alias'])) {
            foreach ($extension['alias'] as $name => $path) {
                Yii::setAlias($name, $path);
            }
        }
        if (isset($extension['bootstrap'])) {
            $component = Yii::createObject($extension['bootstrap']);
            if ($component instanceof BootstrapInterface) {
                Yii::trace('Bootstrap with ' . get_class($component) . '::bootstrap()', __METHOD__);
                $component->bootstrap($this);
            } else {
                Yii::trace('Bootstrap with ' . get_class($component), __METHOD__);
            }
        }
    }

    foreach ($this->bootstrap as $class) {
        $component = null;
        if (is_string($class)) {
            if ($this->has($class)) {
                $component = $this->get($class);
            } elseif ($this->hasModule($class)) {
                $component = $this->getModule($class);
            } elseif (strpos($class, '\\') === false) {
                throw new InvalidConfigException("Unknown bootstrapping component ID: $class");
            }
        }
        if (!isset($component)) {
            $component = Yii::createObject($class);
        }

        if ($component instanceof BootstrapInterface) {
            Yii::trace('Bootstrap with ' . get_class($component) . '::bootstrap()', __METHOD__);
            $component->bootstrap($this);
        } else {
            Yii::trace('Bootstrap with ' . get_class($component), __METHOD__);
        }
    }
}

vendor/yiisoft/extensions.php内容:

<?php

$vendorDir = dirname(__DIR__);

return array (
  'yiisoft/yii2-bootstrap' => 
  array (
    'name' => 'yiisoft/yii2-bootstrap',
    'version' => '2.0.6.0',
    'alias' => 
    array (
      '@yii/bootstrap' => $vendorDir . '/yiisoft/yii2-bootstrap',
    ),
  ),
  'yiisoft/yii2-gii' => 
  array (
    'name' => 'yiisoft/yii2-gii',
    'version' => '2.0.5.0',
    'alias' => 
    array (
      '@yii/gii' => $vendorDir . '/yiisoft/yii2-gii',
    ),
  ),
  'yiisoft/yii2-faker' => 
  array (
    'name' => 'yiisoft/yii2-faker',
    'version' => '2.0.3.0',
    'alias' => 
    array (
      '@yii/faker' => $vendorDir . '/yiisoft/yii2-faker',
    ),
  ),
  'yiisoft/yii2-swiftmailer' => 
  array (
    'name' => 'yiisoft/yii2-swiftmailer',
    'version' => '2.0.6.0',
    'alias' => 
    array (
      '@yii/swiftmailer' => $vendorDir . '/yiisoft/yii2-swiftmailer',
    ),
  ),
  'callmez/yii2-wechat-sdk' => 
  array (
    'name' => 'callmez/yii2-wechat-sdk',
    'version' => '9999999-dev',
    'alias' => 
    array (
      '@callmez/wechat/sdk' => $vendorDir . '/callmez/yii2-wechat-sdk',
    ),
  ),
  'yiisoft/yii2-debug' => 
  array (
    'name' => 'yiisoft/yii2-debug',
    'version' => '2.0.9.0',
    'alias' => 
    array (
      '@yii/debug' => $vendorDir . '/yiisoft/yii2-debug',
    ),
  ),
  'yiisoft/yii2-redis' => 
  array (
    'name' => 'yiisoft/yii2-redis',
    'version' => '2.0.6.0',
    'alias' => 
    array (
      '@yii/redis' => $vendorDir . '/yiisoft/yii2-redis',
    ),
  ),
);

Yii::createObject($class);调用的是yii\BaseYiicreateObject()

<?php
/**
 * Creates a new object using the given configuration.
 *
 * You may view this method as an enhanced version of the `new` operator.
 * The method supports creating an object based on a class name, a configuration array or
 * an anonymous function.
 *
 * Below are some usage examples:
 *
 * 
 * // create an object using a class name
 * $object = Yii::createObject('yii\db\Connection');
 *
 * // create an object using a configuration array
 * $object = Yii::createObject([
 *     'class' => 'yii\db\Connection',
 *     'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
 *     'username' => 'root',
 *     'password' => '',
 *     'charset' => 'utf8',
 * ]);
 *
 * // create an object with two constructor parameters
 * $object = \Yii::createObject('MyClass', [$param1, $param2]);
 * 
 *
 * Using [[\yii\di\Container|dependency injection container]], this method can also identify
 * dependent objects, instantiate them and inject them into the newly created object.
 *
 * @param string|array|callable $type the object type. This can be specified in one of the following forms:
 *
 * - a string: representing the class name of the object to be created
 * - a configuration array: the array must contain a `class` element which is treated as the object class,
 *   and the rest of the name-value pairs will be used to initialize the corresponding object properties
 * - a PHP callable: either an anonymous function or an array representing a class method (`[$class or $object, $method]`).
 *   The callable should return a new instance of the object being created.
 *
 * @param array $params the constructor parameters
 * @return object the created object
 * @throws InvalidConfigException if the configuration is invalid.
 * @see \yii\di\Container
 */
public static function createObject($type, array $params = [])
{
    if (is_string($type)) {
        return static::$container->get($type, $params);
    } elseif (is_array($type) && isset($type['class'])) {
        $class = $type['class'];
        unset($type['class']);
        return static::$container->get($class, $params, $type);
    } elseif (is_callable($type, true)) {
        return static::$container->invoke($type, $params);
    } elseif (is_array($type)) {
        throw new InvalidConfigException('Object configuration must be an array containing a "class" element.');
    }

    throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type));
} 

追踪后发现bootstrap()这一段未发现与我们组件直接相关。

可能关系隐藏在Yii::$app->get("queue"")中。

注册服务

上面说到Yii::configure($this, $config)中的$thisyii\web\Application对象, 也是Yii::configure($object, $properties)中的$object->$name = $value;$object, 而这个$name则是配置参数中的components$valuecomponents的属性值(包含queuequeueNew等组件)。 这个看到给yii\web\Application对象设置components属性,如果这个属性不存在,就会调用__set()方法, 因为未重载过__set()方法,所以调用的还是yii\base\Component__set()方法:

<?php
/**
 * Sets the value of a component property.
 * This method will check in the following order and act accordingly:
 *
 *  - a property defined by a setter: set the property value
 *  - an event in the format of "on xyz": attach the handler to the event "xyz"
 *  - a behavior in the format of "as xyz": attach the behavior named as "xyz"
 *  - a property of a behavior: set the behavior property value
 *
 * Do not call this method directly as it is a PHP magic method that
 * will be implicitly called when executing `$component->property = $value;`.
 * @param string $name the property name or the event name
 * @param mixed $value the property value
 * @throws UnknownPropertyException if the property is not defined
 * @throws InvalidCallException if the property is read-only.
 * @see __get()
 */
public function __set($name, $value)
{
    $setter = 'set' . $name;
    if (method_exists($this, $setter)) {
        // set property
        $this->$setter($value);

        return;
    } elseif (strncmp($name, 'on ', 3) === 0) {
        // on event: attach event handler
        $this->on(trim(substr($name, 3)), $value);

        return;
    } elseif (strncmp($name, 'as ', 3) === 0) {
        // as behavior: attach behavior
        $name = trim(substr($name, 3));
        $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));

        return;
    }

    // behavior property
    $this->ensureBehaviors();
    foreach ($this->_behaviors as $behavior) {
        if ($behavior->canSetProperty($name)) {
            $behavior->$name = $value;
            return;
        }
    }

    if (method_exists($this, 'get' . $name)) {
        throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name);
    }

    throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name);
}

可以看到这里提供了属性、事件、行为的支持。我们只看当前实例中有没有setcomponents()方法,php大小写不敏感, 发现yii\di\ServiceLocator中有setComponents()方法:

<?php
/**
 * Registers a set of component definitions in this locator.
 *
 * This is the bulk version of [[set()]]. The parameter should be an array
 * whose keys are component IDs and values the corresponding component definitions.
 *
 * For more details on how to specify component IDs and definitions, please refer to [[set()]].
 *
 * If a component definition with the same ID already exists, it will be overwritten.
 *
 * The following is an example for registering two component definitions:
 *
 * ``php
 * [
 *     'db' => [
 *         'class' => 'yii\db\Connection',
 *         'dsn' => 'sqlite:path/to/file.db',
 *     ],
 *     'cache' => [
 *         'class' => 'yii\caching\DbCache',
 *         'db' => 'db',
 *     ],
 * ]
 * ``
 *
 * @param array $components component definitions or instances
 */
public function setComponents($components)
{
    foreach ($components as $id => $component) {
        $this->set($id, $component);
    }
}

循环调用set()方法,yii\di\ServiceLocator中有:

<?php
/**
 * Registers a component definition with this locator.
 *
 * For example,
 *
 * 
 * // a class name
 * $locator->set('cache', 'yii\caching\FileCache');
 *
 * // a configuration array
 * $locator->set('db', [
 *     'class' => 'yii\db\Connection',
 *     'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
 *     'username' => 'root',
 *     'password' => '',
 *     'charset' => 'utf8',
 * ]);
 *
 * // an anonymous function
 * $locator->set('cache', function ($params) {
 *     return new \yii\caching\FileCache;
 * });
 *
 * // an instance
 * $locator->set('cache', new \yii\caching\FileCache);
 * 
 *
 * If a component definition with the same ID already exists, it will be overwritten.
 *
 * @param string $id component ID (e.g. `db`).
 * @param mixed $definition the component definition to be registered with this locator.
 * It can be one of the following:
 *
 * - a class name
 * - a configuration array: the array contains name-value pairs that will be used to
 *   initialize the property values of the newly created object when [[get()]] is called.
 *   The `class` element is required and stands for the the class of the object to be created.
 * - a PHP callable: either an anonymous function or an array representing a class method (e.g. `['Foo', 'bar']`).
 *   The callable will be called by [[get()]] to return an object associated with the specified component ID.
 * - an object: When [[get()]] is called, this object will be returned.
 *
 * @throws InvalidConfigException if the definition is an invalid configuration array
 */
public function set($id, $definition)
{
    if ($definition === null) {
        unset($this->_components[$id], $this->_definitions[$id]);
        return;
    }

    unset($this->_components[$id]);

    if (is_object($definition) || is_callable($definition, true)) {
        // an object, a class name, or a PHP callable
        $this->_definitions[$id] = $definition;
    } elseif (is_array($definition)) {
        // a configuration array
        if (isset($definition['class'])) {
            $this->_definitions[$id] = $definition;
        } else {
            throw new InvalidConfigException("The configuration for the \"$id\" component must contain a \"class\" element.");
        }
    } else {
        throw new InvalidConfigException("Unexpected configuration type for the \"$id\" component: " . gettype($definition));
    }
}

这就是定义服务(给$_definitions参数写入数据)。 不过只是定义而已,未说明依赖,也没有实例化。在使用的时候,才会自动加载依赖,才会实例化,这是获取服务的事了。

获取服务

我们获取queue队列服务时,一般会用:

Yii::$app->queue;

获取对象属性会用到__get()方法,yii\di\ServiceLocator重载了yii\base\Component__get()方法:

<?php
/**
 * Getter magic method.
 * This method is overridden to support accessing components like reading properties.
 * @param string $name component or property name
 * @return mixed the named property value
 */
public function __get($name)
{
    if ($this->has($name)) {
        return $this->get($name);
    } else {
        return parent::__get($name);
    }
}

/**
 * Returns a value indicating whether the locator has the specified component definition or has instantiated the component.
 * This method may return different results depending on the value of `$checkInstance`.
 *
 * - If `$checkInstance` is false (default), the method will return a value indicating whether the locator has the specified
 *   component definition.
 * - If `$checkInstance` is true, the method will return a value indicating whether the locator has
 *   instantiated the specified component.
 *
 * @param string $id component ID (e.g. `db`).
 * @param bool $checkInstance whether the method should check if the component is shared and instantiated.
 * @return bool whether the locator has the specified component definition or has instantiated the component.
 * @see set()
 */
public function has($id, $checkInstance = false)
{
    return $checkInstance ? isset($this->_components[$id]) : isset($this->_definitions[$id]);
}

/**
 * Returns the component instance with the specified ID.
 *
 * @param string $id component ID (e.g. `db`).
 * @param bool $throwException whether to throw an exception if `$id` is not registered with the locator before.
 * @return object|null the component of the specified ID. If `$throwException` is false and `$id`
 * is not registered before, null will be returned.
 * @throws InvalidConfigException if `$id` refers to a nonexistent component ID
 * @see has()
 * @see set()
 */
public function get($id, $throwException = true)
{
    if (isset($this->_components[$id])) {
        return $this->_components[$id];
    }

    if (isset($this->_definitions[$id])) {
        $definition = $this->_definitions[$id];
        if (is_object($definition) && !$definition instanceof Closure) {
            return $this->_components[$id] = $definition;
        } else {
            return $this->_components[$id] = Yii::createObject($definition);
        }
    } elseif ($throwException) {
        throw new InvalidConfigException("Unknown component ID: $id");
    } else {
        return null;
    }
}

上面把直接调用的两个方法也写出来了。看来问题的核心在Yii::createObject($definition);创建对象上。 这里可以看一下服务定位器, 不过要想看明白,还是要先看完依赖注入和依赖注入容器

附一下yii\BaseYiicreateObject()方法:

<?php
/**
 * Creates a new object using the given configuration.
 *
 * You may view this method as an enhanced version of the `new` operator.
 * The method supports creating an object based on a class name, a configuration array or
 * an anonymous function.
 *
 * Below are some usage examples:
 *
 * ``php
 * // create an object using a class name
 * $object = Yii::createObject('yii\db\Connection');
 *
 * // create an object using a configuration array
 * $object = Yii::createObject([
 *     'class' => 'yii\db\Connection',
 *     'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
 *     'username' => 'root',
 *     'password' => '',
 *     'charset' => 'utf8',
 * ]);
 *
 * // create an object with two constructor parameters
 * $object = \Yii::createObject('MyClass', [$param1, $param2]);
 * ``
 *
 * Using [[\yii\di\Container|dependency injection container]], this method can also identify
 * dependent objects, instantiate them and inject them into the newly created object.
 *
 * @param string|array|callable $type the object type. This can be specified in one of the following forms:
 *
 * - a string: representing the class name of the object to be created
 * - a configuration array: the array must contain a `class` element which is treated as the object class,
 *   and the rest of the name-value pairs will be used to initialize the corresponding object properties
 * - a PHP callable: either an anonymous function or an array representing a class method (`[$class or $object, $method]`).
 *   The callable should return a new instance of the object being created.
 *
 * @param array $params the constructor parameters
 * @return object the created object
 * @throws InvalidConfigException if the configuration is invalid.
 * @see \yii\di\Container
 */
public static function createObject($type, array $params = [])
{
    if (is_string($type)) {
        return static::$container->get($type, $params);
    } elseif (is_array($type) && isset($type['class'])) {
        $class = $type['class'];
        unset($type['class']);
        return static::$container->get($class, $params, $type);
    } elseif (is_callable($type, true)) {
        return static::$container->invoke($type, $params);
    } elseif (is_array($type)) {
        throw new InvalidConfigException('Object configuration must be an array containing a "class" element.');
    }

    throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type));
}

说到这,我们思考一下LRabbitQueue::init()何时调用的呢?

在依赖注入通过Reflection->newInstanceArgs()获取服务组件实例时,会调用类的__construct(), 一般继承自yii\base\Object的类实例化时都会调用它的构建方法:

<?php
public function __construct($config = [])
{
    if (!empty($config)) {
        Yii::configure($this, $config);
    }
    $this->init();
}

在这里看到了init()方法。

容器

这里拓展说一下上面多处看到的 static::$container

static::$container 指的是 yii\BaseYii 中的属性public static $container;

Yii 继承了 BaseYii:class Yii extends \yii\BaseYii。 赋值的地方是/vendor/yiisoft/yii2/Yii.php文件中的一行代码Yii::$container = new yii\di\Container();

<?php
/**
 * @link https://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license https://www.yiiframework.com/license/
 */

require __DIR__ . '/BaseYii.php';

/**
 * Yii is a helper class serving common framework functionalities.
 *
 * It extends from [[\yii\BaseYii]] which provides the actual implementation.
 * By writing your own Yii class, you can customize some functionalities of [[\yii\BaseYii]].
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
class Yii extends \yii\BaseYii
{
}

spl_autoload_register(['Yii', 'autoload'], true, true);
Yii::$classMap = require __DIR__ . '/classes.php';
Yii::$container = new yii\di\Container();

应用 yii\base\Application 类中 preInit(&$config) 方法的一段代码$this->setContainer($config['container']);设定容器参数:

public function preInit(&$config)
{
    if (!isset($config['id'])) {
        throw new InvalidConfigException('The "id" configuration for the Application is required.');
    }
    if (isset($config['basePath'])) {
        $this->setBasePath($config['basePath']);
        unset($config['basePath']);
    } else {
        throw new InvalidConfigException('The "basePath" configuration for the Application is required.');
    }

    if (isset($config['vendorPath'])) {
        $this->setVendorPath($config['vendorPath']);
        unset($config['vendorPath']);
    } else {
        // set "@vendor"
        $this->getVendorPath();
    }
    if (isset($config['runtimePath'])) {
        $this->setRuntimePath($config['runtimePath']);
        unset($config['runtimePath']);
    } else {
        // set "@runtime"
        $this->getRuntimePath();
    }

    if (isset($config['timeZone'])) {
        $this->setTimeZone($config['timeZone']);
        unset($config['timeZone']);
    } elseif (!ini_get('date.timezone')) {
        $this->setTimeZone('UTC');
    }

    if (isset($config['container'])) {
        $this->setContainer($config['container']);

        unset($config['container']);
    }

    // merge core components with custom components
    foreach ($this->coreComponents() as $id => $component) {
        if (!isset($config['components'][$id])) {
            $config['components'][$id] = $component;
        } elseif (is_array($config['components'][$id]) && !isset($config['components'][$id]['class'])) {
            $config['components'][$id]['class'] = $component['class'];
        }
    }
}

$this->setContainer() 也是 yii\base\Application 类中方法:

public function setContainer($config)
{
    Yii::configure(Yii::$container, $config);
}

这里就是给yii\BaseYii 中的 属性$container对象 赋值了。

接下来容器具体使用部分,查看 https://ibaiyang.github.io/blog/yii2/2019/06/28/深入理解Yii2.0-依赖注入和依赖注入容器.html






参考资料

PHP 常用设计模式-单例模式 https://ibaiyang.github.io/blog/php/2019/07/30/PHP-常用设计模式.html#单例模式singleton

深入理解Yii2.0 依赖注入和依赖注入容器 https://ibaiyang.github.io/blog/yii2/2019/06/28/深入理解Yii2.0-依赖注入和依赖注入容器.html

深入理解Yii2.0 服务定位器 https://ibaiyang.github.io/blog/yii2/2019/06/29/深入理解Yii2.0-服务定位器.html


返回