Laravel服务容器

2024-11-06 3

Laravel服务容器是一个强大的工具,用于管理类的依赖并执行依赖注入。依赖注入(Dependency Injection,简称 DI)是一个常见的设计模式,它的本质是通过构造函数或某些情况下的 setter 方法将依赖注入到类中。

一、实例

<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Repositories\UserRepository;
use App\Models\User;
use Illuminate\View\View;
class UserController extends Controller
{
/
* 创建一个新的控制器实例。
*/
public function __construct(
protected UserRepository $users,
) {}
/
* 显示给定用户的个人资料。
*/
public function show(string $id): View
{
$user = $this->users->find($id);
return view('user.profile', ['user' => $user]);
}
}

在这个例子中,”UserController” 需要通过 “UserRepository” 从数据源中检索用户信息。”UserRepository” 类被注入到控制器中,负责处理用户的检索逻辑。在这个上下文中,”UserRepository” 很可能会使用 Eloquent 从数据库中查询用户数据。然而,因为我们使用依赖注入的方式将 “UserRepository” 注入到控制器中,因此如果需要的话,可以轻松地替换它为其他实现方式。在测试应用程序时,我们还可以轻松地模拟 “UserRepository” 或创建一个虚拟实现。

二、零配置解析

如果一个类没有依赖关系,或者它的依赖是具体的类(而不是接口),容器通常不需要指示如何解析这个类。例如,在 “routes/web.php” 中,你可以直接写:

<?php
class Service
{
// ...
}
Route::get('/', function (Service $service) {
die($service::class);
});

在这个例子中,访问 “/” 路由时,Laravel 会自动解析 “Service” 类并将其实例注入到路由的处理程序中。这种自动化依赖注入的方式改变了开发的方式,让开发者能够专注于业务逻辑,而不必担心配置问题。

幸运的是,在 Laravel 中,很多类都会自动通过容器接收它们的依赖,包括控制器、事件监听器、中间件等。此外,依赖注入还可以应用在队列作业的 “handle” 方法中。当你体验到容器的零配置依赖注入后,你会觉得它是开发中不可或缺的一部分。

三、何时使用Laravel服务容器

由于自动化依赖注入,许多情况下你可以在路由、控制器、事件监听器等地方直接通过类型提示依赖,而无需手动与容器交互。例如,你可以在路由中直接通过类型提示 “Illuminate\Http\Request” 对象,以便轻松访问当前的 HTTP 请求:

use Illuminate\Http\Request;
Route::get('/', function (Request $request) {
// 处理请求
});

在上面的代码中,尽管我们没有显式地与容器交互,容器依然在幕后处理了所有的依赖注入。然而,有些情况下你可能需要直接与容器进行交互。以下是两种典型的场景:

  • 接口绑定:当你编写了一个接口的实现类,并且希望在路由或类构造函数中通过类型提示该接口时,必须告诉容器如何解析这个接口;
  • 开发 Laravel 包:如果你正在开发一个 Laravel 包,并且希望将你的服务提供给其他 Laravel 开发者,你可能需要将包中的服务绑定到容器中。

四、Laravel服务容器绑定

1、简单绑定

大多数服务容器绑定会在服务提供者中注册。你可以通过 “$this->app” 属性访问容器,并使用 “bind” 方法进行绑定。以下是一个简单的绑定示例:

