- 閱讀時間 7 分鐘

Symfony 的自動組態

Symfony
Markdown logo

Symfony 框架中非常強大的機制之一是自動組態系統。預設啟用時,它會根據我們類別的性質,容器中分類我們的服務。

運作方式

服務容器中,我們會發現非常多的服務,它們來自不同的地方:Symfony 組件、函式庫,甚至是我們應用程式中編寫的服務。

服務容器中,分類這些服務變得必要。目標很簡單:如果我有一個要執行的動作,擁有讓我執行此動作的服務集合會很有用,手動尋找能解決我問題的服務而不是。

啟用

組態檔中加入一行就啟用自動組態。

config/services.yaml 檔案中,預設有指令:autoconfigure: true

範例:Voters(投票者)

Symfony 中,Voters的系統讓安全性組件根據情況做出決定,並管理權限:使用者是否有權對資源執行動作嗎?

要建立投票者,Symfony 建議建立一個實作 VoterInterface 的類別,或者更好的是繼承 Voter 抽象類別。

例如,如果我想要對 Project 實體的項目進行存取控制,我可以建立一個繼承 VoterProjectVoter 類別:

<?php

namespace App\Security\Voter;

use App\Entity\Project;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;

class ProjectVoter extends Voter
{
    protected function supports($attribute, $subject): bool
    {
        return in_array($attribute, ['VIEW', 'EDIT'])
            && $subject instanceof Project;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
    {
        // 做出決定:回傳 true 或 false
    }
}

一旦我們的類別編譯完成,我們可以在服務容器中檢查它:

php bin/console debug:container ProjectVoter

我們可以看到我們服務的屬性中,有一個 Tags 項目,裡面有 security.voter 值。

標記系統(Tag System)

自動組態機制分析投票者時,已將我們的服務與安全性組件的其他投票者集合。為了將它放在正確的位置,只是發現服務繼承 Voter 抽象類別。

分類是透過標記系統完成的:一個標籤,用於更好地分辨服務的性質。

我們之前提到,Voter 抽象類別實作 VoterInterface 介面。事實上,是這個介面被標記為自動組態。Symfony 安全性套件中可以看到 security.voter 標記:

// security-bundle/DependencyInjection/SecurityExtension.php

$container->registerForAutoconfiguration(VoterInterface::class)
    ->addTag('security.voter');

編譯服務容器時,AddSecurityVotersPass 類別會擷取標記的服務,並將它們註冊到 AccessDecisionManager 中:

// security-bundle/DependencyInjection/Compiler/AddSecurityVoterPass.php

class AddSecurityVotersPass implements CompilerPassInterface
{
    //...

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

        // 擷取投票者
        $voters = $this->findAndSortTaggedServices('security.voter', $container);

        // 建立一個包含服務的 $voterServices 陣列(含或不含除錯)...

        // 將投票者陣列傳遞給 AccessDecisionManager
        $container->getDefinition('security.access.decision_manager')
            ->replaceArgument(0, new IteratorArgument($voterServices));
    }
}

AccessDecisionManagercollectResults 的函式,它會實際執行投票並擷取結果:

// security-core/Authorization/AccessDecisionManager.php

private function collectResults(TokenInterface $token, array $attributes, mixed $object, AccessDecision $accessDecision): \Traversable
{
    foreach ($this->getVoters($attributes, $object) as $voter) {
        $vote = new Vote();
        $result = $voter->vote($token, $object, $attributes, $vote);

        //...
    }
}

我們可以看到,這會呼叫每個投票者的 vote 函式!

多型

AccessDecisionManager 類別中,將每個已註冊的投票者呼叫 vote 函式並收集結果。

AccessDecisionManager 類別的觀點來看,這不是一個問題,因為包含在此類別中的所有投票者都被視為實作 VoterInterface 型別的實體:

// security-core/Authorization/AccessDecisionManager.php

final class AccessDecisionManager implements AccessDecisionManagerInterface
{
    //...

    /**
     * @param iterable<mixed, VoterInterface> $voters An array or an iterator of VoterInterface instances
     */
    public function __construct(
        private iterable $voters = [],
        //...
    ) {
        //...
    }
}

因此,Voter 抽象類別實作 VoterInterface,所以會有 vote 函式的定義。函式中會呼叫 supportsvoteOnAttribute 函式。

此機制讓我們定義自己的程式碼,並將我們自己的權限邏輯掛接到 Symfony 安全性組件中。安全性組件中,行為是多型的:呼叫投票者時,將它們都視為實作抽象的實體:無論 vote 函式如何編譯,我們知道如果實作了 VoterInterface,則類別必須提供 vote 函式的定義。

Voter 抽象類別宣告兩個抽象函式:supportsvoteOnAttribute:這強制實作的類別提供這兩個函式的定義。因為自動組態機制及投票者繼承 Voter 類別,所有實作權限機制的類別都會自動註冊並分類到安全性組件中!

其他範例

Symfony 還會偵測其他實作或繼承,以便分類服務。甚至可以使用 PHP8 屬性來觸發自動組態。

Normalizers

Normalizers 負責將資料轉換為陣列。Symfony Serializer 組件中已經有內建的 normalizers。但也可以建立自己的 normalizer。

只要建立一個實作 NormalizerInterface 介面的類別,然後實作合約的 3 個函式:normalizesupportsNormalizationgetSupportedTypes

自動組態機制會自動偵測實作 NormalizerInterface 的類別,並使用 serializer.normalizer 標記註冊它們,以便在 normalization 過程中可用。

終端機指令

Symfony 應用程式中可以建立自己的終端機指令。

為此,只要建立一個類別,而上面使用 #[AsCommand] PHP8 屬性。Symfony 會自動將 console.command 標記新增到類別,指令將可透過 php bin/console 使用。