资讯专栏INFORMATION COLUMN

【译】深入研究Laravel的依赖注入容器

chavesgu / 1370人阅读

摘要:原文地址下面是中文翻译拥有强大的控制反转依赖注入容器。单例在使用自动绑定和时,每次需要时都会创建一个新的实例或者调用闭包。

原文地址

Laravel"s Dependency Injection Container in Depth


下面是中文翻译


Laravel拥有强大的控制反转(IoC)/依赖注入(DI) 容器。不幸的是官方文档并没有涵盖所有可用的功能,因此,我决定尝试写文档为自己记录一下。以下是基于Laravel 5.4.26,其他版本可能有所不同。

依赖注入简介

我不会尝试在这里解释DI/IOC背后的原理,如果你不熟悉它们,你可能需要去阅读由Fabien Potencier(Symfony框架作者)创建的什么是依赖注入

访问容器

在Laravel中有几种访问Container实例的方法,但最简单的方法是调用app()helper方法:

$container = app();

我今天不会描述其他方式,而是我想专注于Container类本身。

注意: 如果你读了官方文档,它使用$this->app代替$container

(在Laravel应用程序中,它实际上是Container的一个子类,称为Application这就是为什么称为助手app(),但是这篇文章,我只会描述Container方法)

在Laravel外使用 IlluminateContainer

要在Laravel外使用Container,请安装它

然后:

use IlluminateContainerContainer;

$container = Container::getInstance();
基本用法

最简单的用法是用你想注入的类键入你的类的构造函数:

class MyClass
{
    private $dependency;

    public function __construct(AnotherClass $dependency)
    {
        $this->dependency = $dependency;
    }
}

然后new MyClass使用容器的make()方法。

$instance = $container->make(MyClass::class);

容器会自动实例化依赖关系,所以这在功能上等同于:

$instance = new MyClass(new AnotherClass());

(除了AnotherClass他自己的一些依赖关系,在这种情况下Container将递归实例化它们,直到没有更多)

实例

以下是一个基于PHP-DI docs的更实用的示例,将邮件功能与用户注册分离:

class Mailer
{
    public function mail($recipient, $content)
    {
        // Send an email to the recipient
        // ...
    }
}
class UserManager
{
    private $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function register($email, $password)
    {
        // Create the user account
        // ...

        // Send the user an email to say hello!
        $this->mailer->mail($email, "Hello and welcome!");
    }
}
use IlluminateContainerContainer;

$container = Container::getInstance();

$userManager = $container->make(UserManager::class);
$userManager->register("dave@davejamesmiller.com", "MySuperSecurePassword!");
将接口(Interfaces)绑定到实现(Implementations)

Container可以很容易的编写一个接口,然后在运行时实例化一个具体的实现,首先定义接口:

interface MyInterface { /* ... */ }
interface AnotherInterface { /* ... */ }

并声明实现这些接口的具体类,他们可能依赖于其他接口(或以前的具体类)

class MyClass implements MyInterface
{
    private $dependency;

    public function __construct(AnotherInterface $dependency)
    {
        $this->dependency = $dependency;
    }
}

然后使用bind()去将每个接口映射到具体的类

$container->bind(MyInterface::class, MyClass::class);
$container->bind(AnotherInterface::class, AnotherClass::class);

最后通过将接口名代替类名去传递给make()

$instance = $container->make(MyInterface::class);

注意: 如果你忘记去绑定一个接口,你将会得到一个稍微神秘的致命错误:

Fatal error: Uncaught ReflectionException: Class MyInterface does not exist

这是因为容器会尝试实例化interface (new MyInterface),而这不是一个有效的类。

实例

下面是一个实用的例子,一个可交换的缓存层

interface Cache
{
    public function get($key);
    public function put($key, $value);
}
class RedisCache implements Cache
{
    public function get($key) { /* ... */ }
    public function put($key, $value) { /* ... */ }
}
class Worker
{
    private $cache;

    public function __construct(Cache $cache)
    {
        $this->cache = $cache;
    }

    public function result()
    {
        // Use the cache for something...
        $result = $this->cache->get("worker");

        if ($result === null) {
            $result = do_something_slow();

            $this->cache->put("worker", $result);
        }

        return $result;
    }
}
use IlluminateContainerContainer;

$container = Container::getInstance();
$container->bind(Cache::class, RedisCache::class);

