资讯专栏INFORMATION COLUMN

Laravel Container (容器) 深入理解 (下)

eternalshallow / 2405人阅读

摘要:意味着依赖被注入进构造函数或者方法如果需要复用实例,可以定义为单例可以用接口或任何名称来代替具体类。技能重写构造函数参数方法允许将附加参数传递给构造函数。

本文大部分翻译自 DAVE JAMES MILLER 的 《Laravel’s Dependency Injection Container in Depth》 。

上文介绍了 Dependency Injection Containers (容器) 的基本概念,现在接着深入讲解 LaravelContainer

Laravel 中实现的 Inversion of Control (IoC) / Dependency Injection (DI) Container 非常强悍,但文档中很低调的没有细讲它。

本文中示例基于 Laravel 5.5 ,其它版本差不多。

准备工作 1.Dependency Injection

关于 DI 请看这篇 《Laravel Dependency Injection (依赖注入) 概念详解》,这里不再赘述。

2. 初识 Container

Laravel 中有一大堆访问 Container 实例的姿势,比如最简单的:

$container = app();

但我们还是先关注下 Container 类本身。

Laravel 官方文档中一般使用 $this->app 代替 $container。它是 Application 类的实例,而 Application 类继承自 Container 类。

3. 在 Laravel 之外使用 IlluminateContainer

如果在 Laravel 之外

mkdir container && cd container
composer require illuminate/container
// 新建一个 container.php,文件名随便取


Container 的技能们
技能Q. 基本用法,用type hint (类型提示) 注入 依赖:

只需要在自己类的构造函数中使用 type hint 就实现 DI

class MyClass
{
    private $dependency;

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

接下来用 Containermake 方法来代替 new MyClass:

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

Container 会自动实例化依赖的对象,所以它等同于:

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

如果 AnotherClass 也有 依赖,那么 Container 会递归注入它所需的依赖。

Container 使用 Reflection (反射) 来找到并实例化构造函数参数中的那些类,实现起来并不复杂,以后的文章里再介绍。

实战

下面是 PHP-DI 文档 中的一个例子,它分离了「用户注册」和「发邮件」的过程:

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)
    {
        // 创建用户账户
        // ...

        // 给用户的邮箱发个 “hello" 邮件
        $this->mailer->mail($email, "Hello and welcome!");
    }
}
use IlluminateContainerContainer;

$container = Container::getInstance();

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

Container 可以轻松地写一个接口,然后在运行时实例化一个具体的实例。 首先定义接口:

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

然后声明实现这些接口的具体类。下面这个类不但实现了一个接口,还依赖了实现另一个接口的类实例:

class MyClass implements MyInterface
{
    private $dependency;

    // 依赖了一个实现 AnotherInterface 接口的类的实例
    public function __construct(AnotherInterface $dependency)
    {
        $this->dependency = $dependency;
    }
}

现在用 Containerbind() 方法来让每个 接口 和实现它的类一一对应起来:

$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"。

实战

下面是可封装的 Cache 层:

interface 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()
    {
        // 去缓存里查询
        $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();

这里用 Redis 做缓存,如果改用其他缓存,只要把 RedisCache 换成别的就行了,easy!

技能E:Binding Abstract & Concret Classes (绑定抽象类和具体类):

绑定还可以用在抽象类:

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

或者继承的类中:

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

如果类需要一些附加的配置项,可以把 bind() 方法中的第二个参数换成 Closure (闭包函数)

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

闭包也可用于定制 具体类 的实例化方式:

$container->bind(GitHubClient::class, function (Container $container) {
    $client = new GitHubClient;
    $client->setEnterpriseUrl(GITHUB_HOST);
    return $client;
});
技能T:Resolving Callbacks (回调)

可用 resolveing() 方法来注册一个 callback (回调函数),而不是直接覆盖掉之前的 绑定。 这个函数会在绑定的类解析完成之后调用。

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

如果有一大堆 callbacks,他们全部都会被调用。对于 接口抽象类 也可以这么用:

$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);

diao 的是,还可以注册成「什么类解析完之后都调用」:

$container->resolving(function ($object, Container $container) {
    // ...
});

但这个估计只有 loggingdebugging 才会用到。

技能Y:Extending a Class (扩展一个类)

使用 extend() 方法,可以封装一个类然后返回一个不同的对象 (装饰模式):

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

注意:这两个类要实现相同的 接口,不然用类型提示的时候会出错:

interface Getable
{
    public function get();
}
class APIClient implements Getable
{
    public function get()
    {
        return "yes!";
    }
}
class APIClientDecorator implements Getable
{
    private $client;

    public function __construct(APIClient $client)
    {
        $this->client = $client;
    }

    public function get()
    {
        return "no!";
    }
}
class User
{
    private $client;

    public function __construct(Getable $client)
    {
        $this->client = $client;
    }
}
$container->extend(APIClient::class, function ($client, Container $container) {
    return new APIClientDecorator($client);
});
//
$container->bind(Getable::class, APIClient::class);

// 此时 $instance 的 $client 属性已经是 APIClentDecorator 类型了
$instance = $container->make(User::class);
技能U:单例

使用 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);
技能I:Arbitrary Binding Names (任意绑定名称)

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);
技能O:保存任何值

Container 还可以用来保存任何值,例如 configuration 数据:

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

它支持数组访问语法,这样用起来更自然:

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

这是因为 Container 实现了 PHP 的 ArrayAccess 接口。

当处理 Closure 绑定的时候,你会发现这个方式非常好用:

