跳到內容

編譯容器

編輯此頁面

服務容器可以基於各種原因進行編譯。這些原因包括檢查任何潛在問題,例如循環參考,並通過解析參數和移除未使用的服務,使容器更有效率。此外,某些功能 - 例如使用父服務 - 需要編譯容器。

它通過執行以下命令進行編譯

1
$container->compile();

compile 方法使用編譯器 Pass進行編譯。DependencyInjection 组件附帶了幾個 Pass,它們會自動註冊以進行編譯。例如,CheckDefinitionValidityPass 檢查容器中已設定的定義是否存在各種潛在問題。在此以及其他幾個檢查容器有效性的 Pass 之後,會使用進一步的編譯器 Pass 來最佳化設定,然後再進行快取。例如,私有服務和抽象服務會被移除,別名也會被解析。

使用擴充功能管理設定

除了如DependencyInjection 组件中所示,直接將設定載入容器外,您還可以通過向容器註冊擴充功能來管理它。編譯過程的第一步是從註冊到容器的任何擴充功能類別中載入設定。與直接載入的設定不同,它們僅在容器編譯時才被處理。如果您的應用程式是模組化的,那麼擴充功能允許每個模組註冊和管理自己的服務設定。

擴充功能必須實作 ExtensionInterface,並且可以使用以下方式註冊到容器中

1
$container->registerExtension($extension);

擴充功能的主要工作是在 load() 方法中完成的。在 load() 方法中,您可以從一個或多個設定檔載入設定,以及使用如何使用服務定義物件中顯示的方法來操作容器定義。

load() 方法會傳遞一個新的容器來設定,然後將其合併到它註冊的容器中。這允許您擁有幾個擴充功能獨立地管理容器定義。擴充功能在添加時不會添加到容器的設定中,而是在呼叫容器的 compile() 方法時處理。

一個非常簡單的擴充功能可能只是將設定檔載入到容器中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;

class AcmeDemoExtension implements ExtensionInterface
{
    public function load(array $configs, ContainerBuilder $container): void
    {
        $loader = new XmlFileLoader(
            $container,
            new FileLocator(__DIR__.'/../Resources/config')
        );
        $loader->load('services.xml');
    }

    // ...
}

與直接將檔案載入到正在建置的整體容器相比,這並沒有獲得太多優勢。它只是允許檔案在模組/套件之間分割。需要能夠從模組/套件外部的設定檔影響模組的設定,才能使複雜的應用程式可配置。這可以通過將直接載入到容器中的設定檔的區段指定為特定擴充功能的區段來完成。配置上的這些區段將不會由容器直接處理,而是由相關的擴充功能處理。

擴充功能必須指定 getAlias() 方法來實作介面

1
2
3
4
5
6
7
8
9
10
11
// ...

class AcmeDemoExtension implements ExtensionInterface
{
    // ...

    public function getAlias(): string
    {
        return 'acme_demo';
    }
}

對於 YAML 設定檔,將擴充功能的別名指定為鍵,表示這些值將傳遞給擴充功能的 load() 方法

1
2
3
4
# ...
acme_demo:
    foo: fooValue
    bar: barValue

如果此檔案載入到設定中,則其中的值僅在容器編譯時處理,屆時將載入擴充功能

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

$container = new ContainerBuilder();
$container->registerExtension(new AcmeDemoExtension);

$loader = new YamlFileLoader($container, new FileLocator(__DIR__));
$loader->load('config.yaml');

// ...
$container->compile();

注意

當載入使用擴充功能別名作為鍵的設定檔時,擴充功能必須已註冊到容器建置器,否則將會拋出例外。

來自設定檔這些區段的值會傳遞到擴充功能的 load() 方法的第一個參數中

1
2
3
4
5
public function load(array $configs, ContainerBuilder $container): void
{
    $foo = $configs[0]['foo']; //fooValue
    $bar = $configs[0]['bar']; //barValue
}

$configs 參數是一個陣列,其中包含載入到容器中的每個不同的設定檔。在上面的範例中,您僅載入單個設定檔,但它仍然會在陣列中。該陣列將如下所示

1
2
3
4
5
6
[
    [
        'foo' => 'fooValue',
        'bar' => 'barValue',
    ],
]