$result = $container->make(Worker::class)->result();
绑定抽象类和具体类(Abstract & Concrete Classes)

Binding 也可以使用到 abstract 类:

$container->bind(MyAbstract::class, MyConcreteClass::class);

或者用一个子类替换一个具体的类:

$container->bind(MySQLDatabase::class, CustomMySQLDatabase::class);
自定义绑定

如果该类需要额外的配置,你可以传递一个闭包来代替类名作为bind()的第二个参数:

$container->bind(Database::class, function (Container $container) {
    return new MySQLDatabase(MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS);
});

每次需要数据库接口时,都会创建并使用一个新的MySQLDatabase实例,并使用指定的配置值。(要想共享单个实例,请参考下面的单例)闭包接收Container实例作为第一个参数,并且可以在需要时用于实例化其他类:

$container->bind(Logger::class, function (Container $container) {
    $filesystem = $container->make(Filesystem::class);

    return new FileLogger($filesystem, "logs/error.log");
});

闭包也可以用来定制具体类如何实例化

$container->bind(GitHubClient::class, function (Container $container) {
    $client = new GitHubClient;
    $client->setEnterpriseUrl(GITHUB_HOST);
    return $client;
});
解决回调

你可以使用resolving()去注册一个用于绑定完成后的回调函数:

$container->resolving(GitHubClient::class, function ($client, Container $container) {
    $client->setEnterpriseUrl(GITHUB_HOST);
});

如果有多个回调,它们将全部被调用,它们也为接口和抽象类工作

$container->resolving(Logger::class, function (Logger $logger) {
    $logger->setLevel("debug");
});

$container->resolving(FileLogger::class, function (FileLogger $logger) {
    $logger->setFilename("logs/debug.log");
});

$container->bind(Logger::class, FileLogger::class);

$logger = $container->make(Logger::class);

也可以通过添加一个回调来处理无论是哪个类被解析,总是调用该回调函数。但是我认为他可能只能在日志/调试中使用:

$container->resolving(function ($object, Container $container) {
    // ...
});
扩展一个类

或者你可以使用extend()包装类并返回一个不同的对象:

$container->extend(APIClient::class, function ($client, Container $container) {
    return new APIClientDecorator($client);
});

结果对象仍然应该实现相同的接口,否则使用类型提示会出错。

单例(Singletons)

在使用自动绑定和bind()时,每次需要时都会创建一个新的实例(或者调用闭包)。想要共享一个实例,使用singleton() 代替 bind()

$container->singleton(Cache::class, RedisCache::class);

或者使用一个闭包:

$container->singleton(Database::class, function (Container $container) {
    return new MySQLDatabase("localhost", "testdb", "user", "pass");
});

要让一个具体的类成为实例,请传递该类且不需要传递第二个参数:

$container->singleton(MySQLDatabase::class);

在不同情况下,单例对象将在第一次需要时创建,然后在随后每次需要时重用。如果你已经有一个实例,你想重用使用instance()方法代替。例如,Laravel使用它来确保无论什么时候将单实例Container实例注入到类中都会返回它:

$container->instance(Container::class, $container);
任意绑定名称

你可以使用任意字符串而不是使用一个类/接口名称,尽管你不能使用类型提示检索它,但必须使用make()代替:

$container->bind("database", MySQLDatabase::class);

$db = $container->make("database");

要同时支持类/接口,请使用alias()

$container->singleton(Cache::class, RedisCache::class);
$container->alias(Cache::class, "cache");

$cache1 = $container->make(Cache::class);
$cache2 = $container->make("cache");

assert($cache1 === $cache2);
存储任意值

你也可以使用容器来存储任意值,例如配置数据:

$container->instance("database.name", "testdb");

$db_name = $container->make("database.name");

它支持数组语法访问,这使得他更自然:

$container["database.name"] = "testdb";

$db_name = $container["database.name"];

当与闭包函数结合使用时,你可以看到为什么这是有用的:

$container->singleton("database", function (Container $container) {
    return new MySQLDatabase(
        $container["database.host"],
        $container["database.name"],
        $container["database.user"],
        $container["database.pass"]
    );
});

(Laravel本是不使用容器进行配置,它使用一个多带带的Config类来代替,但是也是通过PHP-DI实现的)

Tip: 在实例化对象的时候,也可以使用数组语法代替make():