$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 用了。

数组访问语法还可以代替 make() 来实例化对象:

$db = $container["database"];
技能P:Dependency Injection for Functions & Methods (给函数或方法注入依赖)

除了给构造函数注入依赖,Laravel 还可以往任意函数中注入:

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]);

都可以注入。

技能A: 调用实例方法的快捷方式

使用 ClassName@methodName 语法可以快捷调用实例中的方法:

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

因为Container 被用来实例化类。意味着:

依赖 被注入进构造函数(或者方法);

如果需要复用实例,可以定义为单例;

可以用接口或任何名称来代替具体类。

所以这样调用也可以生效:

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

最后,还可以传一个「默认方法」作为第三个参数。如果第一个参数是没有指定方法的类名称,则将调用默认方法。 Laravel 用这种方式来处理 event handlers :

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

// 相当于:
$container->call("MyEventHandler@handle", $parameters);
技能S:Method Call Bindings (方法调用绑定)

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 :-("]);

注意:这种方式不是 Container 接口 的一部分,只有在它的实现类 Container 才有。在这个 PR` 里可以看到它加了什么以及为什么参数被忽略。

技能D:Contextual Bindings (上下文绑定)

有时候你想在不同的地方给接口不同的实现。这里有 Laravel 文档 里的一个例子:

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

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

现在 PhotoControllerVideoController 都依赖了 Filesystem 接口,但是收到了不同的实例。

可以像 bind() 那样,给 give() 传闭包:

    ->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");
技能F:Binding Parameters to Primitives (绑定初始数据)

当有一个类不仅需要接受一个注入类,还需要注入一个基本值(比如整数)。
还可以通过将变量名称 (而不是接口) 传递给 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"];
    });
技能G: Tagging (标记)

Container 可以用来「标记」有关系的绑定:

$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"]);
技能H:Rebinding (重新绑定)

这个功能很少用到,可以跳过,仅供参考。

在绑定或实例被使用之后又发生了变化,将调用一个 rebinding 方法。 下例中, Auth 使用 Session 类后,Session 类将被替换,此时需要通知 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

Rebinding 的更多信息可以看这两个链接:
https://stackoverflow.com/questions/38974593/laravels-ioc-container-rebinding-abstract-types
https://code.tutsplus.com/tutorials/digging-in-to-laravels-ioc-container--cms-22167

还有一个 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;
});

注意:这种方式不是 Container 接口 的一部分,只有在它的实现类 Container 才有。

技能J:Overriding Constructor Parameters (重写构造函数参数)

makeWith 方法允许将附加参数传递给构造函数。它忽略任何现有的实例或单例,可以用于创建具有不同参数的类的多个实例,同时仍然注入依赖关系:

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

注意:Laravel 5.3 及以下使用 make($class, $parameters)Laravel 5.4 中移除了此方法,但是在 5.4.16 以后又重新加回来了 makeWith()

技能K:其它

这涵盖了我认为有用的所有方法,但仅仅是简介,不然这篇文章就写不完了。。。

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() 功能类似,差别在于只有在现有绑定不存在的情况下才注册绑定。 它一般被用在 package 中注册一个可被用户重写的默认绑定。

$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() 之后它会被重置:

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

factory() 方法返回一个不需要参数并调用 make() 的闭包。

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

这个东西我也不知道有什么用。。。

wrap()

wrap 方法包装一个闭包,以便在执行时依赖关系被注入。 它接受一个数组参数; 返回的闭包不带参数:

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

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

$username = $usernameGetter();

我也不知道它有啥用,因为返回的闭包没带回参数。。。

注意:这个方法不是 Container 接口` 的一部分,只有在它的实现类 Container 才有。

afterResolving()

afterResolving() 方法作用与 resolving() 完全相同,不同之处是 调用 「resolving」回调之后再调用 「afterResolving」回调。
不知道什么时候会用到它。。。

最后再附几个

isShared() – 确定一个给定的类型是一个 singleton/instance
isAlias() – 确定给定的字符串是否是已注册的 别名
hasMethodBinding() - 确定容器是否具有给定的 method binding
getBindings() - 取回所有已注册绑定的原始数组
getAlias($abstract) - 获取基础类/绑定名称的别名
forgetInstance($abstract) - 清除单个实例对象
forgetInstances() - 清除所有实例对象
flush() - 清除所有绑定和实例,有效地重置容器
setInstance() - 替换 getInstance() 使用的实例 (提示:使用 setInstance(null)来清除它,这样下一次它将生成一个新的实例)

注意:这些方法不是 Container 接口 的一部分,只有在它的实现类 Container 才有。

原创。 所有 Laravel 文章均已收录至 laravel-tips 项目。

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

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

相关文章

  • Laravel深入学习2 - 控制反转容器

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

    worldligang 评论0 收藏0
  • 深入剖析 Laravel 服务容器

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

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

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

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

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

    tomato 评论0 收藏0
  • 【译】深入研究Laravel的依赖注入容器

    摘要:原文地址下面是中文翻译拥有强大的控制反转依赖注入容器。单例在使用自动绑定和时,每次需要时都会创建一个新的实例或者调用闭包。 原文地址 Laravels Dependency Injection Container in Depth 下面是中文翻译 Laravel拥有强大的控制反转(IoC)/依赖注入(DI) 容器。不幸的是官方文档并没有涵盖所有可用的功能,因此,我决定尝试写文档为自...

    chavesgu 评论0 收藏0

发表评论

0条评论

eternalshallow

|高级讲师

TA的文章

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