iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 3
0

昨天有提到 Application 是 Laravel Service Container 的實作,它繼承了 Container ,是負責管理元件如何產生的元件。比方說:

$container = new Container();

$container->singleton(MyClass::class, function () {
    return new MyClass('dep');
});

如此一來,當使用 make() 時,它就會觸發 callback ,依照 callback 的寫法來產生對應的物件

$instance1 = $container->make(MyClass::class); // return instance of MyClass

$instance2 = $container->make(MyClass::class); // return the same instance, because it's singleton

事實上,它最好用的地方,正是自動處理依賴注入的功能:

class Dep
{
}

class MyClass
{
    public function __construct(Dep $dep)
    {
        // ...
    }
}

$container = new Container();

$container->make(MyClass::class); // 這是 Work 的

為什麼會這麼神奇呢?讓我們一起看看原始碼吧!

類別圖

首先,依賴很單純,它只依賴 Contracts 並實作 ArrayAccess

PlantUML 原始碼如下:

@startuml
interface Psr\Container\ContainerInterface {
  + {abstract} get($id)
  + {abstract} has($id)
}

interface Contracts\Container\Container {
  + {abstract} bound($abstract)
  + {abstract} alias($abstract, $alias)
  + {abstract} tag($abstracts, $tags)
  + {abstract} tagged($tag)
  + {abstract} bind($abstract, $concrete = null, $shared = false)
  + {abstract} bindIf($abstract, $concrete = null, $shared = false)
  + {abstract} singleton($abstract, $concrete = null)
  + {abstract} extend($abstract, Closure $closure)
  + {abstract} instance($abstract, $instance)
  + {abstract} when($concrete)
  + {abstract} factory($abstract)
  + {abstract} make($abstract, array $parameters = [])
  + {abstract} call($callback, array $parameters = [], $defaultMethod = null)
  + {abstract} resolved($abstract)
  + {abstract} resolving($abstract, Closure $callback = null)
  + {abstract} afterResolving($abstract, Closure $callback = null)
}

class Illuminate\Container\BoundMethod {
  + {static} call()
}

Psr\Container\ContainerInterface <|-- Contracts\Container\Container
Contracts\Container\Container <|.. Illuminate\Container\Container
ArrayAccess <|.. Illuminate\Container\Container
Illuminate\Container\Container --> Illuminate\Container\BoundMethod : static call
Illuminate\Container\Container *-- Illuminate\Container\ContextualBindingBuilder
@enduml

從類別圖可以了解:

  • 核心角色為 Illuminate\Container\Container (下稱 Container
  • Illuminate\Container\BoundMethod (下稱 BoundMethod)為類似 helper 的輔助角色
  • Illuminate\Container\ContextualBindingBuilder (下稱 ContextualBindingBuilder)也是輔助角色,協助產生 container 的設定。

singleton() 做了什麼事

從一開始的範例,我們知道 singleton() 是設定 callback 表示該物件如何建置,而 make() 則是產生。首先看 singleton() 的實作是什麼:

public function singleton($abstract, $concrete = null)
{
    $this->bind($abstract, $concrete, true);
}

這裡可以了解,它是 bind() 的另一種呼叫方法,因為 PHP 並不像 Java 有多型,所以常會使用這一類的寫法增加可用性與可閱讀性。如:

public function getData(array $query)
{
    // ...
}

public function getDataById($id)
{
    $this->>getData([
        'id' => $id,
    ]);
}

bind() 的原始碼如下,雖然已經有清楚的註解了,不過還是簡單用中文描述:

public function bind($abstract, $concrete = null, $shared = false)
{
    // 先把舊的實例丟掉,從這個方法的實作可以得知,跟實例有關係的屬性是 instances 和 aliases
    $this->dropStaleInstances($abstract);

    // 當沒有給 concrete 的話,則會把 abstract 當 concrete 來處理
    if (is_null($concrete)) {
        $concrete = $abstract;
    }

    // 當 concrete 不是 Closure 的話,會預期它是 class 名稱, Laravel 會把它包裝成預設的 Closure
    if (! $concrete instanceof Closure) {
        $concrete = $this->getClosure($abstract, $concrete);
    }

    // 屬性 `bindings` 會放綁定相關資訊
    $this->bindings[$abstract] = compact('concrete', 'shared');

    // 當 abstract 已被解析過的話,會觸發 rebound 事件,跟 resolved 相關的屬性是 resolved 與 instances
    if ($this->resolved($abstract)) {
        $this->rebound($abstract);
    }
}

在追程式碼的過程中,會同時注意屬性有哪些,因為不同方法之間的關聯,是由屬性連繫起來的。

其中,我們需要先了解預設的 Closure 是什麼:

protected function getClosure($abstract, $concrete)
{
    return function ($container, $parameters = []) use ($abstract, $concrete) {
        if ($abstract == $concrete) {
            return $container->build($concrete);
        }
        return $container->make($concrete, $parameters);
    };
}

這裡的 $container ,指的就是 $this 。剛剛 bind() 裡面有提到:

當沒有給 concrete 的話,則會把 abstract 當 concrete 來處理

這裡原始碼可以發現,如果上述情況的話,它會使用 build() 建置實例;不是的話,則會使用一開始提到的 make() 建置。

這兩個之間的差異,只要繼續看 make() 就會了解。

make() 做了什麼事

make()singleton() 類似,也是在呼叫另一個方法 resolve() ,不過這個做法會比較像是 proxy pattern。

public function make($abstract, array $parameters = [])
{
    return $this->resolve($abstract, $parameters);
}

resolve() 可就複雜了:

protected function resolve($abstract, $parameters = [])
{
    // 取得抽象的別名,這是從屬性 `aliases` 取得的
    $abstract = $this->getAlias($abstract);

    // 確定是否需要用 ContextualBuild
    $needsContextualBuild = ! empty($parameters) || ! is_null(
        $this->getContextualConcrete($abstract)
    );

    // 這裡是 singleton 實作的一部分,所以同時也可以知道屬性 `instances` 是用來實作 registry singleton pattern 的 
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    // 屬性 `with` 是用拿暫存的,後面產生實例的過程會用到 parameters ,將會從 with 取得
    $this->with[] = $parameters;

    // 這裡會嘗試由 abstract 取得 concrete ,真的都找不到的話,將會回傳 abstract
    $concrete = $this->getConcrete($abstract);

    // 產生實例
    if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete);
    } else {
        $object = $this->make($concrete);
    }

    // 如果有定義 extender 就跑一下,它可以幫原本的物件定義做一些改變或裝飾,類似 decorator pattern
    foreach ($this->getExtenders($abstract) as $extender) {
        $object = $extender($object, $this);
    }

    // 如果是 singleton 的話,會把實例存在屬性 `instances` 裡,也就是剛剛上面看到的 registry singleton pattern 會使用到
    if ($this->isShared($abstract) && ! $needsContextualBuild) {
        $this->instances[$abstract] = $object;
    }

    // 觸發解析事件,類似 observer pattern
    $this->fireResolvingCallbacks($abstract, $object);

    // 標記這個物件已被解析過
    $this->resolved[$abstract] = true;

    // 把剛剛暫存的 parameters 資料移除
    array_pop($this->with);

    return $object;
}