$db = $container["database"];
函数和方法(Functions & Methods)的依赖注入

到现在为止,我们已经看到了构造函数的依赖注入(DI),但是Laravel还支持任意函数的依赖注入(DI):

function do_something(Cache $cache) { /* ... */ }

$result = $container->call("do_something");

其他参数可以作为索引或关联数组传递:

function show_product(Cache $cache, $id, $tab = "details") { /* ... */ }

// show_product($cache, 1)
$container->call("show_product", [1]);
$container->call("show_product", ["id" => 1]);

// show_product($cache, 1, "spec")
$container->call("show_product", [1, "spec"]);
$container->call("show_product", ["id" => 1, "tab" => "spec"]);

这可以用于任意可调用的方法:

闭包
$closure = function (Cache $cache) { /* ... */ };

$container->call($closure);
静态方法
class SomeClass
{
    public static function staticMethod(Cache $cache) { /* ... */ }
}
$container->call(["SomeClass", "staticMethod"]);
// or:
$container->call("SomeClass::staticMethod");
实例方法
class PostController
{
    public function index(Cache $cache) { /* ... */ }
    public function show(Cache $cache, $id) { /* ... */ }
}
$controller = $container->make(PostController::class);

$container->call([$controller, "index"]);
$container->call([$controller, "show"], ["id" => 1]);
调用实例方法的快捷方式

有一个快捷方式来实例化一个类并一次调用一个方法,使用ClassName@methodName

$container->call("PostController@index");
$container->call("PostController@show", ["id" => 4]);

该容器用于实例化类,即:

依赖项注入到构造函数(以及方法)中。

如果你希望重用它,你可以将该类定义为单例。

你可以使用接口或任意名称而不是具体类。

例如:

class PostController
{
    public function __construct(Request $request) { /* ... */ }
    public function index(Cache $cache) { /* ... */ }
}
$container->singleton("post", PostController::class);
$container->call("post@index");

最后,你可以传递一个“默认方法”作为第三个参数,如果第一个参数是没有指定方法的类名,则会调用默认方法,Laravel使用它来实现事件处理

$container->call(MyEventHandler::class, $parameters, "handle");

// Equivalent to:
$container->call("MyEventHandler@handle", $parameters);
方法调用绑定

bindMethod()方法可以用于重写方法调用,例如传递其他参数:

$container->bindMethod("PostController@index", function ($controller, $container) {
    $posts = get_posts(...);

    return $controller->index($posts);
});

所有这些都可以通过使用闭包代替原始方法进行工作:

$container->call("PostController@index");
$container->call("PostController", [], "index");
$container->call([new PostController, "index"]);

但是,任何多余传递给call()的参数都不会传递到闭包中,因此无法使用他们。

$container->call("PostController@index", ["Not used :-("]);

_Notes: 该方法不是 Container interface的一部分, 只适用于具体的 Container 类。为什么忽略参数,请参阅PR

上下文绑定

有时候你想在不同的地方使用不同的接口实现,下面是Laravel 文档中的一个例子:

$container
    ->when(PhotoController::class)
    ->needs(Filesystem::class)
    ->give(LocalFilesystem::class);

$container
    ->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give(S3Filesystem::class);

现在,PhotoController和VideoController都可以依赖文件系统接口,但是每个接口都会接受到不同的实现,你也可以像使用bind()一样使用闭包give()

$container
    ->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give(function () {
        return Storage::disk("s3");
    });

或者一个命名的依赖关系:

$container->instance("s3", $s3Filesystem);

$container
    ->when(VideoController::class)
    ->needs(Filesystem::class)
    ->give("s3");
将参数绑定到原函数

你也可以通过传递变量名称给needs()(而不是接口)和传递变量给give()来绑定原函数

$container
    ->when(MySQLDatabase::class)
    ->needs("$username")
    ->give(DB_USER);

你可以使用闭包来延迟检索值直到需要用到它:

$container
    ->when(MySQLDatabase::class)
    ->needs("$username")
    ->give(function () {
        return config("database.user");
    });

在这里,你不能传递一个类或者一个命名依赖(例如give("database.user")),因为它会作为一个字面值返回,要做到这一点,你将不得不使用闭包:

$container
    ->when(MySQLDatabase::class)
    ->needs("$username")
    ->give(function (Container $container) {
        return $container["database.user"];
    });