雖然您可以手動管理合併不同的檔案,但最好使用Config 组件來合併和驗證設定值。使用設定處理,您可以通過這種方式存取設定值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Config\Definition\Processor;
// ...

public function load(array $configs, ContainerBuilder $container): void
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);

    $foo = $config['foo']; //fooValue
    $bar = $config['bar']; //barValue

    // ...
}

還有另外兩個您必須實作的方法。一個是傳回 XML 命名空間,以便 XML 設定檔的相關部分傳遞給擴充功能。另一個是指定 XSD 檔案的基本路徑,以驗證 XML 設定

1
2
3
4
5
6
7
8
9
public function getXsdValidationBasePath(): string
{
    return __DIR__.'/../Resources/config/';
}

public function getNamespace(): string
{
    return 'http://www.example.com/symfony/schema/';
}

注意

XSD 驗證是可選的,從 getXsdValidationBasePath() 方法傳回 false 將會停用它。

然後,XML 版本的設定將如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="https://symfony.dev.org.tw/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:acme-demo="http://www.example.com/schema/dic/acme_demo"
    xsi:schemaLocation="https://symfony.dev.org.tw/schema/dic/services
        https://symfony.dev.org.tw/schema/dic/services/services-1.0.xsd
        http://www.example.com/schema/dic/acme_demo
        https://www.example.com/schema/dic/acme_demo/acme_demo-1.0.xsd"
>
    <acme-demo:config>
        <acme_demo:foo>fooValue</acme_demo:foo>
        <acme_demo:bar>barValue</acme_demo:bar>
    </acme-demo:config>
</container>

注意

在 Symfony 全堆疊框架中,有一個基本的擴充功能類別實作了這些方法,以及用於處理設定的快捷方法。有關更多詳細資訊,請參閱如何在套件內載入服務設定

現在,處理後的設定值可以作為容器參數添加,就像它列在設定檔的 parameters 區段中一樣,但具有合併多個檔案和驗證設定的額外好處

1
2
3
4
5
6
7
8
9
10
public function load(array $configs, ContainerBuilder $container): void
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);

    $container->setParameter('acme_demo.FOO', $config['foo']);

    // ...
}

更複雜的設定需求可以在擴充功能類別中滿足。例如,您可以選擇載入主要的服務設定檔,但僅在設定了特定參數時才載入次要的服務設定檔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function load(array $configs, ContainerBuilder $container): void
{
    $configuration = new Configuration();
    $processor = new Processor();
    $config = $processor->processConfiguration($configuration, $configs);

    $loader = new XmlFileLoader(
        $container,
        new FileLocator(__DIR__.'/../Resources/config')
    );
    $loader->load('services.xml');

    if ($config['advanced']) {
        $loader->load('advanced.xml');
    }
}

您還可以在擴充功能中棄用容器參數,以警告使用者不要再使用它們。這有助於跨擴充功能的主要版本進行遷移。

棄用僅在使用 PHP 配置擴充功能時才有可能,而不是在使用 XML 或 YAML 時。使用 ContainerBuilder::deprecateParameter() 方法來提供棄用詳細資訊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function load(array $configs, ContainerBuilder $containerBuilder)
{
    // ...

    $containerBuilder->setParameter('acme_demo.database_user', $configs['db_user']);

    $containerBuilder->deprecateParameter(
        'acme_demo.database_user',
        'acme/database-package',
        '1.3',
        // optionally you can set a custom deprecation message
        '"acme_demo.database_user" is deprecated, you should configure database credentials with the "acme_demo.database_dsn" parameter instead.'
    );
}

被棄用的參數必須在宣告為已棄用之前設定。否則,將會拋出 ParameterNotFoundException 例外。

注意

僅將擴充功能註冊到容器不足以使其包含在容器編譯時處理的擴充功能中。載入使用擴充功能別名作為鍵的設定(如上述範例中所示)將確保載入它。也可以使用容器建置器的 loadFromExtension() 方法告知容器建置器載入它

1
2
3
4
5
6
7
use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$extension = new AcmeDemoExtension();
$container->registerExtension($extension);
$container->loadFromExtension($extension->getAlias());
$container->compile();

注意

