服务

简介

什么是服务?

CodeIgniter 4 中的 服务 提供了创建和共享新类实例的功能。它通过 Config\Services 类实现。

CodeIgniter 内部的所有核心类都以“服务”形式提供。这仅仅意味着,与硬编码要加载的类名不同,需要调用的类被定义在一个非常简单的配置文件中。该文件充当一种工厂,用于创建所需类的新实例。

为什么要使用服务?

一个简单的例子可能更容易理解。假设你需要引入一个 Timer 类的实例。最直接的方法就是创建该类的一个新实例:

<?php

$timer = new \CodeIgniter\Debug\Timer();

这非常有效。直到你决定要用一个不同的计时器类来替代它。也许这个类提供了一些默认计时器不具备的高级功能。为了实现这一点,你现在必须找到应用程序中所有使用了计时器类的地方。如果你为了持续记录应用程序性能而保留了这些代码,那么这种方式会非常耗时且容易出错。服务正是为了解决这个问题而设计的。

我们不再自己创建实例,而是让一个中心类来为我们创建该类的实例。这个类非常简单,它只为每个我们想作为服务使用的类包含一个方法。该方法通常返回该类的一个 共享实例,并将任何可能的依赖项传递给它。然后,我们将计时器创建代码替换为调用这个全局函数或 Services 类的代码:

<?php

$timer = service('timer');

// The code above is the same as the code below.
$timer = \Config\Services::timer();

当你需要更改所使用的实现时,可以修改服务配置文件,更改会自动在整个应用程序中生效,而无需你做任何额外操作。现在你只需利用任何新功能即可。这种方式非常简单且不易出错。

备注

建议仅在控制器中创建服务。其他文件,如模型和库,应通过构造函数或 setter 方法传入依赖项。

如何获取服务

由于许多 CodeIgniter 类都以服务形式提供,你可以像下面这样获取它们:

<?php

$timer = service('timer');

$timer 是 Timer 类的一个实例,如果你再次调用 service('timer'),你将得到完全相同的实例。

服务通常返回该类的一个 共享实例。以下代码在第一次调用时创建一个 CURLRequest 实例,第二次调用则返回完全相同的实例。

<?php

$options1 = [
    'baseURI' => 'http://example.com/api/v1/',
    'timeout' => 3,
];
$client1 = service('curlrequest', $options1);

$options2 = [
    'baseURI' => 'http://another.example.com/api/v2/',
    'timeout' => 10,
];
$client2 = service('curlrequest', $options2);
// $options2 does not work.
// $client2 is the exactly same instance as $client1.

因此,$client2 的参数 $options2 不会生效,它会被忽略。

获取新实例

如果你想获取 Timer 类的一个新实例,需要将参数 $getShared 设为 false

<?php

$timer = \Config\Services::timer(false);

便捷函数

提供了两个用于获取服务的函数,这些函数始终可用。

service()

第一个是 service(),它返回所请求服务的一个实例。唯一必需的参数是服务名称。这与 Services 文件中的方法相同,始终返回该类的一个 共享实例,因此多次调用该函数应始终返回相同的实例:

<?php

$logger = service('logger');

// The code above is the same as the code below.
$logger = \Config\Services::logger();

备注

自 v4.5.0 起,当你不向服务传递参数时,由于性能提升,推荐使用全局函数 service()

如果创建方法需要额外的参数,可以在服务名称后传递它们:

<?php

$renderer = service('renderer', APPPATH . 'views/');

// The code above is the same as the code below.
$renderer = \Config\Services::renderer(APPPATH . 'views/');

single_service()

第二个函数 single_service() 的工作方式与 service() 类似,但返回一个新实例:

<?php

$logger = single_service('logger');

// The code above is the same as the code below.
$logger = \Config\Services::logger(false);

定义服务

为了使服务正常工作,你必须能够依赖每个类都具有一个稳定的 API 或 接口。CodeIgniter 的几乎所有类都提供了一个它们遵循的接口。当你想要扩展或替换核心类时,你只需确保满足该接口的要求,就能知道这些类是兼容的。