做标记

你可以使用容器去“标记”相关的绑定:

$container->tag(MyPlugin::class, "plugin");
$container->tag(AnotherPlugin::class, "plugin");

然后以数组方式检索所有标记的实例:

foreach ($container->tagged("plugin") as $plugin) {
    $plugin->init();
}

tag()的两个参数也可以传递数组:

$container->tag([MyPlugin::class, AnotherPlugin::class], "plugin");
$container->tag(MyPlugin::class, ["plugin", "plugin.admin"]);
重新绑定

_Note: 这个更高级一点,但是很少用到,可以跳过它

打工绑定或者实例已经被使用后,rebinding()调用一个回调函数。例如,这里的session类在被Auth类使用后被替换,所以Auth需要被告知更改:

$container->singleton(Auth::class, function (Container $container) {
    $auth = new Auth;
    $auth->setSession($container->make(Session::class));

    $container->rebinding(Session::class, function ($container, $session) use ($auth) {
        $auth->setSession($session);
    });

    return $auth;
});

$container->instance(Session::class, new Session(["username" => "dave"]));
$auth = $container->make(Auth::class);
echo $auth->username(); // dave
$container->instance(Session::class, new Session(["username" => "danny"]));

echo $auth->username(); // danny

(有关重新绑定的更多信息,请查看 这里 和 这里.)

刷新

还有一种更便捷的方法来处理这种模式,通过refresh()

$container->singleton(Auth::class, function (Container $container) {
    $auth = new Auth;
    $auth->setSession($container->make(Session::class));

    $container->refresh(Session::class, $auth, "setSession");

    return $auth;
});

它也返回现有的实例或绑定(如果有的话),所以你可以这样做:

// This only works if you call singleton() or bind() on the class
$container->singleton(Session::class);

$container->singleton(Auth::class, function (Container $container) {
    $auth = new Auth;
    $auth->setSession($container->refresh(Session::class, $auth, "setSession"));
    return $auth;
});

(我个人觉得这个语法更令人困惑,并且更喜欢上面的更详细的版本)

Note: 这些方法不是 Container interface的一部分, 只是具体的Container class.

重写构造函数参数

makeWith()方法允许您将其他参数传递给构造函数,她忽略了任何现有的实例或单例,并且可以用于创建具有不同参数的类的多个实例,同时依然注入依赖关系:

class Post
{
    public function __construct(Database $db, int $id) { /* ... */ }
}
$post1 = $container->makeWith(Post::class, ["id" => 1]);
$post2 = $container->makeWith(Post::class, ["id" => 2]);

Note: 在 Laravel 5.3 以及以下版本中,它很简单 make($class, $parameters), 但在 Laravel 5.4中被删除, 但在5.4.16 被重新添加为 makeWith() 。 在Laravel 5.5 可能会 恢复到Laravel 5.3 语法.

其他方法

这里涵盖了我认为有用的所有方法,但只是为了整理一些内容。下面这些是对其余共用方法的总结:

bound()

如果类或名称使用bind(), singleton(), instance()alias()绑定,bound()将会返回true

if (! $container->bound("database.user")) {
    // ...
}

你还可以使用数组语法和isset()访问:

if (! isset($container["database.user"])) {
    // ...
}

它可以使用unset()重置、删除指定的绑定/实例/别名

unset($container["database.user"]);
var_dump($container->bound("database.user")); // false
bindIf()

bindIf()bind()相同,除了他只在不存在绑定的情况下才回注册绑定(请参见上面的bound()),它可以用于在包注册中默认绑定,同事允许用户覆盖它:

$container->bindIf(Loader::class, FallbackLoader::class);

没有singletonIf()方法,但是你可以使用bindIf($abstract, $concrete, true)实现它:

$container->bindIf(Loader::class, FallbackLoader::class, true);

或者全部写出来:

if (! $container->bound(Loader::class)) {
    $container->singleton(Loader::class, FallbackLoader::class);
}
resolved()

如果一个类已经被解析,resolved()方法返回true

var_dump($container->resolved(Database::class)); // false
$container->make(Database::class);
var_dump($container->resolved(Database::class)); // true