use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
$this->app->bind(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

在这个例子中,我们将 “Transistor” 类绑定到容器,并通过闭包返回实例。闭包中的 “$app” 参数是容器实例,可以用来解析依赖项。这样一来,每次从容器解析 “Transistor” 类时,都会创建一个新的实例。

如果你希望在服务提供者之外的代码中与容器交互,也可以使用 “App” 门面:

use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\App;
App::bind(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

2、绑定单例

使用 “singleton” 方法可以将一个类或接口绑定到容器中,并确保这个类在应用程序生命周期内只被实例化一次。后续对该类的所有请求都会返回相同的实例:

use App\Services\Transistor;
use App\Services\PodcastParser;
use Illuminate\Contracts\Foundation\Application;
$this->app->singleton(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

如果你希望仅在该类型尚未绑定时才进行单例绑定,可以使用 “singletonIf” 方法:

$this->app->singletonIf(Transistor::class, function (Application $app) {
return new Transistor($app->make(PodcastParser::class));
});

3、绑定实例

你还可以使用 “instance” 方法将现有对象绑定到容器中。给定的实例将在之后的容器解析中始终返回相同的对象:

use App\Services\Transistor;
use App\Services\PodcastParser;
$service = new Transistor(new PodcastParser);
$this->app->instance(Transistor::class, $service);

在这个例子中,我们将已经创建的 “Transistor” 实例绑定到容器中,从而确保在之后的解析中,容器返回相同的实例。

五、将接口绑定到实现

在 Laravel 中,服务容器有一个非常强大的特性,可以将接口绑定到特定的实现。假设我们有一个 “EventPusher” 接口和一个 “RedisEventPusher” 实现。通过将接口与实现绑定,我们可以确保每当容器需要 “EventPusher” 时,它将注入 “RedisEventPusher” 实现。代码如下:

use App\Contracts\EventPusher;
use App\Services\RedisEventPusher;
$this->app->bind(EventPusher::class, RedisEventPusher::class);

这行代码告诉容器,任何需要 “EventPusher” 实现的地方,都应该使用 “RedisEventPusher”。接下来,在需要 “EventPusher” 实现的类构造函数中,我们可以直接类型提示 “EventPusher” 接口,如下所示:

use App\Contracts\EventPusher;
/
* 创建一个新的类实例。
*/
public function __construct(
protected EventPusher $pusher
) {}

1、上下文绑定

有时,可能会有两个类依赖相同的接口,但希望注入不同的实现。例如,两个控制器可能依赖 “Illuminate\Contracts\Filesystem\Filesystem” 接口的不同实现。在这种情况下,Laravel 提供了一种简洁的方式来处理这种需求:

use App\Http\Controllers\PhotoController;
use App\Http\Controllers\UploadController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
$this->app->when([VideoController::class, UploadController::class])
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});

2、上下文属性

Laravel 还提供了上下文绑定属性,可以在不显式定义服务提供者的情况下,向类中注入特定的驱动程序或配置值。例如,使用 “Storage” 属性,可以注入特定的存储磁盘:

<?php
namespace App\Http\Controllers;
use Illuminate\Container\Attributes\Storage;
use Illuminate\Contracts\Filesystem\Filesystem;
class PhotoController extends Controller
{
public function __construct(
#[Storage('local')] protected Filesystem $filesystem
)
{
// ...
}
}

除了 “Storage” 属性,Laravel 还提供了以下上下文属性:”Auth”、”Cache”、”Config”、”DB” 和 “Log”,可以将它们用于注入相应的服务:

<?php
namespace App\Http\Controllers;
use Illuminate\Container\Attributes\Auth;
use Illuminate\Container\Attributes\Cache;
use Illuminate\Container\Attributes\Config;
use Illuminate\Container\Attributes\DB;
use Illuminate\Container\Attributes\Log;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Database\Connection;
use Psr\Log\LoggerInterface;
class PhotoController extends Controller
{
public function __construct(
#[Auth('web')] protected Guard $auth,
#[Cache('redis')] protected Repository $cache,
#[Config('app.timezone')] protected string $timezone,
#[DB('mysql')] protected Connection $connection,
#[Log('daily')] protected LoggerInterface $log,
)
{
// ...
}
}

此外,Laravel 还提供了 “CurrentUser” 属性,可以将当前认证的用户直接注入到路由或类中:

use App\Models\User;
use Illuminate\Container\Attributes\CurrentUser;
Route::get('/user', function (#[CurrentUser] User $user) {
return $user;
})->middleware('auth');

3、自定义属性的定义

在 Laravel 中,可以通过实现 “Illuminate\Contracts\Container\ContextualAttribute” 接口来创建自定义上下文属性。容器会自动调用属性的 “resolve” 方法,该方法应返回要注入到使用该属性的类中的值。下面的示例演示了如何重新实现 Laravel 内置的 “Config” 属性:

<?php
namespace App\Attributes;
use Illuminate\Contracts\Container\ContextualAttribute;
use Illuminate\Contracts\Container\Container;
#[Attribute(Attribute::TARGET_PARAMETER)]
class Config implements ContextualAttribute
{
/
* 创建一个新的属性实例。
*
* @param string $key 配置键
* @param mixed $default 默认值
*/
public function __construct(public string $key, public mixed $default = null)
{
}/
* 解析配置值。
*
* @param self $attribute
* @param Container $container
* @return mixed
*/
public static function resolve(self $attribute, Container $container)
{
return $container->make('config')->get($attribute->key, $attribute->default);
}
}

4、绑定基本类型

有时,可能会遇到类需要注入其他类实例的同时,还需要注入基本类型(例如整数或字符串)。可以使用容器的上下文绑定功能来为类提供这些基本值:

use App\Http\Controllers\UserController;
$this->app->when(UserController::class)
->needs('$variableName')
->give($value);

如果一个类依赖于某个“类别”的一组实例(通过标签标记),可以使用 “giveTagged” 方法来注入该标签下的所有绑定:

$this->app->when(ReportAggregator::class)
->needs('$reports')
->giveTagged('reports');

如果需要从配置文件中注入某个值,可以使用 “giveConfig” 方法:

$this->app->when(ReportAggregator::class)
->needs('$timezone')
->giveConfig('app.timezone');

5、绑定类型化可变参数

当一个类的构造函数接收一个类型化的对象数组作为参数时,可以使用上下文绑定通过闭包为这些参数提供解析。例如:

<?php
use App\Models\Filter;
use App\Services\Logger;
class Firewall
{
/
* 过滤器实例数组。
*
* @var array
*/
protected $filters;
/
* 创建一个新的类实例。
*/
public function __construct(
protected Logger $logger,
Filter ...$filters,
) {
$this->filters = $filters;
}
}

使用容器的上下文绑定,可以通过闭包返回一个 “Filter” 实例的数组:

$this->app->when(Firewall::class)
->needs(Filter::class)
->give(function (Application $app) {
return [
$app->make(NullFilter::class),
$app->make(ProfanityFilter::class),
$app->make(TooLongFilter::class),
];
});

为了简化操作,还可以直接提供一个类名数组,容器会在 “Firewall” 需要 “Filter” 实例时进行解析:

$this->app->when(Firewall::class)
->needs(Filter::class)
->give([
NullFilter::class,
ProfanityFilter::class,
TooLongFilter::class,
]);

6、使用标签(Tagging)

有时可能需要解析某类标记下的所有服务实例。例如,构建一个报告分析器时,可能会接收到多个 “Report” 接口实现的实例数组。在这种情况下,可以为报告实现类分配标签:

$this->app->bind(CpuReport::class, function () {
// ...
});
$this->app->bind(MemoryReport::class, function () {
// ...
});
$this->app->tag([CpuReport::class, MemoryReport::class], 'reports');

一旦服务被标记,就可以使用容器的 “tagged” 方法轻松解析所有带有该标签的服务:

$this->app->bind(ReportAnalyzer::class, function (Application $app) {
return new ReportAnalyzer($app->tagged('reports'));
});

7、扩展绑定

“extend” 方法允许修改一个已经解析的服务。例如,可以在服务解析时运行额外的逻辑,来装饰或配置该服务。”extend” 方法接收两个参数:服务类名和一个返回修改后的服务实例的闭包。闭包接收服务实例和容器实例作为参数:

$this->app->extend(Service::class, function (Service $service, Application $app) {
return new DecoratedService($service);
});

六、解析服务

1、”make” 方法

可以使用容器的 “make” 方法来解析类实例。”make” 方法接受希望解析的类或接口的名称:

use App\Services\Transistor;
$transistor = $this->app->make(Transistor::class);

如果某些构造函数参数无法通过容器自动解析,可以使用 “makeWith” 方法并手动传递所需的参数:

use App\Services\Transistor;
$transistor = $this->app->makeWith(Transistor::class, ['id' => 1]);

还可以使用 “bound” 方法检查类或接口是否已经在容器中绑定:

if ($this->app->bound(Transistor::class)) {
// 服务已绑定,执行相关操作
}

如果没有直接访问 “$app” 实例的权限,可以使用 “App” 门面或 “app” 助手来解析类实例:

use App\Services\Transistor;
use Illuminate\Support\Facades\App;
$transistor = App::make(Transistor::class);
$transistor = app(Transistor::class);

如果希望在类中注入 Laravel 的容器实例本身,可以直接在构造函数中类型提示 “Illuminate\Container\Container” 类。Laravel 将会自动注入容器实例:

use Illuminate\Container\Container;
/
* 创建一个新的类实例。
*/
public function __construct(
protected Container $container
) {}

这样做的好处是,你可以在类的内部访问整个应用容器,进行更复杂的服务解析和依赖注入。

2、自动注入

除了容器实例之外,你还可以在类的构造函数中注入其他依赖,Laravel 会自动解析并注入所需的类。例如,在控制器、事件监听器、中间件等中,常常需要将依赖注入到构造函数中:

<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
use App\Models\User;
class UserController extends Controller
{
/
* 创建一个新的控制器实例。
*/
public function __construct(
protected UserRepository $users
) {}
/
* 显示具有给定 ID 的用户。
*/
public function show(string $id): User
{
$user = $this->users->findOrFail($id);
return $user;
}
}

在上述示例中,”UserRepository” 会自动注入到控制器中,无需手动传递。

七、方法调用和注入

你还可以在类的方法中类型提示依赖,Laravel 容器将会在调用该方法时自动注入所需的依赖项。这种方式尤其适用于需要动态创建并执行方法的情况。例如:

<?php
namespace App;
use App\Repositories\UserRepository;
class UserReport
{
/
* 生成一个新的用户报告。
*/
public function generate(UserRepository $repository): array
{
return [
// ...
];
}
}

可以通过 Laravel 的容器直接调用 “generate” 方法,并自动注入 “UserRepository”:

use App\UserReport;
use Illuminate\Support\Facades\App;
$report = App::call([new UserReport, 'generate']);

另外也可以使用容器来调用闭包,并自动注入闭包中的依赖项:

use App\Repositories\UserRepository;
use Illuminate\Support\Facades\App;
$result = App::call(function (UserRepository $repository) {
// ...
});

八、容器事件

Laravel 容器还允许你在解析某个类或服务时触发事件,使用 “resolving” 方法可以监听这个事件并做进一步处理。你可以在容器解析对象时动态设置对象的属性,或进行其他操作:

use App\Services\Transistor;
use Illuminate\Contracts\Foundation\Application;
// 监听特定类的解析事件
$this->app->resolving(Transistor::class, function (Transistor $transistor, Application $app) {
// 当容器解析 "Transistor" 类型的对象时被调用...
});
// 监听任何类型的解析事件
$this->app->resolving(function (mixed $object, Application $app) {
// 当容器解析任何类型的对象时被调用...
});

在 “resolving” 方法的回调中,你可以访问被解析的对象,并在其使用前设置任何附加的属性或执行其他操作。

九、PSR-11兼容接口

Laravel 的服务容器实现了 PSR-11 容器接口。这意味着你可以类型提示 “Psr\Container\ContainerInterface” 来获取 Laravel 容器的实例,这对于需要与其他遵循 PSR-11 的库兼容的情况非常有用:

use App\Services\Transistor;
use Psr\Container\ContainerInterface;
Route::get('/', function (ContainerInterface $container) {
$service = $container->get(Transistor::class);
// ...
});

如果容器无法解析指定的标识符,Laravel 会抛出异常。具体的异常类型包括:

  • “Psr\Container\NotFoundExceptionInterface”,当标识符未绑定时抛出;
  • “Psr\Container\ContainerExceptionInterface”,当容器无法解析已经绑定的标识符时抛出。
  • 广告合作

  • QQ群号:707632017

温馨提示:
1、本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。邮箱:2942802716#qq.com(#改为@)。 2、本站原创内容未经允许不得转裁,转载请注明出处“站长百科”和原文地址。