- 后端
- Web框架
【免费下载链接】yii2
Yii 2: The Fast, Secure and Professional PHP Framework
导读
服务定位器(Service Locator)是 Yii 2 框架中负责"按 ID 提供各类服务/组件"的核心对象:它保证每个组件在定位器内只存在单一实例,并允许你通过 ID 以"读属性"一样的方式获取组件。本篇以 docs/guide-ja/concept-service-locator.md 为主线,结合 yii\di\ServiceLocator 源码、yii\base\Module 的树遍历实现与 ServiceLocatorTest 测试用例,系统讲解组件注册的四种方式、惰性实例化与共享实例机制、components批量配置、模块树遍历解析,以及封装第三方库时的推荐实践。读完本篇,你将能够熟练使用Yii::$app->get('db')/Yii::$app->db访问并自定义应用组件,也能在自己的模块中按需注册与覆写服务。
什么是服务定位器
服务定位器是一个"知道如何提供应用可能需要的各种服务(或组件)"的对象。在服务定位器内部,每个组件只以单一实例存在,并通过唯一的ID标识;你使用该 ID 即可从定位器中取出组件。
在 Yii 2 中,服务定位器就是 yii\di\ServiceLocator 的实例或其子类实例。该类继承自 yii\base\Component,因此它本身也是带属性、事件与行为的组件对象。源码类注释明确指出其实现的是经典的service locator 模式(framework/di/ServiceLocator.php)。
Yii 中最常用的服务定位器是通过\Yii::$app访问的应用(Application)对象。它提供的服务被称为应用组件(Application Components),例如request、response、urlManager等。借助服务定位器提供的功能,你可以轻松配置这些组件,甚至用自研实现整体替换它们。除应用对象外,每个模块(Module)对象也都是一个服务定位器,并且模块实现了 树遍历(tree traversal)能力。
注册组件:set() 的四种定义形式
使用服务定位器的第一步是向其注册组件,注册通过 yii\di\ServiceLocator::set() 完成。set()接受组件 ID 与组件定义(definition),共有四种合法定义形式:
| 定义形式 | 说明 | 源码判定分支(framework/di/ServiceLocator.php) |
|---|---|---|
| 类名字符串 | 例如'yii\caching\ApcCache',取用时由容器按类名创建实例 | is_callable($definition, true)且为字符串 |
| 配置数组 | 必须包含class键(或__class键),其余键值对用于初始化属性 | is_array($definition) |
| PHP 可调用(匿名函数/静态方法) | 取用时调用该回调并返回对象 | is_object($definition) || is_callable($definition, true) |
| 已实例化的对象 | 取用时直接返回该对象本身 | is_object($definition) && !$definition instanceof Closure |
set()的注册逻辑非常严格:如果传入的是配置数组但缺少class(或__class)元素,会抛出InvalidConfigException,提示"The configuration for the "$id" component must contain a "class" element.";如果传入的类型既非对象、非可调用、也非数组(例如字符串、整数等),同样会抛出InvalidConfigException(framework/di/ServiceLocator.php)。此外,set()总是先执行unset($this->_components[$id]),这意味着重新注册同名组件会使之前已实例化的旧实例作废;而传入null则等价于移除该组件定义(framework/di/ServiceLocator.php)。
文档给出了四种注册方式的完整示例:
use yii\di\ServiceLocator; use yii\caching\FileCache; $locator = new ServiceLocator; // 使用类名注册 "cache"(取用时创建组件) $locator->set('cache', 'yii\caching\ApcCache'); // 使用配置数组注册 "db" $locator->set('db', [ 'class' => 'yii\db\Connection', 'dsn' => 'mysql:host=localhost;dbname=demo', 'username' => 'root', 'password' => '', ]); // 使用匿名函数注册 "search"(函数负责构建组件) $locator->set('search', function () { return new app\components\SolrService; }); // 使用现成组件实例注册 "pageCache" $locator->set('pageCache', new FileCache);关于set()的更多细节,可参考源码中的完整示例注释(framework/di/ServiceLocator.php),其中还展示了配置数组形式的db组件可附带'charset' => 'utf8'等更多初始化参数,以及['Foo', 'bar']这种"类方法数组"形式的 PHP 可调用定义。
获取组件:get() 与属性式访问
组件注册完成后,可以通过以下两种等价方式中的任意一种,使用其 ID 访问:
$cache = $locator->get('cache'); // 或者 $cache = $locator->cache;第二种写法之所以可行,是因为ServiceLocator重写了魔术方法__get():当访问的属性名是一个已注册组件 ID 时,直接委托给get();否则才回退到父类Component::__get()的常规属性解析(framework/di/ServiceLocator.php)。同理,__isset()也会优先检查组件是否已注册或已加载(framework/di/ServiceLocator.php)。
惰性实例化与共享单例语义
get()的行为(framework/di/ServiceLocator.php)可概括为:
- 若
_components[$id]中已存在实例,直接返回——即后续访问返回同一实例; - 若只存在定义(
_definitions[$id]):- 定义是对象且非
Closure(即注册时传入的实例),直接缓存并返回该对象; - 其他形式(类名、配置数组、可调用)交给
Yii::createObject($definition)创建后缓存并返回;
- 定义是对象且非
- 若既无实例也无定义,且
$throwException为true,抛出InvalidConfigException: Unknown component ID: $id;为false时返回null。
也就是说,组件在你第一次访问时才被创建(惰性实例化),之后一直被复用(共享单例)。Yii::createObject()实际委托给Yii::$container(全局 DI 容器)完成实例创建、构造器依赖解析与属性配置(framework/BaseYii.php),这也是服务定位器与 依赖注入容器 协作的关键点。
测试用例 testShared() 明确验证了共享语义:连续两次get()返回的对象通过assertSame()判定为同一实例,同时验证了配置数组('prop1' => 10、'prop2' => 20)被正确应用到新建组件上。
检查与清理
- has() 用于判断组件 ID 是否已注册(
$checkInstance = false时检查是否存在定义),或是否已经实例化($checkInstance = true时检查_components)。 - clear($id) 同时移除定义与已加载实例。
- getComponents($returnDefinitions) 返回全部组件定义或已加载实例的列表。
通过 components 属性批量注册
由于服务定位器通常通过配置(configuration)创建,ServiceLocator提供了一个可写属性components(对应 setComponents()),允许一次性配置并注册多个组件。下面的配置数组可为服务定位器(例如应用)注册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; }, ], ];注意tz组件展示了回调定义的另一实用场景:它可以在回调体内引用Yii::$app->formatter->defaultTimeZone等运行时状态,动态构建组件,而类名与静态配置数组都无法做到这一点。
应用如何预置核心组件
对于 Yii 应用而言,components配置会在 Application::__construct() 中与"核心组件"合并:coreComponents()返回的log、view、formatter、i18n、urlManager、assetManager、security(以及安装了 swiftmailer 时的mailer)等定义会被预置(framework/base/Application.php)。合并规则是:若你在配置中未定义该 ID,则使用默认定义;若已定义但缺少class键,则自动补充默认类名。这就是"应用组件可以按需覆写"机制的底层来源。
用静态工厂方法封装第三方库
上述配置中,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 组件时尤为推荐:用上述静态方法表达构建第三方对象的复杂逻辑,而组件的使用者只需调用静态方法即可完成组件配置,无需关心第三方库的构建细节。
树遍历(Tree Traversal)
模块允许任意嵌套,一个 Yii 应用本质上就是一棵模块树。由于每个模块都是服务定位器,让子模块能够访问其父模块的服务是顺理成章的。这使得模块内部可以使用$this->get('db'),而不必引用根服务定位器写Yii::$app->get('db')。额外的好处是,开发者可以在模块内覆写(override)配置。
任何从模块请求服务的操作,如果该模块自身无法满足,都会向上传递给父模块处理。
源码实现印证了这一点:Module::get() 先检查自身是否挂在父模块下(isset($this->module)),若存在父模块,则先以$throwException = false在自身查找,找不到再递归调用$this->module->get($id, $throwException)——父模块可能是应用对象或其他上级模块。对应的 Module::has() 也采用同样的"自身或祖先"判定逻辑。该行为自 2.0.13 起加入(源码注释明确说明)。
需要特别注意的是:模块内组件的配置永远不会与父模块内同名组件的配置合并。服务定位器模式允许我们定义具名服务,但不能假定同名服务使用相同的配置参数。这也是"模块可覆写、但覆写是整体替换而非合并"这一规则的根源——一旦子模块以某个 ID 注册了组件,该请求就由子模块的定义完全接管,不会再回溯到父模块。
常见陷阱与最佳实践小结
- 重复访问不会重建实例:
get()返回的是缓存后的同一实例,这是服务定位器的核心契约;若需要"每次取新对象",应改用 DI 容器(Yii::$container)而非服务定位器。 - 注册同名组件会清空旧实例:
set()先unset(_components[$id]),重注册后再get()将按新定义重建。 - 配置数组必须有 class:否则抛出
InvalidConfigException;__class键作为class的别名也会被归一化处理(framework/di/ServiceLocator.php)。 - 模块树遍历只向上回溯,不做配置合并:子模块同名组件完全覆盖父模块定义。
- 第三方库封装优先使用静态工厂方法:让用户只调用
SolrServiceBuilder::build(...)即可完成配置,隐藏第三方对象构建复杂度。 - 应用组件可安全覆写:通过
components配置覆盖核心组件时,只需给出class与所需属性,框架会保留其余默认行为(见 framework/base/Application.php)。
延伸阅读
- 概念:配置(Configurations):理解
components配置数组的完整语法与合并规则。 - 概念:依赖注入容器(DI Container):服务定位器与 DI 容器的区别与协作(
Yii::createObject的底层)。 - 结构:应用(Applications):应用对象作为服务定位器的完整生命周期。
- 结构:应用组件(Application Components):内置应用组件的分类与自定义方式。
- 源码:yii\di\ServiceLocator、yii\base\Module、yii\base\Application、BaseYii::createObject。
- 测试:tests/framework/di/ServiceLocatorTest.php,其中
testShared()(共享实例)、testCallable()(回调注册)、testObject()(实例注册)、testModulePropertyIsset()(组件属性式访问)等用例直接验证了本文所述行为。
- 后端
- 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 Service Locator 全面解析:应用组件注册、获取与模块树遍历机制
Yii 2 Service Locator 全面解析:应用组件注册、获取与模块树遍历机制 Service Locator(服务定位器)是 Yii 2 框架依赖管
后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考