例如,RouteCollection 类实现了 RouteCollectionInterface。当你想要创建一个提供不同路由创建方式的替代类时,你只需创建一个实现 RouteCollectionInterface 的新类:

<?php

namespace App\Router;

use CodeIgniter\Router\RouteCollectionInterface;

class MyRouteCollection implements RouteCollectionInterface
{
    // Implement required methods here.
}

最后,将 routes() 方法添加到 app/Config/Services.php 中,以创建 MyRouteCollection 的新实例,而非 CodeIgniter\Router\RouteCollection

<?php

namespace Config;

use CodeIgniter\Config\BaseService;

class Services extends BaseService
{
    // ...

    public static function routes()
    {
        return new \App\Router\MyRouteCollection(static::locator(), config('Modules'));
    }
}

允许传入参数

在某些情况下,你可能希望在实例化类时传入一个设置。由于服务文件是一个非常简单的类,实现这一点很容易。

一个很好的例子是 renderer 服务。默认情况下,我们希望该类能在 APPPATH . 'views/' 找到视图。但我们希望开发者能够根据需要更改该路径。因此,该类接受 $viewPath 作为构造函数参数。服务方法如下所示:

<?php

namespace Config;

use CodeIgniter\Config\BaseService;

class Services extends BaseService
{
    // ...

    public static function renderer($viewPath = APPPATH . 'views/')
    {
        return new \CodeIgniter\View\View($viewPath);
    }
}

这在构造函数方法中设置了默认路径,但也允许轻松更改所使用的路径:

<?php

$renderer = \Config\Services::renderer('/shared/views/');

共享类

有时你需要确保只创建一个服务的实例。这可以通过工厂方法内部调用的 getSharedInstance() 方法轻松处理。该方法会检查该实例是否已在服务类中被创建并保存,如果没有,则创建一个新实例。所有的工厂方法都提供 $getShared = true 作为最后一个参数。你也应该遵循这种方法:

<?php

namespace Config;

use CodeIgniter\Config\BaseService;

class Services extends BaseService
{
    // ...

    public static function routes($getShared = true)
    {
        if ($getShared) {
            return static::getSharedInstance('routes');
        }

        return new \App\Router\MyRouteCollection(static::locator(), config('Modules'));
    }
}

服务发现

CodeIgniter 可以自动发现你在任何已定义的命名空间内创建的 Config/Services.php 文件。这使得可以轻松地使用任何模块的服务文件。为了使自定义服务文件被发现,它们必须满足以下要求:

  • 其命名空间必须在 app/Config/Autoload.php 中定义

  • 在命名空间内,该文件必须位于 Config/Services.php

  • 它必须继承 CodeIgniter\Config\BaseService

一个小例子可以阐明这一点。

假设你在项目根目录下创建了一个名为 Blog 的新目录。这将包含一个带有控制器、模型等的 博客模块,并且你希望将其中的一些类作为服务提供。第一步是创建一个新文件:Blog/Config/Services.php。该文件的骨架如下:

<?php

namespace Blog\Config;

use CodeIgniter\Config\BaseService;

class Services extends BaseService
{
    public static function postManager()
    {
        // ...
    }
}

现在你可以像上面描述的那样使用这个文件。当你想从任何控制器获取文章服务时,只需使用框架的 Config\Services 类来获取你的服务:

<?php

$postManager = service('postManager');

备注

如果多个服务文件具有相同的方法名,将返回找到的第一个实例。

重置服务缓存

在 4.6.0 版本加入.

当在框架初始化过程早期首次调用 Services 类时,通过自动发现找到的服务类会被缓存在一个类属性中,并且不会被更新。

如果稍后动态加载了模块,并且这些模块中有服务,那么必须更新缓存。

这可以通过运行 Config\Services::resetServicesCache() 来实现。这将清除缓存,并在需要时强制重新进行服务发现。