跳到內容

擴展操作引數解析

編輯此頁

控制器指南中,您已了解可以透過控制器中的引數取得 Request 物件。此引數必須以 Request 類別進行類型提示才能被識別。這是透過 ArgumentResolver 完成的。透過建立和註冊自訂值解析器,您可以擴展此功能。

內建的值解析器

Symfony 隨附 HttpKernel component 中的下列值解析器

BackedEnumValueResolver

嘗試從符合引數名稱的路由路徑參數解析 backed enum case。如果值不是 enum 類型的有效 backing value,則會導致 404 Not Found 回應。

例如,如果您的 backed enum 是

1
2
3
4
5
6
7
8
9
namespace App\Model;

enum Suit: string
{
    case Hearts = 'H';
    case Diamonds = 'D';
    case Clubs = 'C';
    case Spades = 'S';
}

而您的控制器包含以下內容

1
2
3
4
5
6
7
8
9
10
class CardController
{
    #[Route('/cards/{suit}')]
    public function list(Suit $suit): Response
    {
        // ...
    }

    // ...
}

當請求 /cards/H URL 時,$suit 變數將會儲存 Suit::Hearts case。

此外,您可以使用 EnumRequirement 將路由參數的允許值限制為僅一個(或多個)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Symfony\Component\Routing\Requirement\EnumRequirement;

// ...

class CardController
{
    #[Route('/cards/{suit}', requirements: [
        // this allows all values defined in the Enum
        'suit' => new EnumRequirement(Suit::class),
        // this restricts the possible values to the Enum values listed here
        'suit' => new EnumRequirement([Suit::Diamonds, Suit::Spades]),
    ])]
    public function list(Suit $suit): Response
    {
        // ...
    }

    // ...
}

上面的範例僅允許請求 /cards/D/cards/S URL,在其他兩種情況下會導致 404 Not Found 回應。

RequestPayloadValueResolver

將請求酬載或查詢字串對應到類型提示物件中。

由於這是目標值解析器,您必須使用 MapRequestPayloadMapQueryString 屬性才能使用此解析器。

RequestAttributeValueResolver
嘗試尋找符合引數名稱的請求屬性。
DateTimeValueResolver

嘗試尋找符合引數名稱的請求屬性,並在以擴展 DateTimeInterface 的類別進行類型提示時,注入 DateTimeInterface 物件。

預設情況下,任何可以被 PHP 解析為日期字串的輸入都會被接受。您可以使用 MapDateTime 屬性來限制輸入的格式。

提示

DateTimeInterface 物件是使用 Clock component 產生的。這讓您可以完全控制控制器在測試應用程式和使用 MockClock 實作時接收的日期和時間值。

RequestValueResolver
如果類型提示為 Request 或擴展 Request 的類別,則注入目前的 Request
ServiceValueResolver
如果類型提示為有效的服務類別或介面,則注入服務。這就像自動裝配一樣運作。
SessionValueResolver
如果類型提示為 SessionInterface 或實作 SessionInterface 的類別,則注入已設定的實作 SessionInterface 的 session 類別。
DefaultValueResolver
如果引數存在且為選用引數,則會設定引數的預設值。
UidValueResolver

嘗試將路由路徑參數中的任何 UID 值轉換為 UID 物件。如果值不是有效的 UID,則會導致 404 Not Found 回應。

例如,以下程式碼會將 token 參數轉換為 UuidV4 物件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Controller/DefaultController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Uid\UuidV4;

class DefaultController
{
    #[Route('/share/{token}')]
    public function share(UuidV4 $token): Response
    {
        // ...
    }
}
VariadicValueResolver
驗證請求資料是否為陣列,並將所有資料新增至引數清單。當呼叫動作時,最後一個(可變引數)引數將會包含此陣列的所有值。

此外,某些元件、橋接器和官方套件組合還提供其他值解析器

UserValueResolver