如果您需要操作擴充功能載入的設定,那麼您無法從另一個擴充功能執行此操作,因為它使用新的容器。您應該改為使用編譯器 Pass,它在擴充功能處理完成後處理完整的容器。

預先設定傳遞給擴充功能的設定

擴充功能可以在 load() 方法被呼叫之前,通過實作 PrependExtensionInterface 來預先設定任何套件的設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
// ...

class AcmeDemoExtension implements ExtensionInterface, PrependExtensionInterface
{
    // ...

    public function prepend(ContainerBuilder $container): void
    {
        // ...

        $container->prependExtensionConfig($name, $config);

        // ...
    }
}

有關更多詳細資訊,請參閱如何簡化多個套件的設定,這特定於 Symfony 框架,但包含有關此功能的更多詳細資訊。

在編譯期間執行程式碼

您還可以通過編寫自己的編譯器 Pass 在編譯期間執行自訂程式碼。通過在您的擴充功能中實作 CompilerPassInterface,添加的 process() 方法將在編譯期間被呼叫

1
2
3
4
5
6
7
8
9
10
11
12
// ...
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;

class AcmeDemoExtension implements ExtensionInterface, CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        // ... do something during the compilation
    }

    // ...
}

由於 process() 是在所有擴充功能載入之後呼叫的,因此它允許您編輯其他擴充功能的服務定義,以及檢索有關服務定義的資訊。

可以使用如何使用服務定義物件中描述的方法來操作容器的參數和定義。

注意

請注意,擴充功能類別中的 process() 方法在 PassConfig::TYPE_BEFORE_OPTIMIZATION 步驟期間被呼叫。如果您需要在另一個步驟中編輯容器,可以閱讀下一節

注意

作為規則,僅在編譯器 Pass 中處理服務定義,並且不要建立服務實例。實際上,這表示使用方法 has()findDefinition()getDefinition()setDefinition() 等,而不是 get()set() 等。

提示

確保您的編譯器 Pass 不需要服務存在。如果某些必要的服務不可用,則中止方法呼叫。

編譯器 Pass 的常見用例是搜尋具有特定標籤的所有服務定義,以便將每個服務動態插入到其他服務中。有關範例,請參閱關於服務標籤的章節。

建立個別的編譯器 Pass

有時,您需要在編譯期間執行多項操作,想要在沒有擴充功能的情況下使用編譯器 Pass,或者您需要在編譯過程的另一個步驟中執行一些程式碼。在這些情況下,您可以建立一個新的類別來實作 CompilerPassInterface

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class CustomPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container): void
    {
        // ... do something during the compilation
    }
}

然後,您需要向容器註冊您的自訂 Pass

1
2
3
4
use Symfony\Component\DependencyInjection\ContainerBuilder;

$container = new ContainerBuilder();
$container->addCompilerPass(new CustomPass());

注意

如果您使用的是全堆疊框架,則編譯器 Pass 的註冊方式不同,有關更多詳細資訊,請參閱如何使用編譯器 Pass

控制 Pass 順序

預設的編譯器 Pass 分組為最佳化 Pass 和移除 Pass。最佳化 Pass 首先執行,包括解析定義內參考等任務。移除 Pass 執行諸如移除私有別名和未使用的服務等任務。當使用 addCompilerPass() 註冊編譯器 Pass 時,您可以配置何時執行您的編譯器 Pass。預設情況下,它們在最佳化 Pass 之前執行。

您可以使用以下常數來決定何時執行您的 Pass

  • PassConfig::TYPE_BEFORE_OPTIMIZATION
  • PassConfig::TYPE_OPTIMIZE
  • PassConfig::TYPE_BEFORE_REMOVING
  • PassConfig::TYPE_REMOVE
  • PassConfig::TYPE_AFTER_REMOVING

例如,要在預設移除 Pass 執行後執行您的自訂 Pass,請使用

1
2
3
4
5
// ...
$container->addCompilerPass(
    new CustomPass(),
    PassConfig::TYPE_AFTER_REMOVING
);

您還可以控制編譯 Pass 在每個編譯階段的執行順序。使用 addCompilerPass() 的可選第三個參數將優先順序設定為整數。預設優先順序為 0,並且其值越高,執行得越早

