- 后端
- Web框架
【免费下载链接】yii2
Yii 2: The Fast, Secure and Professional PHP Framework
Service Locator(服务定位器)是 Yii 2 框架依赖管理体系的核心设计模式,它通过 ID 向应用提供统一的服务获取入口。本文将基于 docs/guide-pt-BR/concept-service-locator.md 与框架源码,系统讲解如何在 Yii 2 中注册、配置与获取组件,深入剖析\Yii::$app背后的单例共享机制、components批量配置方式以及模块间的树遍历查找规则,帮助读者彻底掌握应用组件与模块组件的底层工作原理。
什么是 Service Locator
Service Locator 是一个知道如何提供应用可能需要的各类服务(或称组件)的对象。在 Service Locator 中,每个组件只存在一个实例,并通过唯一的ID进行标识。你使用这个 ID 从 Service Locator 中取回对应组件。
在 Yii 2 中,Service Locator 就是yii\di\ServiceLocator类本身,或其任意子类的一个实例。其核心职责非常清晰:
- 维护一张"组件 ID → 组件定义"的注册表;
- 在首次访问时按定义创建组件实例,并在后续访问中复用同一实例;
- 对外暴露
get()、set()、has()等统一的组件管理接口。
Yii 2 中最常用的 Service Locator 是应用对象(application),可通过\Yii::$app全局访问。应用对象提供的服务被称为应用组件(application components),例如request、response、urlManager、db、cache等。借助 Service Locator 提供的能力,你可以轻松地配置这些组件,甚至用自己的实现替换它们。
除了应用对象之外,每个模块(module)对象本身也是一个 Service Locator——从源码可以看到,framework/base/Module.php中Module类直接继承自ServiceLocator:
class Module extends ServiceLocator这意味着模块可以注册自己的私有组件,也可以沿模块树向上查找父级(包括应用)注册的组件,具体机制将在下文"树遍历"小节展开。
注册组件:set() 的四种方式
使用 Service Locator 的第一步是向它注册组件。注册通过ServiceLocator::set()方法完成。下面的代码展示了注册组件的四种不同方式:
use yii\di\ServiceLocator; use yii\caching\FileCache; $locator = new ServiceLocator; // 方式一:使用类名字符串,get() 时会自动实例化该类 $locator->set('cache', 'yii\caching\ApcCache'); // 方式二:使用配置数组,用于创建并配置组件 $locator->set('db', [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=demo', 'username' => 'root', 'password' => '', ]); // 方式三:使用匿名函数(回调),返回一个组件实例 $locator->set('search', function () { return new app\components\SolrService; }); // 方式四:直接注册一个对象实例 $locator->set('pageCache', new FileCache);从源码看 set() 的合法定义类型
ServiceLocator::set()的实现明确了组件定义(definition)的四种合法形态,并给出了严格的校验逻辑:
| 定义类型 | 说明 | 内部处理 |
|---|---|---|
| 类名字符串 | 如'yii\caching\ApcCache' | 属于可调用(callable)形态,直接存入_definitions |
| PHP 回调 | 匿名函数或['Foo', 'bar']形式的类方法数组 | 存入_definitions,get()时调用返回对象 |
| 配置数组 | 必须包含class元素 | 存入_definitions,get()时交给Yii::createObject()创建 |
| 对象实例 | 已实例化好的对象 | 存入_definitions,get()时原样返回 |
几个值得注意的实现细节:
- 重复注册会覆盖:
set()首先执行unset($this->_components[$id]),清除该 ID 下已缓存的实例,再写入新定义;若传入null则相当于注销该组件。 - 配置数组必须含
class:若配置数组既没有class也没有__class元素,set()会抛出InvalidConfigException("The configuration for the "..." component must contain a "class" element.")。从testDi3Compatibility测试可以看出,__class是兼容旧版 DI 写法的别名键,最终会被统一改写为class。 - 其他非法类型(如整数、字符串但非类名等)会抛出
InvalidConfigException提示 "Unexpected configuration type"。
ServiceLocator内部使用两个私有数组维护状态:_components存放已实例化的组件(按 ID 索引),_definitions存放组件定义。这两个数组正是理解"单例共享"机制的关键。
获取组件:get() 与属性访问
组件注册完成后,即可用其 ID 通过以下两种等价方式获取:
$cache = $locator->get('cache'); // 或者 $cache = $locator->cache;第二种写法之所以可行,是因为ServiceLocator重写了__get()魔术方法(framework/di/ServiceLocator.php):当访问的属性名是一个已注册的组件 ID 时,直接委托给get($name)返回组件;否则回退到父类Component::__get()处理普通属性。同理,__isset()也被重写,使得isset($app->inputWidget)这类判断对组件同样生效(见testModulePropertyIsset测试)。
共享单例机制
get()方法 的核心逻辑如下:
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; } // 其余定义(类名/配置数组/回调)统一交给 Yii::createObject() return $this->_components[$id] = Yii::createObject($definition); } elseif ($throwException) { throw new InvalidConfigException("Unknown component ID: $id"); } return null; }可以看到:
- 首次访问时创建:
get()首次被调用时,会根据注册信息创建组件新实例并返回;此后再次访问时,直接从_components返回同一个实例——这就是 Service Locator 的"单例"语义。测试tests/framework/di/ServiceLocatorTest.php中的testShared用assertSame($object, $object2)验证了多次get()返回的是同一对象。 - 延迟实例化:组件只在被真正访问时才创建,未被访问的注册组件不会实例化,因此不会产生无谓的内存与性能开销。
- 无效 ID 抛出异常:若调用
get()时传入一个未注册的 ID,会抛出InvalidConfigException(异常消息为 "Unknown component ID: ...")。若将第二个参数$throwException设为false,则返回null而不是抛异常。
检查组件是否已注册:has()
你可以使用has()检查某个组件 ID 是否已经注册:
if ($locator->has('cache')) { // 已注册 }has($id, $checkInstance = false)的行为取决于第二个参数:默认(false)检查是否有组件定义;传true则检查是否已实例化(即存在于_components中)。
通过 components 配置批量注册组件
由于 Service Locator 通常是通过配置创建的,ServiceLocator专门提供了可写属性components(对应setComponents()方法),允许一次性配置并注册多个组件。下面的配置数组展示了如何为一个 Service Locator(例如一个应用)注册db、cache、tz和search组件:
return [ // ... 'components' => [ 'db' => [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=demo', 'username' => 'root', 'password' => '', ], 'cache' => 'yii\caching\ApcCache', 'tz' => function() { return new \DateTimeZone(Yii::$app->formatter->defaultTimeZone); }, 'search' => function () { $solr = new app\components\SolrService('127.0.0.1'); // ... 其他初始化 ... return $solr; }, ], ];setComponents()的实现非常简单——遍历数组并对每一项调用set(),因此数组中每个值的写法规则与set()完全一致(类名、配置数组、回调、实例均可混用)。与之对应的getComponents($returnDefinitions = true)则用于取出当前全部定义(或已加载实例)列表。
用静态工厂方法替代回调:封装第三方库的推荐姿势
上面示例中配置search组件还有一种替代思路:不必直接写一个构建SolrService实例的 PHP 回调,而是用一个类的静态方法返回这样的回调,如下所示:
class SolrServiceBuilder { public static function build($ip) { return function () use ($ip) { $solr = new app\components\SolrService($ip); // ... 其他初始化 ... return $solr; }; } } return [ // ... 'components' => [ // ... 'search' => SolrServiceBuilder::build('127.0.0.1'), ], ];这种替代方式在你发布一个封装了第三方库的 Yii 组件时最值得采用:把构造第三方对象的复杂逻辑收敛到静态方法内部,组件使用者只需调用一次静态方法即可完成配置,无需了解第三方库的初始化细节。
树遍历:模块中组件查找的父子传递
模块允许任意嵌套——一个 Yii 应用本质上就是一棵模块树。由于每个模块都是 Service Locator,让子模块能够访问父模块就变得顺理成章。这样模块内部可以直接使用$this->get('db'),而无需绕道引用根定位器Yii::$app->get('db')。额外的好处是,开发者可以在模块中覆盖父级同名组件的配置。
当某个模块无法满足一个组件的请求时,该请求会沿模块树向上传递给父模块继续查找。这一行为的源码位于Module::get()与Module::has():
public function get($id, $throwException = true) { if (!isset($this->module)) { return parent::get($id, $throwException); } $component = parent::get($id, false); if ($component === null) { $component = $this->module->get($id, $throwException); } return $component; } public function has($id, $checkInstance = false) { return parent::has($id, $checkInstance) || (isset($this->module) && $this->module->has($id, $checkInstance)); }其查找顺序可以概括为:
- 先在当前模块自身注册的组件中查找;
- 未命中则递归地向父模块查找(父模块可能是上一级模块,也可能是应用本身);
- 一直向上直到根级,若仍找不到且
$throwException为true则抛出InvalidConfigException。
该树遍历能力自 Yii 2.0.13 版本起提供(见Module::get()的文档注释)。
需要特别注意一点:模块中组件的配置永远不会与父模块中同名组件的配置合并。Service Locator 模式允许定义具名服务,但不能假设同名服务使用了相同的配置参数——模块可以在components中为db提供一套全新的定义来覆盖父级定义,这是有意为之的设计。
应用核心组件:预注册机制
作为最常用的 Service Locator,应用对象在初始化时就会预注册一组核心组件。Application::coreComponents()(framework/base/Application.php)返回的默认核心组件包括:
| 组件 ID | 类 |
|---|---|
log | yii\log\Dispatcher |
view | yii\web\View |
formatter | yii\i18n\Formatter |
i18n | yii\i18n\I18N |
urlManager | yii\web\UrlManager |
assetManager | yii\web\AssetManager |
security | yii\base\Security |
mailer(当yii\swiftmailer\Mailer类存在时) | yii\swiftmailer\Mailer |
在Application::preInit()中(framework/base/Application.php),框架会把核心组件与用户自定义组件合并:如果配置中没有某个核心组件 ID,则自动补上默认定义;如果用户为某个核心组件 ID 提供了配置数组但没有指定class,则沿用核心组件的默认类,用户只需覆盖其余属性。这也是为什么你可以在config里只写:
'components' => [ 'cache' => [ 'class' => 'yii\caching\FileCache', ], ],而不必重新定义view、urlManager等组件的原因。
此外,ServiceLocator本身继承自yii\base\Component,因此它还具备事件(event)与行为(behavior)能力,可与其他 Yii 组件一样被扩展。
测试佐证:共享、回调与对象注册
仓库的单元测试tests/framework/di/ServiceLocatorTest.php从多个角度验证了上述行为:
testCallable:分别用匿名函数和['Creator', 'create']静态方法数组注册,验证get()能正确创建带属性初始化的实例;testObject:注册已实例化对象后,get()返回的assertSame同一对象;testShared:注册配置数组后两次get()返回同一实例,印证共享单例语义;testModulePropertyIsset:验证isset()与属性访问对组件的可用性;testDi3Compatibility:验证__class键与class键的兼容处理。
这些测试与 framework/di/ServiceLocator.php 的实现相互印证,可以作为理解本文所讲机制的第一手参考。
小结
Yii 2 的 Service Locator 是一个"按 ID 提供单例服务"的轻量级容器:通过set()/setComponents()注册,通过get()或属性语法获取,通过has()查询,配合延迟实例化与共享缓存实现高效的组件生命周期管理。应用对象与模块对象都是它的实例,前者预注册了log、view、urlManager等核心组件,后者则通过树遍历沿父子链向上解析服务。掌握这些机制,你就能在配置文件中灵活地注册、覆盖和替换应用组件,也能在模块化开发中正确理解同名组件的覆盖规则,从而真正驾驭 Yii 2 的应用架构。
- 后端
- Web框架
【免费下载链接】yii2
Yii 2: The Fast, Secure and Professional PHP Framework
相关推荐
Yii 2 服务定位器(Service Locator)深入解析:从组件注册到模块树遍历
Yii 2 服务定位器(Service Locator)深入解析:从组件注册到模块树遍历 服务定位器(Service Locator)是 Yii 2 框架依赖管
后端Web框架Yii 2 服务定位器(Service Locator)深度解析:组件注册、单例共享与模块树遍历
Yii 2 服务定位器(Service Locator)深度解析:组件注册、单例共享与模块树遍历 服务定位器(Service Locator)是 Yii 2 框
后端Web框架Yii 2 应用组件(Application Components)全面解析:服务定位、注册方式与核心组件配置实战
Yii 2 应用组件(Application Components)全面解析:服务定位、注册方式与核心组件配置实战 应用组件(Application Compo
后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考