擴展操作引數解析
在控制器指南中,您已了解可以透過控制器中的引數取得 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
-
將請求酬載或查詢字串對應到類型提示物件中。
由於這是目標值解析器,您必須使用 MapRequestPayload 或 MapQueryString 屬性才能使用此解析器。
- 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\ServerRequestInterface
、Psr\Http\Message\RequestInterface
或Psr\Http\Message\MessageInterface
類型的 PSR-7 物件建立的 Symfony HttpFoundationRequest
物件。這需要安裝 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
的服務,但您可以自行設定它以變更其 priority
或 name
屬性。
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
}
}