我不确定他有什么用处,如果使用unset()它会被重置(请看上面的bound()

unset($container[Database::class]);
var_dump($container->resolved(Database::class)); // false
factory()

factory()方法返回一个不带参数和调用的闭包make()

$dbFactory = $container->factory(Database::class);

$db = $dbFactory();

我不确定他有什么用处

wrap()

wrap()方法封装了一个闭包,以便在其执行时注册他的依赖关系,wrap方法接收一个数组参数,返回的闭包不带参数:

$cacheGetter = function (Cache $cache, $key) {
    return $cache->get($key);
};

$usernameGetter = $container->wrap($cacheGetter, ["username"]);

$username = $usernameGetter();

我不确定他有什么用处,因为闭包不需要参数

Note: 此方法不是Container interface的一部分, 只是具体的 Container class.

afterResolving()

afterResolving()方法的作用和resolving()类似,不同的点是在resolving()回调后调用afterResolving。我不确定何时会用到。。。

最后

isShared() - 确定给定类型是否是共享单例/实例

isAlias() - 确定给定的字符串是否是已注册的别名

hasMethodBinding() - 确定容器是否具有给定的方法绑定

getBindings() - 检索所有注册绑定的原始数组

getAlias($abstract) - 解析底层类/绑定名称的别名

forgetInstance($abstract) - 清除单个实例对象

forgetInstances() - 清除所有实例对象

flush() - 清除所有绑定和实例,有效的重置容器

setInstance() - 使用getInstance()替换使用的实例

_Note: 最后一节的方法都不是 Container interface.的一部分


本文最初发布于2017年6月15日的DaveJamesMiller.com

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/28305.html

相关文章

  • 深入剖析 Laravel 服务容器

    摘要:划下重点,服务容器是用于管理类的依赖和执行依赖注入的工具。类的实例化及其依赖的注入,完全由服务容器自动的去完成。 本文首发于 深入剖析 Laravel 服务容器,转载请注明出处。喜欢的朋友不要吝啬你们的赞同,谢谢。 之前在 深度挖掘 Laravel 生命周期 一文中,我们有去探究 Laravel 究竟是如何接收 HTTP 请求,又是如何生成响应并最终呈现给用户的工作原理。 本章将带领大...

    abson 评论0 收藏0
  • 深入理解控制反转(IoC)和依赖注入(DI)

    摘要:本文一大半内容都是通过举例来让读者去理解什么是控制反转和依赖注入,通过理解这些概念,来更加深入。这种由外部负责其依赖需求的行为,我们可以称其为控制反转。工厂模式,依赖转移当然,实现控制反转的方法有几种。 容器,字面上理解就是装东西的东西。常见的变量、对象属性等都可以算是容器。一个容器能够装什么,全部取决于你对该容器的定义。当然,有这样一种容器,它存放的不是文本、数值,而是对象、对象的描...

    HollisChuang 评论0 收藏0
  • Laravel深入学习2 - 控制反转容器

    摘要:控制反转容器控制反转使依赖注入变得更加便捷。有瑕疵控制反转容器是实现的控制翻转容器的一种替代方案。容器的独立使用即使没有使用框架,我们仍然可以在项目中使用安装组件来使用的控制反转容器。在没有给定任何信息的情况下,容器是无法实例化相关依赖的。 声明:本文并非博主原创,而是来自对《Laravel 4 From Apprentice to Artisan》阅读的翻译和理解,当然也不是原汁原味...

    worldligang 评论0 收藏0
  • php实现依赖注入(DI)和控制反转(IOC)

    摘要:工厂模式,依赖转移当然,实现控制反转的方法有几种。其实我们稍微改造一下这个类,你就明白,工厂类的真正意义和价值了。虽然如此,工厂模式依旧十分优秀,并且适用于绝大多数情况。 此篇文章转载自laravel-china,chongyi的文章https://laravel-china.org/top...原文地址: http://www.insp.top/learn-lar... ,转载务必保...

    tomato 评论0 收藏0
  • Laravel深入学习1 - 依赖注入

    摘要:然而,我们需要注意的是仅是软件设计模式依赖注入的一种便利的实现形式。容器本身不是依赖注入的必要条件,在框架他只是让其变得更加简便。首先,让我们探索下为什么依赖注入是有益的。继续深入让我们通过另一个示例来加深对依赖注入的理解。 声明:本文并非博主原创,而是来自对《Laravel 4 From Apprentice to Artisan》阅读的翻译和理解,当然也不是原汁原味的翻译,能保证9...

    sunsmell 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<