如果類型提示為 UserInterface,則注入代表目前已登入使用者的物件。您也可以類型提示您自己的 User 類別,但您必須接著將 #[CurrentUser] 屬性新增至引數。如果匿名使用者可以存取控制器,則預設值可以設定為 null。這需要安裝 SecurityBundle

如果引數不可為 null,且沒有已登入的使用者,或已登入的使用者具有與類型提示類別不符的使用者類別,則解析器會擲回 AccessDeniedException 以防止存取控制器。

SecurityTokenValueResolver

如果類型提示為 TokenInterface 或擴展它的類別,則注入代表目前已登入 token 的物件。

如果引數不可為 null,且沒有已登入的 token,則解析器會擲回狀態碼為 401 的 HttpException,以防止存取控制器。

EntityValueResolver

自動查詢實體並將其作為引數傳遞至您的控制器。

例如,以下程式碼將查詢將 {id} 作為主要金鑰的 Product 實體

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Controller/DefaultController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class DefaultController
{
    #[Route('/product/{id}')]
    public function share(Product $product): Response
    {
        // ...
    }
}

若要進一步了解 EntityValueResolver 的使用方式,請參閱專門章節自動抓取物件

PSR-7 Objects Resolver
注入從 Psr\Http\Message\ServerRequestInterfacePsr\Http\Message\RequestInterfacePsr\Http\Message\MessageInterface 類型的 PSR-7 物件建立的 Symfony HttpFoundation Request 物件。這需要安裝 PSR-7 Bridge 元件。

管理值解析器

對於每個引數,將會呼叫每個標記為 controller.argument_value_resolver 的解析器,直到其中一個解析器提供值為止。呼叫它們的順序取決於它們的優先順序。例如,SessionValueResolver 將會在 DefaultValueResolver 之前呼叫,因為它的優先順序較高。這允許編寫例如 SessionInterface $session = null 以在有 session 時取得 session,或者在沒有 session 時取得 null

在該特定情況下,您不需要在 SessionValueResolver 之前執行任何解析器,因此略過它們不僅可以提高效能,還可以防止其中一個解析器在 SessionValueResolver 有機會之前提供值。

ValueResolver 屬性可讓您透過「目標」您想要的解析器來執行此操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Controller/SessionController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver;
use Symfony\Component\Routing\Attribute\Route;

class SessionController
{
    #[Route('/')]
    public function __invoke(
        #[ValueResolver(SessionValueResolver::class)]
        SessionInterface $session = null
    ): Response
    {
        // ...
    }
}

在上面的範例中,SessionValueResolver 將會先被呼叫,因為它是目標。如果沒有提供值,則接下來將會呼叫 DefaultValueResolver;這就是為什麼您可以將 null 指派為 $session 的預設值。

您可以透過傳遞其名稱作為 ValueResolver 的第一個引數來鎖定解析器。為了方便起見,內建解析器的名稱是它們的 FQCN。

也可以透過將 ValueResolver$disabled 引數傳遞為 true 來停用目標解析器;這就是 MapEntity 允許針對特定控制器停用 EntityValueResolver 的方式。是的,MapEntity 擴展了 ValueResolver

新增自訂值解析器

在下一個範例中,您將建立一個值解析器,以便在控制器引數具有實作 IdentifierInterface 的類型(例如 BookingId)時注入 ID 值物件

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/Controller/BookingController.php
namespace App\Controller;

use App\Reservation\BookingId;
use Symfony\Component\HttpFoundation\Response;

class BookingController
{
    public function index(BookingId $id): Response
    {
        // ... do something with $id
    }
}

新增值解析器需要建立實作 ValueResolverInterface 的類別,並為其定義服務。

此介面包含一個 resolve() 方法,該方法會針對控制器的每個引數呼叫。它會接收目前的 Request 物件和 ArgumentMetadata 執行個體,其中包含方法簽章中的所有資訊。