整個過程可大略分成下面幾個重點:

  • make() or build()
  • Registry singleton pattern
  • Extenders (或 decorator pattern)
  • Fire callback (或 observer pattern)

後面三個有設計模式可以參考,都很好理解它們的目的甚至實作;建置如果使用 make() 的話,就會發生遞迴呼叫(recursive call),因此要先了解 isBuildable() 實作,先知道什麼情況會發生遞迴呼叫,什麼情況會終止。

protected function isBuildable($concrete, $abstract)
{
    return $concrete === $abstract || $concrete instanceof Closure;
}

實作非常簡單:如果 abstract 與 concrete 相同,或是 concrete 是 Closure 的話,會使用 build();反之,兩個不同,而且 concrete 不是 Closure 時,則會使用 make()

回顧上面 bind() 曾提到的:

當 concrete 不是 Closure 的話,會預期它是 class 名稱, Laravel 會把它包裝成預設的 Closure

換句話說,只要曾使用 bind() 定義過的類別,就一定會使用 build() ,舉幾個官網的例子

$this->app->bind('HelpSpot\API', function ($app) {
    return new HelpSpot\API($app->make('HttpClient'));
});

這個情況因為 concrete 是 Closure ,所以會使用 build()。再看另一個例子:

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

因為 concrete 不是 Closure,它會包裝成預設的 Closure 存起來,所以最後也會使用 build()

如果沒有綁定過的類別拿來 make() 呢?比方說一開始舉的例子:

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

這時就得了解 concrete 的來源: getConcrete() 的實作了:

protected function getConcrete($abstract)
{
    // 看看 contextual binding 有沒有對應的設定,有的話就回傳
    if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
        return $concrete;
    }

    // 屬性 `bindings` 只有 bind() 才會 assign ,因此它必定為 Closure ,而流程會走 build()
    if (isset($this->bindings[$abstract])) {
        return $this->bindings[$abstract]['concrete'];
    }

    // 如果都不是,就回傳 abstract ,流程也會走 build()
    return $abstract;
}

看起來關鍵就是 getContextualConcrete() 做了什麼事了:

protected function getContextualConcrete($abstract)
{
    // 先找看看有沒有 contextual binding
    if (! is_null($binding = $this->findInContextualBindings($abstract))) {
        return $binding;
    }

    // alias 也沒搞頭的話,就回傳 null
    if (empty($this->abstractAliases[$abstract])) {
        return;
    }

    // 有的話,就都找看看有沒有 contextual binding 
    foreach ($this->abstractAliases[$abstract] as $alias) {
        if (! is_null($binding = $this->findInContextualBindings($alias))) {
            return $binding;
        }
    }
}

Contextual Binding 是用在同一個類別,在不同地方會使用到不同的實例,這裡再講下去會太複雜,就先跳過。

從以上追過原始碼的結果會發現,除非是使用 Contextual Binding ,它才會在 resolve() 的時候使用 make() ,其他都會使用 build()

今天先到此結束,明天再繼續看 build() 做了什麼。


上一篇
分析 bootstrap 流程
下一篇
分析 Container(2)
系列文
Laravel 原始碼分析46

尚未有邦友留言

立即登入留言