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”,当容器无法解析已经绑定的标识符时抛出。