1
2
3
4
5
6
7
8
// ...
// FirstPass is executed after SecondPass because its priority is lower
$container->addCompilerPass(
    new FirstPass(), PassConfig::TYPE_AFTER_REMOVING, 10
);
$container->addCompilerPass(
    new SecondPass(), PassConfig::TYPE_AFTER_REMOVING, 30
);

傾印設定以提升效能

一旦服務數量眾多,使用設定檔來管理服務容器可能會比使用 PHP 更容易理解。然而,這種便利性是以效能為代價的,因為需要解析設定檔並從中建置 PHP 設定。編譯過程使容器更有效率,但執行需要時間。但是,您可以通過使用設定檔,然後傾印和快取產生的設定,從而獲得兩全其美的效果。PhpDumper 用於傾印編譯後的容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Dumper\PhpDumper;

$file = __DIR__ .'/cache/container.php';

if (file_exists($file)) {
    require_once $file;
    $container = new ProjectServiceContainer();
} else {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    $dumper = new PhpDumper($container);
    file_put_contents($file, $dumper->dump());
}

提示

file_put_contents() 函數不是原子操作。這可能會在具有多個並行請求的生產環境中引起問題。相反,請使用 Symfony Filesystem 组件中的 dumpFile() 方法 或 Symfony 提供的其他方法(例如 $containerConfigCache->write()),它們是原子操作。

ProjectServiceContainer 是傾印的容器類別的預設名稱。但是,您可以使用傾印時的 class 選項來變更此名稱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
$file = __DIR__ .'/cache/container.php';

if (file_exists($file)) {
    require_once $file;
    $container = new MyCachedContainer();
} else {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    $dumper = new PhpDumper($container);
    file_put_contents(
        $file,
        $dumper->dump(['class' => 'MyCachedContainer'])
    );
}

現在,您將獲得 PHP 配置容器的速度,以及使用設定檔的便利性。此外,以這種方式傾印容器還可以進一步最佳化容器建立服務的方式。

在上面的範例中,每當您進行任何變更時,都需要刪除快取的容器檔案。添加一個變數檢查來確定您是否處於偵錯模式,使您可以在生產環境中保持快取容器的速度,同時在開發應用程式時獲得最新的設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ...

// based on something in your project
$isDebug = ...;

$file = __DIR__ .'/cache/container.php';

if (!$isDebug && file_exists($file)) {
    require_once $file;
    $container = new MyCachedContainer();
} else {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    if (!$isDebug) {
        $dumper = new PhpDumper($container);
        file_put_contents(
            $file,
            $dumper->dump(['class' => 'MyCachedContainer'])
        );
    }
}

通過僅在偵錯模式下,當容器的設定發生變更時,而不是在每個請求時重新編譯容器,可以進一步改進這一點。這可以通過以組態组件文檔中的“基於資源的快取”中所述的方式快取用於配置容器的資源檔案來完成。

您無需找出要快取哪些檔案,因為容器建置器會追蹤用於配置它的所有資源,而不僅僅是設定檔,還包括擴充功能類別和編譯器 Pass。這表示對任何這些檔案的任何變更都將使快取失效,並觸發容器的重建。您需要向容器請求這些資源,並將它們用作快取的元數據

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ...

// based on something in your project
$isDebug = ...;

$file = __DIR__ .'/cache/container.php';
$containerConfigCache = new ConfigCache($file, $isDebug);

if (!$containerConfigCache->isFresh()) {
    $container = new ContainerBuilder();
    // ...
    $container->compile();

    $dumper = new PhpDumper($container);
    $containerConfigCache->write(
        $dumper->dump(['class' => 'MyCachedContainer']),
        $container->getResources()
    );
}

require_once $file;
$container = new MyCachedContainer();

現在,無論是否啟用偵錯模式,都會使用快取的傾印容器。不同之處在於 ConfigCache 的第二個建構子參數設定為偵錯模式。當快取未處於偵錯模式時,如果快取存在,則將始終使用快取的容器。在偵錯模式下,將寫入包含所有涉及的資源檔案的額外元數據檔案。然後檢查這些檔案以查看其時間戳是否已更改,如果已更改,則快取將被視為過時。

注意

在全堆疊框架中,容器的編譯和快取由您負責處理。

這項工作,包括程式碼範例,已根據 Creative Commons BY-SA 3.0 授權條款授權。
目錄
    版本