resolve() 方法應傳回空陣列(如果無法解析此引數)或包含已解析值的陣列。通常,引數會解析為單一值,但可變引數需要解析多個值。這就是為什麼您必須始終傳回陣列,即使是單一值也是如此

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/ValueResolver/IdentifierValueResolver.php
namespace App\ValueResolver;

use App\IdentifierInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

class BookingIdValueResolver implements ValueResolverInterface
{
    public function resolve(Request $request, ArgumentMetadata $argument): iterable
    {
        // get the argument type (e.g. BookingId)
        $argumentType = $argument->getType();
        if (
            !$argumentType
            || !is_subclass_of($argumentType, IdentifierInterface::class, true)
        ) {
            return [];
        }

        // get the value from the request, based on the argument name
        $value = $request->attributes->get($argument->getName());
        if (!is_string($value)) {
            return [];
        }

        // create and return the value object
        return [$argumentType::fromString($value)];
    }
}

此方法首先檢查它是否可以解析值

  • 引數必須以實作自訂 IdentifierInterface 的類別進行類型提示;
  • 引數名稱(例如 $id)必須符合請求屬性的名稱(例如使用 /booking/{id} 路由預留位置)。

當滿足這些需求時,該方法會建立自訂值物件的新執行個體,並將其作為此引數的值傳回。

就這樣!現在您要做的就是新增服務容器的組態。這可以透過將以下其中一個標籤新增至您的值解析器來完成。

controller.argument_value_resolver

此標籤會自動新增至每個實作 ValueResolverInterface 的服務,但您可以自行設定它以變更其 priorityname 屬性。

1
2
3
4
5
6
7
8
9
10
11
12
# config/services.yaml
services:
    _defaults:
        # ... be sure autowiring is enabled
        autowire: true
    # ...

    App\ValueResolver\BookingIdValueResolver:
        tags:
            - controller.argument_value_resolver:
                name: booking_id
                priority: 150

雖然新增優先順序是選用的,但建議新增一個優先順序以確保注入預期的值。內建的 RequestAttributeValueResolver(從 Request 提取屬性)的優先順序為 100。如果您的解析器也提取 Request 屬性,請將優先順序設定為 100 或更高。否則,請設定低於 100 的優先順序,以確保在 Request 屬性存在時不會觸發引數解析器。

為了確保您的解析器新增在正確的位置,您可以執行下列命令來查看目前有哪些引數解析器以及它們的執行順序

1
$ php bin/console debug:container debug.argument_resolver.inner --show-arguments

您也可以設定傳遞至 ValueResolver 屬性的名稱,以鎖定您的解析器。否則,它將預設為服務的 ID。

controller.targeted_value_resolver

如果您希望僅在 ValueResolver 屬性鎖定您的解析器時才呼叫它,請設定此標籤。與 controller.argument_value_resolver 類似,您可以自訂可以鎖定您的解析器的名稱。

或者,您可以將 AsTargetedValueResolver 屬性新增至您的解析器,並將您的自訂名稱作為其第一個引數傳遞

1
2
3
4
5
6
7
8
9
10
11
// src/ValueResolver/IdentifierValueResolver.php
namespace App\ValueResolver;

use Symfony\Component\HttpKernel\Attribute\AsTargetedValueResolver;
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;

#[AsTargetedValueResolver('booking_id')]
class BookingIdValueResolver implements ValueResolverInterface
{
    // ...
}

然後,您可以將此名稱作為 ValueResolver 的第一個引數傳遞,以鎖定您的解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Controller/BookingController.php
namespace App\Controller;

use App\Reservation\BookingId;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\ValueResolver;

class BookingController
{
    public function index(#[ValueResolver('booking_id')] BookingId $id): Response
    {
        // ... do something with $id
    }
}
這項工作,包括程式碼範例,均根據 Creative Commons BY-SA 3.0 授權條款授權。
目錄
    版本