Repository: symfony/http-kernel Branch: 8.1 Commit: 47bc33d92b1c Files: 339 Total size: 1.4 MB Directory structure: gitextract_533_a94u/ ├── .gitattributes ├── .github/ │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ └── close-pull-request.yml ├── .gitignore ├── Attribute/ │ ├── AsController.php │ ├── AsTargetedValueResolver.php │ ├── Cache.php │ ├── IsSignatureValid.php │ ├── MapDateTime.php │ ├── MapQueryParameter.php │ ├── MapQueryString.php │ ├── MapRequestHeader.php │ ├── MapRequestPayload.php │ ├── MapUploadedFile.php │ ├── Serialize.php │ ├── ValueResolver.php │ ├── WithHttpStatus.php │ └── WithLogLevel.php ├── Bundle/ │ ├── AbstractBundle.php │ ├── Bundle.php │ ├── BundleExtension.php │ └── BundleInterface.php ├── CHANGELOG.md ├── CacheClearer/ │ ├── CacheClearerInterface.php │ ├── ChainCacheClearer.php │ └── Psr6CacheClearer.php ├── CacheWarmer/ │ ├── CacheWarmer.php │ ├── CacheWarmerAggregate.php │ ├── CacheWarmerInterface.php │ └── WarmableInterface.php ├── Config/ │ └── FileLocator.php ├── Controller/ │ ├── ArgumentResolver/ │ │ ├── BackedEnumValueResolver.php │ │ ├── DateTimeValueResolver.php │ │ ├── DefaultValueResolver.php │ │ ├── NotTaggedControllerValueResolver.php │ │ ├── QueryParameterValueResolver.php │ │ ├── RequestAttributeValueResolver.php │ │ ├── RequestHeaderValueResolver.php │ │ ├── RequestPayloadValueResolver.php │ │ ├── RequestValueResolver.php │ │ ├── ServiceValueResolver.php │ │ ├── SessionValueResolver.php │ │ ├── TraceableValueResolver.php │ │ ├── UidValueResolver.php │ │ └── VariadicValueResolver.php │ ├── ArgumentResolver.php │ ├── ArgumentResolverInterface.php │ ├── ContainerControllerResolver.php │ ├── ControllerReference.php │ ├── ControllerResolver.php │ ├── ControllerResolverInterface.php │ ├── ErrorController.php │ ├── TraceableArgumentResolver.php │ ├── TraceableControllerResolver.php │ └── ValueResolverInterface.php ├── ControllerMetadata/ │ ├── ArgumentMetadata.php │ ├── ArgumentMetadataFactory.php │ └── ArgumentMetadataFactoryInterface.php ├── DataCollector/ │ ├── AjaxDataCollector.php │ ├── ConfigDataCollector.php │ ├── DataCollector.php │ ├── DataCollectorInterface.php │ ├── DumpDataCollector.php │ ├── EventDataCollector.php │ ├── ExceptionDataCollector.php │ ├── LateDataCollectorInterface.php │ ├── LoggerDataCollector.php │ ├── MemoryDataCollector.php │ ├── RequestDataCollector.php │ ├── RouterDataCollector.php │ └── TimeDataCollector.php ├── Debug/ │ ├── ErrorHandlerConfigurator.php │ ├── TraceableEventDispatcher.php │ └── VirtualRequestStack.php ├── DependencyInjection/ │ ├── ConfigurableExtension.php │ ├── ControllerArgumentValueResolverPass.php │ ├── ControllerAttributesListenerPass.php │ ├── Extension.php │ ├── FragmentRendererPass.php │ ├── LazyLoadingFragmentHandler.php │ ├── LoggerPass.php │ ├── MergeExtensionConfigurationPass.php │ ├── RegisterControllerArgumentLocatorsPass.php │ ├── RegisterLocaleAwareServicesPass.php │ ├── RemoveEmptyControllerArgumentLocatorsPass.php │ ├── ResettableServicePass.php │ ├── ServicesResetter.php │ └── ServicesResetterInterface.php ├── Event/ │ ├── ControllerArgumentsEvent.php │ ├── ControllerArgumentsMetadata.php │ ├── ControllerAttributeEvent.php │ ├── ControllerEvent.php │ ├── ControllerMetadata.php │ ├── ExceptionEvent.php │ ├── FinishRequestEvent.php │ ├── KernelEvent.php │ ├── RequestEvent.php │ ├── ResponseEvent.php │ ├── TerminateEvent.php │ └── ViewEvent.php ├── EventListener/ │ ├── AbstractSessionListener.php │ ├── AddRequestFormatsListener.php │ ├── CacheAttributeListener.php │ ├── ControllerAttributesListener.php │ ├── DebugHandlersListener.php │ ├── DisallowRobotsIndexingListener.php │ ├── DumpListener.php │ ├── ErrorListener.php │ ├── FragmentListener.php │ ├── IsSignatureValidAttributeListener.php │ ├── LocaleAwareListener.php │ ├── LocaleListener.php │ ├── ProfilerListener.php │ ├── ResponseListener.php │ ├── RouterListener.php │ ├── SerializeControllerResultAttributeListener.php │ ├── SessionListener.php │ ├── SurrogateListener.php │ └── ValidateRequestListener.php ├── Exception/ │ ├── AccessDeniedHttpException.php │ ├── BadRequestHttpException.php │ ├── ConflictHttpException.php │ ├── ControllerDoesNotReturnResponseException.php │ ├── GoneHttpException.php │ ├── HttpException.php │ ├── HttpExceptionInterface.php │ ├── InvalidMetadataException.php │ ├── LengthRequiredHttpException.php │ ├── LockedHttpException.php │ ├── MethodNotAllowedHttpException.php │ ├── NearMissValueResolverException.php │ ├── NotAcceptableHttpException.php │ ├── NotFoundHttpException.php │ ├── PreconditionFailedHttpException.php │ ├── PreconditionRequiredHttpException.php │ ├── ResolverNotFoundException.php │ ├── ServiceUnavailableHttpException.php │ ├── TooManyRequestsHttpException.php │ ├── UnauthorizedHttpException.php │ ├── UnexpectedSessionUsageException.php │ ├── UnprocessableEntityHttpException.php │ └── UnsupportedMediaTypeHttpException.php ├── Fragment/ │ ├── AbstractSurrogateFragmentRenderer.php │ ├── EsiFragmentRenderer.php │ ├── FragmentHandler.php │ ├── FragmentRendererInterface.php │ ├── FragmentUriGenerator.php │ ├── FragmentUriGeneratorInterface.php │ ├── HIncludeFragmentRenderer.php │ ├── InlineFragmentRenderer.php │ ├── RoutableFragmentRenderer.php │ └── SsiFragmentRenderer.php ├── HttpCache/ │ ├── AbstractSurrogate.php │ ├── CacheWasLockedException.php │ ├── Esi.php │ ├── HttpCache.php │ ├── ResponseCacheStrategy.php │ ├── ResponseCacheStrategyInterface.php │ ├── Ssi.php │ ├── Store.php │ ├── StoreInterface.php │ ├── SubRequestHandler.php │ └── SurrogateInterface.php ├── HttpClientKernel.php ├── HttpKernel.php ├── HttpKernelBrowser.php ├── HttpKernelInterface.php ├── Kernel.php ├── KernelEvents.php ├── KernelInterface.php ├── LICENSE ├── Log/ │ ├── DebugLoggerConfigurator.php │ ├── DebugLoggerInterface.php │ └── Logger.php ├── Profiler/ │ ├── FileProfilerStorage.php │ ├── Profile.php │ ├── Profiler.php │ ├── ProfilerStateChecker.php │ └── ProfilerStorageInterface.php ├── README.md ├── RebootableInterface.php ├── Resources/ │ └── welcome.html.php ├── TerminableInterface.php ├── Tests/ │ ├── Attribute/ │ │ └── WithLogLevelTest.php │ ├── Bundle/ │ │ └── BundleTest.php │ ├── CacheClearer/ │ │ ├── ChainCacheClearerTest.php │ │ └── Psr6CacheClearerTest.php │ ├── CacheWarmer/ │ │ ├── CacheWarmerAggregateTest.php │ │ └── CacheWarmerTest.php │ ├── Config/ │ │ └── FileLocatorTest.php │ ├── Controller/ │ │ ├── ArgumentResolver/ │ │ │ ├── BackedEnumValueResolverTest.php │ │ │ ├── DateTimeValueResolverTest.php │ │ │ ├── NotTaggedControllerValueResolverTest.php │ │ │ ├── QueryParameterValueResolverTest.php │ │ │ ├── RequestAttributeValueResolverTest.php │ │ │ ├── RequestHeaderValueResolverTest.php │ │ │ ├── RequestPayloadValueResolverTest.php │ │ │ ├── RequestValueResolverTest.php │ │ │ ├── ServiceValueResolverTest.php │ │ │ ├── TraceableValueResolverTest.php │ │ │ ├── UidValueResolverTest.php │ │ │ └── UploadedFileValueResolverTest.php │ │ ├── ArgumentResolverTest.php │ │ ├── ContainerControllerResolverTest.php │ │ ├── ControllerResolverTest.php │ │ ├── ErrorControllerTest.php │ │ ├── TraceableArgumentResolverTest.php │ │ └── TraceableControllerResolverTest.php │ ├── ControllerMetadata/ │ │ ├── ArgumentMetadataFactoryTest.php │ │ └── ArgumentMetadataTest.php │ ├── DataCollector/ │ │ ├── Compiler.log │ │ ├── ConfigDataCollectorTest.php │ │ ├── DataCollectorTest.php │ │ ├── DumpDataCollectorTest.php │ │ ├── ExceptionDataCollectorTest.php │ │ ├── LoggerDataCollectorTest.php │ │ ├── MemoryDataCollectorTest.php │ │ ├── RequestDataCollectorTest.php │ │ ├── RouterDataCollectorTest.php │ │ └── TimeDataCollectorTest.php │ ├── Debug/ │ │ ├── ErrorHandlerConfiguratorTest.php │ │ └── TraceableEventDispatcherTest.php │ ├── DependencyInjection/ │ │ ├── ControllerArgumentValueResolverPassTest.php │ │ ├── ControllerAttributesListenerPassTest.php │ │ ├── FragmentRendererPassTest.php │ │ ├── LazyLoadingFragmentHandlerTest.php │ │ ├── LoggerPassTest.php │ │ ├── MergeExtensionConfigurationPassTest.php │ │ ├── RegisterControllerArgumentLocatorsPassTest.php │ │ ├── RegisterLocaleAwareServicesPassTest.php │ │ ├── RemoveEmptyControllerArgumentLocatorsPassTest.php │ │ ├── ResettableServicePassTest.php │ │ └── ServicesResetterTest.php │ ├── Event/ │ │ ├── ControllerArgumentsEventTest.php │ │ ├── ControllerAttributeEventTest.php │ │ ├── ControllerEventTest.php │ │ └── ExceptionEventTest.php │ ├── EventListener/ │ │ ├── AddRequestFormatsListenerTest.php │ │ ├── CacheAttributeListenerTest.php │ │ ├── ControllerAttributesListenerTest.php │ │ ├── DebugHandlersListenerTest.php │ │ ├── DisallowRobotsIndexingListenerTest.php │ │ ├── DumpListenerTest.php │ │ ├── ErrorListenerTest.php │ │ ├── FragmentListenerTest.php │ │ ├── IsSignatureValidAttributeListenerTest.php │ │ ├── LocaleAwareListenerTest.php │ │ ├── LocaleListenerTest.php │ │ ├── ProfilerListenerTest.php │ │ ├── ResponseListenerTest.php │ │ ├── RouterListenerTest.php │ │ ├── SerializeControllerResultListenerTest.php │ │ ├── SessionListenerTest.php │ │ ├── SurrogateListenerTest.php │ │ └── ValidateRequestListenerTest.php │ ├── Exception/ │ │ ├── AccessDeniedHttpExceptionTest.php │ │ ├── BadRequestHttpExceptionTest.php │ │ ├── ConflictHttpExceptionTest.php │ │ ├── GoneHttpExceptionTest.php │ │ ├── HttpExceptionTest.php │ │ ├── LengthRequiredHttpExceptionTest.php │ │ ├── LockedHttpExceptionTest.php │ │ ├── MethodNotAllowedHttpExceptionTest.php │ │ ├── NotAcceptableHttpExceptionTest.php │ │ ├── NotFoundHttpExceptionTest.php │ │ ├── PreconditionFailedHttpExceptionTest.php │ │ ├── PreconditionRequiredHttpExceptionTest.php │ │ ├── ServiceUnavailableHttpExceptionTest.php │ │ ├── TooManyRequestsHttpExceptionTest.php │ │ ├── UnauthorizedHttpExceptionTest.php │ │ ├── UnprocessableEntityHttpExceptionTest.php │ │ └── UnsupportedMediaTypeHttpExceptionTest.php │ ├── Fixtures/ │ │ ├── AcmeFooBundle/ │ │ │ ├── AcmeFooBundle.php │ │ │ └── Resources/ │ │ │ └── config/ │ │ │ ├── definition.php │ │ │ └── services.php │ │ ├── Attribute/ │ │ │ ├── Bar.php │ │ │ ├── Baz.php │ │ │ ├── Buz.php │ │ │ ├── Foo.php │ │ │ ├── Qux.php │ │ │ └── SubBuz.php │ │ ├── Bundle1Bundle/ │ │ │ ├── Resources/ │ │ │ │ └── .gitkeep │ │ │ └── foo.txt │ │ ├── BundleCompilerPass/ │ │ │ └── BundleAsCompilerPassBundle.php │ │ ├── ClearableService.php │ │ ├── Controller/ │ │ │ ├── ArgumentResolver/ │ │ │ │ └── UploadedFile/ │ │ │ │ ├── file-big.txt │ │ │ │ └── file-small.txt │ │ │ ├── AttributeController.php │ │ │ ├── BasicTypesController.php │ │ │ ├── CacheAttributeController.php │ │ │ ├── ControllerAttributesController.php │ │ │ ├── ExtendingRequest.php │ │ │ ├── ExtendingSession.php │ │ │ ├── NullableController.php │ │ │ └── VariadicController.php │ │ ├── DataCollector/ │ │ │ ├── CloneVarDataCollector.php │ │ │ └── DummyController.php │ │ ├── ExtensionNotValidBundle/ │ │ │ ├── DependencyInjection/ │ │ │ │ └── ExtensionNotValidExtension.php │ │ │ └── ExtensionNotValidBundle.php │ │ ├── ExtensionPresentBundle/ │ │ │ ├── DependencyInjection/ │ │ │ │ └── ExtensionPresentExtension.php │ │ │ └── ExtensionPresentBundle.php │ │ ├── IntEnum.php │ │ ├── IsSignatureValidAttributeController.php │ │ ├── IsSignatureValidAttributeMethodsController.php │ │ ├── KernelWithoutBundles.php │ │ ├── LazyResettableService.php │ │ ├── MockableUploadFileWithClientSize.php │ │ ├── MultiResettableService.php │ │ ├── ResettableService.php │ │ ├── Suit.php │ │ ├── TestClient.php │ │ ├── UsePropertyInDestruct.php │ │ └── WithPublicObjectProperty.php │ ├── Fragment/ │ │ ├── EsiFragmentRendererTest.php │ │ ├── FragmentHandlerTest.php │ │ ├── HIncludeFragmentRendererTest.php │ │ ├── InlineFragmentRendererTest.php │ │ ├── RoutableFragmentRendererTest.php │ │ └── SsiFragmentRendererTest.php │ ├── HttpCache/ │ │ ├── EsiTest.php │ │ ├── HttpCacheTest.php │ │ ├── HttpCacheTestCase.php │ │ ├── ResponseCacheStrategyTest.php │ │ ├── SsiTest.php │ │ ├── StoreTest.php │ │ ├── SubRequestHandlerTest.php │ │ ├── TestHttpKernel.php │ │ └── TestMultipleHttpKernel.php │ ├── HttpClientKernelTest.php │ ├── HttpKernelBrowserTest.php │ ├── HttpKernelTest.php │ ├── KernelTest.php │ ├── Log/ │ │ └── LoggerTest.php │ ├── Logger.php │ ├── Profiler/ │ │ ├── FileProfilerStorageTest.php │ │ └── ProfilerTest.php │ └── TestHttpKernel.php ├── composer.json └── phpunit.xml.dist ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ /Tests export-ignore /phpunit.xml.dist export-ignore /.git* export-ignore ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ Please do not submit any Pull Requests here. They will be closed. --- Please submit your PR here instead: https://github.com/symfony/symfony This repository is what we call a "subtree split": a read-only subset of that main repository. We're looking forward to your PR there! ================================================ FILE: .github/workflows/close-pull-request.yml ================================================ name: Close Pull Request on: pull_request_target: types: [opened] jobs: run: runs-on: ubuntu-latest steps: - uses: superbrothers/close-pull-request@v3 with: comment: | Thanks for your Pull Request! We love contributions. However, you should instead open your PR on the main repository: https://github.com/symfony/symfony This repository is what we call a "subtree split": a read-only subset of that main repository. We're looking forward to your PR there! ================================================ FILE: .gitignore ================================================ vendor/ composer.lock phpunit.xml Tests/Fixtures/cache/ Tests/Fixtures/logs/ ================================================ FILE: Attribute/AsController.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; /** * Autoconfigures controllers as services by applying * the `controller.service_arguments` tag to them. * * This enables injecting services as method arguments in addition * to other conventional dependency injection strategies. */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_FUNCTION)] class AsController { } ================================================ FILE: Attribute/AsTargetedValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; /** * Service tag to autoconfigure targeted value resolvers. */ #[\Attribute(\Attribute::TARGET_CLASS)] class AsTargetedValueResolver { /** * @param string|null $name The name with which the resolver can be targeted */ public function __construct(public readonly ?string $name = null) { } } ================================================ FILE: Attribute/Cache.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\Request; /** * Describes the default HTTP cache headers on controllers. * Headers defined in the Cache attribute are ignored if they are already set * by the controller. * * @see https://symfony.com/doc/current/http_cache.html#making-your-responses-http-cacheable * * @author Fabien Potencier */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::IS_REPEATABLE)] final class Cache { /** * @internal */ public public(set) readonly array $variables; public function __construct( /** * The expiration date as a valid date for the strtotime() function. */ public ?string $expires = null, /** * The number of seconds that the response is considered fresh by a private * cache like a web browser. */ public int|string|null $maxage = null, /** * The number of seconds that the response is considered fresh by a public * cache like a reverse proxy cache. */ public int|string|null $smaxage = null, /** * If true, the contents will be stored in a public cache and served to all * the next requests. */ public ?bool $public = null, /** * If true, the response is not served stale by a cache in any circumstance * without first revalidating with the origin. */ public bool $mustRevalidate = false, /** * Set "Vary" header. * * Example: * ['Accept-Encoding', 'User-Agent'] * * @see https://symfony.com/doc/current/http_cache/cache_vary.html * * @var string[] */ public array $vary = [], /** * A value evaluated to compute the Last-Modified HTTP header. * * The value may be either an ExpressionLanguage expression or a Closure and * receives all the request attributes and the resolved controller arguments. * * The result must be an instance of \DateTimeInterface. * * @var \DateTimeInterface|string|Expression|\Closure(array, Request, ?object):\DateTimeInterface|null */ public \DateTimeInterface|string|Expression|\Closure|null $lastModified = null, /** * A value evaluated to compute the ETag HTTP header. * * The value may be either an ExpressionLanguage expression or a Closure and * receives all the request attributes and the resolved controller arguments. * * The result must be a string that will be hashed. * * @var string|Expression|\Closure(array, Request, ?object):string|null */ public string|Expression|\Closure|null $etag = null, /** * max-stale Cache-Control header * It can be expressed in seconds or with a relative time format (1 day, 2 weeks, ...). */ public int|string|null $maxStale = null, /** * stale-while-revalidate Cache-Control header * It can be expressed in seconds or with a relative time format (1 day, 2 weeks, ...). */ public int|string|null $staleWhileRevalidate = null, /** * stale-if-error Cache-Control header * It can be expressed in seconds or with a relative time format (1 day, 2 weeks, ...). */ public int|string|null $staleIfError = null, /** * Add the "no-store" Cache-Control directive when set to true. * * This directive indicates that no part of the response can be cached * in any cache (not in a shared cache, nor in a private cache). * * Supersedes the "$public" and "$smaxage" values. * * @see https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.2.3 */ public ?bool $noStore = null, /** * A value evaluated to determine whether the cache attribute should be applied. * * The value may be either an ExpressionLanguage expression or a Closure and * receives all the request attributes and the resolved controller arguments. * * The result must be a boolean. If true the attribute is applied, if false it is ignored. * * @var bool|string|Expression|\Closure(array, Request, ?object):bool */ public bool|string|Expression|\Closure $if = true, ) { } } ================================================ FILE: Attribute/IsSignatureValid.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; /** * Validates the request signature for specific HTTP methods. * * This class determines whether a request's signature should be validated * based on the configured HTTP methods. If the request method matches one * of the specified methods (or if no methods are specified), the signature * is checked. * * If the signature is invalid, a {@see \Symfony\Component\HttpFoundation\Exception\SignedUriException} * is thrown during validation. * * @author Santiago San Martin */ #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)] final class IsSignatureValid { /** @var string[] */ public readonly array $methods; /** * @param string[]|string $methods HTTP methods that require signature validation. An empty array means that no method filtering is done */ public function __construct( array|string $methods = [], ) { $this->methods = (array) $methods; } } ================================================ FILE: Attribute/MapDateTime.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; /** * Controller parameter tag to configure DateTime arguments. */ #[\Attribute(\Attribute::TARGET_PARAMETER)] class MapDateTime extends ValueResolver { /** * @param string|null $format The DateTime format to use, @see https://php.net/datetime.format * @param bool $disabled Whether this value resolver is disabled; this allows to enable a value resolver globally while disabling it in specific cases * @param class-string|string $resolver The name of the resolver to use */ public function __construct( public readonly ?string $format = null, bool $disabled = false, string $resolver = DateTimeValueResolver::class, ) { parent::__construct($resolver, $disabled); } } ================================================ FILE: Attribute/MapQueryParameter.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; /** * Can be used to pass a query parameter to a controller argument. * * @author Ruud Kamphuis * @author Ionut Enache */ #[\Attribute(\Attribute::TARGET_PARAMETER)] final class MapQueryParameter extends ValueResolver { /** * @see https://php.net/manual/filter.constants for filter, flags and options * * @param string|null $name The name of the query parameter; if null, the name of the argument in the controller will be used * @param (FILTER_VALIDATE_*)|(FILTER_SANITIZE_*)|null $filter The filter to pass to "filter_var()", deduced from the type-hint if null * @param int-mask-of<(FILTER_FLAG_*)|FILTER_NULL_ON_FAILURE> $flags * @param array{min_range?: int|float, max_range?: int|float, regexp?: string, ...} $options * @param class-string|string $resolver The name of the resolver to use */ public function __construct( public ?string $name = null, public ?int $filter = null, public int $flags = 0, public array $options = [], string $resolver = QueryParameterValueResolver::class, public int $validationFailedStatusCode = Response::HTTP_NOT_FOUND, ) { parent::__construct($resolver); } } ================================================ FILE: Attribute/MapQueryString.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Validator\Constraints\GroupSequence; /** * Controller parameter tag to map the query string of the request to typed object and validate it. * * @psalm-import-type GroupResolver from RequestPayloadValueResolver * * @author Konstantin Myakshin */ #[\Attribute(\Attribute::TARGET_PARAMETER)] class MapQueryString extends ValueResolver { public ArgumentMetadata $metadata; /** * @param array $serializationContext The serialization context to use when deserializing the query string * @param string|Expression|GroupSequence|GroupResolver|array|null $validationGroups The validation groups to use when validating the query string mapping * @param class-string $resolver The class name of the resolver to use * @param int $validationFailedStatusCode The HTTP code to return if the validation fails */ public function __construct( public readonly array $serializationContext = [], public readonly string|Expression|GroupSequence|\Closure|array|null $validationGroups = null, string $resolver = RequestPayloadValueResolver::class, public readonly int $validationFailedStatusCode = Response::HTTP_NOT_FOUND, public readonly ?string $key = null, public bool $mapWhenEmpty = false, ) { parent::__construct($resolver); } } ================================================ FILE: Attribute/MapRequestHeader.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestHeaderValueResolver; #[\Attribute(\Attribute::TARGET_PARAMETER)] final class MapRequestHeader extends ValueResolver { /** * @param string|null $name The name of the header parameter; if null, the name of the argument in the controller will be used * @param class-string $resolver The class name of the resolver to use * @param int $validationFailedStatusCode The HTTP code to return if the validation fails */ public function __construct( public readonly ?string $name = null, string $resolver = RequestHeaderValueResolver::class, public readonly int $validationFailedStatusCode = Response::HTTP_BAD_REQUEST, ) { parent::__construct($resolver); } } ================================================ FILE: Attribute/MapRequestPayload.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Validator\Constraints\GroupSequence; /** * Controller parameter tag to map the request content to typed object and validate it. * * @psalm-import-type GroupResolver from RequestPayloadValueResolver * * @author Konstantin Myakshin */ #[\Attribute(\Attribute::TARGET_PARAMETER)] class MapRequestPayload extends ValueResolver { public ArgumentMetadata $metadata; /** * @param array|string|null $acceptFormat The payload formats to accept (i.e. "json", "xml") * @param array $serializationContext The serialization context to use when deserializing the payload * @param string|Expression|GroupSequence|GroupResolver|array|null $validationGroups The validation groups to use when validating the query string mapping * @param class-string $resolver The class name of the resolver to use * @param int $validationFailedStatusCode The HTTP code to return if the validation fails * @param class-string|string|null $type The element type for array deserialization */ public function __construct( public readonly array|string|null $acceptFormat = null, public readonly array $serializationContext = [], public readonly string|Expression|GroupSequence|\Closure|array|null $validationGroups = null, string $resolver = RequestPayloadValueResolver::class, public readonly int $validationFailedStatusCode = Response::HTTP_UNPROCESSABLE_ENTITY, public readonly ?string $type = null, public bool $mapWhenEmpty = false, ) { parent::__construct($resolver); } } ================================================ FILE: Attribute/MapUploadedFile.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Validator\Constraint; #[\Attribute(\Attribute::TARGET_PARAMETER)] class MapUploadedFile extends ValueResolver { public ArgumentMetadata $metadata; public function __construct( /** @var Constraint|array|null */ public Constraint|array|null $constraints = null, public ?string $name = null, string $resolver = RequestPayloadValueResolver::class, public readonly int $validationFailedStatusCode = Response::HTTP_UNPROCESSABLE_ENTITY, ) { parent::__construct($resolver); } } ================================================ FILE: Attribute/Serialize.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; /** * Controller tag to serialize response. * * @author Konstantin Myakshin */ #[\Attribute(\Attribute::TARGET_METHOD)] final class Serialize { /** * @param int $code The HTTP status code (200 "OK" by default) * @param array $headers Extra headers to set on the response * @param array $context The serialization context passed to the serializer */ public function __construct( public readonly int $code = 200, public readonly array $headers = [], public readonly array $context = [], ) { } } ================================================ FILE: Attribute/ValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; /** * Defines which value resolver should be used for a given parameter. */ #[\Attribute(\Attribute::TARGET_PARAMETER | \Attribute::IS_REPEATABLE)] class ValueResolver { /** * @param class-string|string $resolver The class name of the resolver to use * @param bool $disabled Whether this value resolver is disabled; this allows to enable a value resolver globally while disabling it in specific cases */ public function __construct( public string $resolver, public bool $disabled = false, ) { } } ================================================ FILE: Attribute/WithHttpStatus.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; /** * Defines the HTTP status code applied to an exception. * * @author Dejan Angelov */ #[\Attribute(\Attribute::TARGET_CLASS)] class WithHttpStatus { /** * @param int $statusCode The HTTP status code to use * @param array $headers The HTTP headers to add to the response */ public function __construct( public readonly int $statusCode, public readonly array $headers = [], ) { } } ================================================ FILE: Attribute/WithLogLevel.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Attribute; use Psr\Log\LogLevel; /** * Defines the log level applied to an exception. * * @author Dejan Angelov */ #[\Attribute(\Attribute::TARGET_CLASS)] final class WithLogLevel { /** * @param LogLevel::* $level The level to use to log the exception */ public function __construct(public readonly string $level) { if (!\defined('Psr\Log\LogLevel::'.strtoupper($this->level))) { throw new \InvalidArgumentException(\sprintf('Invalid log level "%s".', $this->level)); } } } ================================================ FILE: Bundle/AbstractBundle.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Bundle; use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ConfigurableExtensionInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; /** * A Bundle that provides configuration hooks. * * @author Yonel Ceruto */ abstract class AbstractBundle extends Bundle implements ConfigurableExtensionInterface { protected string $extensionAlias = ''; public function configure(DefinitionConfigurator $definition): void { } public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void { } public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { } public function getContainerExtension(): ?ExtensionInterface { if ('' === $this->extensionAlias) { $this->extensionAlias = Container::underscore(preg_replace('/Bundle$/', '', $this->getName())); } return $this->extension ??= new BundleExtension($this, $this->extensionAlias); } public function getPath(): string { if (!isset($this->path)) { $reflected = new \ReflectionObject($this); // assume the modern directory structure by default $this->path = \dirname($reflected->getFileName(), 2); } return $this->path; } } ================================================ FILE: Bundle/Bundle.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Bundle; use Symfony\Component\Console\Application; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; /** * An implementation of BundleInterface that adds a few conventions for DependencyInjection extensions. * * @author Fabien Potencier */ abstract class Bundle implements BundleInterface { protected string $name; protected ExtensionInterface|false|null $extension = null; protected string $path; protected ?ContainerInterface $container; private string $namespace; public function boot(): void { } public function shutdown(): void { } /** * This method can be overridden to register compilation passes, * other extensions, ... */ public function build(ContainerBuilder $container): void { } /** * Returns the bundle's container extension. * * @throws \LogicException */ public function getContainerExtension(): ?ExtensionInterface { if (!isset($this->extension)) { $extension = $this->createContainerExtension(); if (null !== $extension) { if (!$extension instanceof ExtensionInterface) { throw new \LogicException(\sprintf('Extension "%s" must implement Symfony\Component\DependencyInjection\Extension\ExtensionInterface.', get_debug_type($extension))); } // check naming convention $basename = preg_replace('/Bundle$/', '', $this->getName()); $expectedAlias = Container::underscore($basename); if ($expectedAlias != $extension->getAlias()) { throw new \LogicException(\sprintf('Users will expect the alias of the default extension of a bundle to be the underscored version of the bundle name ("%s"). You can override "Bundle::getContainerExtension()" if you want to use "%s" or another alias.', $expectedAlias, $extension->getAlias())); } $this->extension = $extension; } else { $this->extension = false; } } return $this->extension ?: null; } public function getNamespace(): string { if (!isset($this->namespace)) { $this->parseClassName(); } return $this->namespace; } public function getPath(): string { if (!isset($this->path)) { $reflected = new \ReflectionObject($this); $this->path = \dirname($reflected->getFileName()); } return $this->path; } /** * Returns the bundle name (the class short name). */ final public function getName(): string { if (!isset($this->name)) { $this->parseClassName(); } return $this->name; } public function registerCommands(Application $application): void { } /** * Returns the bundle's container extension class. */ protected function getContainerExtensionClass(): string { $basename = preg_replace('/Bundle$/', '', $this->getName()); return $this->getNamespace().'\\DependencyInjection\\'.$basename.'Extension'; } /** * Creates the bundle's container extension. */ protected function createContainerExtension(): ?ExtensionInterface { return class_exists($class = $this->getContainerExtensionClass()) ? new $class() : null; } private function parseClassName(): void { $pos = strrpos(static::class, '\\'); $this->namespace = false === $pos ? '' : substr(static::class, 0, $pos); $this->name ??= false === $pos ? static::class : substr(static::class, $pos + 1); } public function setContainer(?ContainerInterface $container): void { $this->container = $container; } } ================================================ FILE: Bundle/BundleExtension.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Bundle; use Symfony\Component\Config\Definition\Configuration; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ConfigurableExtensionInterface; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Extension\ExtensionTrait; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; /** * @author Yonel Ceruto * * @internal */ class BundleExtension extends Extension implements PrependExtensionInterface { use ExtensionTrait; public function __construct( private ConfigurableExtensionInterface $subject, private string $alias, ) { } public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface { return new Configuration($this->subject, $container, $this->getAlias()); } public function getAlias(): string { return $this->alias; } public function prepend(ContainerBuilder $container): void { $callback = function (ContainerConfigurator $configurator) use ($container) { $this->subject->prependExtension($configurator, $container); }; $this->executeConfiguratorCallback($container, $callback, $this->subject, true); } public function load(array $configs, ContainerBuilder $container): void { $config = $this->processConfiguration($this->getConfiguration([], $container), $configs); $callback = function (ContainerConfigurator $configurator) use ($config, $container) { $this->subject->loadExtension($config, $configurator, $container); }; $this->executeConfiguratorCallback($container, $callback, $this->subject); } } ================================================ FILE: Bundle/BundleInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Bundle; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; /** * BundleInterface. * * @author Fabien Potencier */ interface BundleInterface { /** * Boots the Bundle. */ public function boot(): void; /** * Shutdowns the Bundle. */ public function shutdown(): void; /** * Builds the bundle. * * It is only ever called once when the cache is empty. */ public function build(ContainerBuilder $container): void; /** * Returns the container extension that should be implicitly loaded. */ public function getContainerExtension(): ?ExtensionInterface; /** * Returns the bundle name (the class short name). */ public function getName(): string; /** * Gets the Bundle namespace. */ public function getNamespace(): string; /** * Gets the Bundle directory path. * * The path should always be returned as a Unix path (with /). */ public function getPath(): string; public function setContainer(?ContainerInterface $container): void; } ================================================ FILE: CHANGELOG.md ================================================ CHANGELOG ========= 8.1 --- * Add `#[MapRequestHeader]` to map a header from `Request` to a controller argument * Add `hasErrors()` method to `Profile` to track profiles with errors (exceptions or error-level logs) * Validate typed route parameters before calling controllers and return an HTTP error when an invalid value is provided * Add `ControllerAttributeEvent` et al. to dispatch events named after controller attributes * Add support for `UploadedFile` when using `MapRequestPayload` * Add support for bundles as compiler pass * Add support for `SOURCE_DATE_EPOCH` environment variable * Add property `$controllerMetadata` to several kernel events to give listeners access to controller metadata * Add `Request` attribute `_controller_attributes` to decouple controller attributes from their source code * Return attributes as a flat list when using `Controller[Arguments]Event::getAttributes('*')` * Pass `request` and `args` variables to `Cache` attribute expressions containing the `Request` object and controller arguments * Allow using closures with the `Cache` attribute * Allow setting a condition when the `Cache` attribute should be applied * Add `ControllerEvent::evaluate()` et al. to help with evaluating expressions or closures in controller attributes * Deprecate passing a non-flat list of attributes to `Controller::setController()` * Deprecate the `Symfony\Component\HttpKernel\DependencyInjection\Extension` class, use the parent `Symfony\Component\DependencyInjection\Extension\Extension` class instead * Allow using Expression or \Closure for `validationGroups` in `#[MapRequestPayload]` and `#[MapQueryString]` * Deprecate passing a `ControllerArgumentsEvent` to the `ViewEvent` constructor; pass a `ControllerArgumentsMetadata` instead * Support variadic argument with `#[MapRequestPayload]` * Add `#[Serialize]` to serialize values returned by controllers * Add argument `$mapWhenEmpty` to `MapQueryString` and `MapRequestPayload` for always attempting denormalization with empty query and request payload 8.0 --- * Remove `AddAnnotatedClassesToCachePass` * Remove `Extension::getAnnotatedClassesToCompile()` and `Extension::addAnnotatedClassesToCompile()` * Remove `Kernel::getAnnotatedClassesToCompile()` and `Kernel::setAnnotatedClassCache()` * Make `ServicesResetter` class `final` * Add argument `$logChannel` to `ErrorListener::logException()` * Add argument `$event` to `DumpListener::configure()` * Replace `__sleep/wakeup()` by `__(un)serialize()` on kernels and data collectors * Add method `getShareDir()` to `KernelInterface` 7.4 --- * Add support for the `QUERY` HTTP method * Deprecate implementing `__sleep/wakeup()` on kernels; use `__(un)serialize()` instead * Deprecate implementing `__sleep/wakeup()` on data collectors; use `__(un)serialize()` instead * Add `#[IsSignatureValid]` attribute to validate URI signatures * Make `Profile` final and `Profiler::__sleep()` internal * Collect the application runner class * Allow configuring `DumpListener` to use a different dumper when CLI profiling is enabled 7.3 --- * Record a `waiting` trace in the `HttpCache` when the cache had to wait for another request to finish * Add `$key` argument to `#[MapQueryString]` that allows using a specific key for argument resolving * Support `Uid` in `#[MapQueryParameter]` * Add `ServicesResetterInterface`, implemented by `ServicesResetter` * Allow configuring the logging channel per type of exceptions in ErrorListener 7.2 --- * Remove `@internal` flag and add `@final` to `ServicesResetter` * Add support for `SYMFONY_DISABLE_RESOURCE_TRACKING` env var * Add support for configuring trusted proxies/headers/hosts via env vars 7.1 --- * Add method `isKernelTerminating()` to `ExceptionEvent` that allows to check if an exception was thrown while the kernel is being terminated * Add `HttpException::fromStatusCode()` * Add `$validationFailedStatusCode` argument to `#[MapQueryParameter]` that allows setting a custom HTTP status code when validation fails * Add `NearMissValueResolverException` to let value resolvers report when an argument could be under their watch but failed to be resolved * Add `$type` argument to `#[MapRequestPayload]` that allows mapping a list of items * The `Extension` class is marked as internal, extend the `Extension` class from the DependencyInjection component instead * Deprecate `Extension::addAnnotatedClassesToCompile()` * Deprecate `AddAnnotatedClassesToCachePass` * Deprecate the `setAnnotatedClassCache()` and `getAnnotatedClassesToCompile()` methods of the `Kernel` class * Add `#[MapUploadedFile]` attribute to fetch, validate, and inject uploaded files into controller arguments 7.0 --- * Add argument `$reflector` to `ArgumentResolverInterface::getArguments()` and `ArgumentMetadataFactoryInterface::createArgumentMetadata()` * Remove `ArgumentValueResolverInterface`, use `ValueResolverInterface` instead * Remove `StreamedResponseListener` * Remove `AbstractSurrogate::$phpEscapeMap` * Remove `HttpKernelInterface::MASTER_REQUEST` * Remove `terminate_on_cache_hit` option from `HttpCache` * Require explicit argument when calling `ConfigDataCollector::setKernel()`, `RouterListener::setCurrentRequest()` * Remove `Kernel::stripComments()` * Remove `FileLinkFormatter`, use `FileLinkFormatter` from the ErrorHandler component instead * Remove `UriSigner`, use `UriSigner` from the HttpFoundation component instead * Add argument `$buildDir` to `WarmableInterface` * Add argument `$filter` to `Profiler::find()` and `FileProfilerStorage::find()` 6.4 --- * Support backed enums in #[MapQueryParameter] * `BundleInterface` no longer extends `ContainerAwareInterface` * Add optional `$className` parameter to `ControllerEvent::getAttributes()` * Add native return types to `TraceableEventDispatcher` and to `MergeExtensionConfigurationPass` * Add argument `$validationFailedStatusCode` to `#[MapQueryString]` and `#[MapRequestPayload]` * Add argument `$debug` to `Logger` * Add class `DebugLoggerConfigurator` * Add parameters `kernel.runtime_mode` and `kernel.runtime_mode.*`, all set from env var `APP_RUNTIME_MODE` * Deprecate `Kernel::stripComments()` * Support the `!` character at the beginning of a string as a negation operator in the url filter of the profiler * Deprecate `UriSigner`, use `UriSigner` from the HttpFoundation component instead * Deprecate `FileLinkFormatter`, use `FileLinkFormatter` from the ErrorHandler component instead * Add argument `$buildDir` to `WarmableInterface` * Add argument `$filter` to `Profiler::find()` and `FileProfilerStorage::find()` * Add `ControllerResolver::allowControllers()` to define which callables are legit controllers when the `_check_controller_is_allowed` request attribute is set 6.3 --- * Deprecate parameters `container.dumper.inline_factories` and `container.dumper.inline_class_loader`, use `.container.dumper.inline_factories` and `.container.dumper.inline_class_loader` instead * `FileProfilerStorage` removes profiles automatically after two days * Add `#[WithHttpStatus]` for defining status codes for exceptions * Use an instance of `Psr\Clock\ClockInterface` to generate the current date time in `DateTimeValueResolver` * Add `#[WithLogLevel]` for defining log levels for exceptions * Add `skip_response_headers` to the `HttpCache` options * Introduce targeted value resolvers with `#[ValueResolver]` and `#[AsTargetedValueResolver]` * Add `#[MapRequestPayload]` to map and validate request payload from `Request::getContent()` or `Request::$request->all()` to typed objects * Add `#[MapQueryString]` to map and validate request query string from `Request::$query->all()` to typed objects * Add `#[MapQueryParameter]` to map and validate individual query parameters to controller arguments * Collect data from every event dispatcher 6.2 --- * Add constructor argument `bool $handleAllThrowable` to `HttpKernel` * Add `ControllerEvent::getAttributes()` to handle attributes on controllers * Add `#[Cache]` to describe the default HTTP cache headers on controllers * Add `absolute_uri` option to surrogate fragment renderers * Add `ValueResolverInterface` and deprecate `ArgumentValueResolverInterface` * Add argument `$reflector` to `ArgumentResolverInterface` and `ArgumentMetadataFactoryInterface` * Deprecate calling `ConfigDataCollector::setKernel()`, `RouterListener::setCurrentRequest()` without arguments 6.1 --- * Add `BackedEnumValueResolver` to resolve backed enum cases from request attributes in controller arguments * Add `DateTimeValueResolver` to resolve request attributes into DateTime objects in controller arguments * Deprecate StreamedResponseListener, it's not needed anymore * Add `Profiler::isEnabled()` so collaborating collector services may elect to omit themselves * Add the `UidValueResolver` argument value resolver * Add `AbstractBundle` class for DI configuration/definition on a single file * Update the path of a bundle placed in the `src/` directory to the parent directory when `AbstractBundle` is used 6.0 --- * Remove `ArgumentInterface` * Remove `ArgumentMetadata::getAttribute()`, use `getAttributes()` instead * Remove support for returning a `ContainerBuilder` from `KernelInterface::registerContainerConfiguration()` * Remove `KernelEvent::isMasterRequest()`, use `isMainRequest()` instead * Remove support for `service:action` syntax to reference controllers, use `serviceOrFqcn::method` instead 5.4 --- * Add the ability to enable the profiler using a request query parameter, body parameter or attribute * Deprecate `AbstractTestSessionListener` and `TestSessionListener`, use `AbstractSessionListener` and `SessionListener` instead * Deprecate the `fileLinkFormat` parameter of `DebugHandlersListener` * Add support for configuring log level, and status code by exception class * Allow ignoring "kernel.reset" methods that don't exist with "on_invalid" attribute 5.3 --- * Deprecate `ArgumentInterface` * Add `ArgumentMetadata::getAttributes()` * Deprecate `ArgumentMetadata::getAttribute()`, use `getAttributes()` instead * Mark the class `Symfony\Component\HttpKernel\EventListener\DebugHandlersListener` as internal * Deprecate returning a `ContainerBuilder` from `KernelInterface::registerContainerConfiguration()` * Deprecate `HttpKernelInterface::MASTER_REQUEST` and add `HttpKernelInterface::MAIN_REQUEST` as replacement * Deprecate `KernelEvent::isMasterRequest()` and add `isMainRequest()` as replacement * Add `#[AsController]` attribute for declaring standalone controllers on PHP 8 * Add `FragmentUriGeneratorInterface` and `FragmentUriGenerator` to generate the URI of a fragment 5.2.0 ----- * added session usage * made the public `http_cache` service handle requests when available * allowed enabling trusted hosts and proxies using new `kernel.trusted_hosts`, `kernel.trusted_proxies` and `kernel.trusted_headers` parameters * content of request parameter `_password` is now also hidden in the request profiler raw content section * Allowed adding attributes on controller arguments that will be passed to argument resolvers. * kernels implementing the `ExtensionInterface` will now be auto-registered to the container * added parameter `kernel.runtime_environment`, defined as `%env(default:kernel.environment:APP_RUNTIME_ENV)%` * do not set a default `Accept` HTTP header when using `HttpKernelBrowser` 5.1.0 ----- * allowed to use a specific logger channel for deprecations * made `WarmableInterface::warmUp()` return a list of classes or files to preload on PHP 7.4+; not returning an array is deprecated * made kernels implementing `WarmableInterface` be part of the cache warmup stage * deprecated support for `service:action` syntax to reference controllers, use `serviceOrFqcn::method` instead * allowed using public aliases to reference controllers * added session usage reporting when the `_stateless` attribute of the request is set to `true` * added `AbstractSessionListener::onSessionUsage()` to report when the session is used while a request is stateless 5.0.0 ----- * removed support for getting the container from a non-booted kernel * removed the first and second constructor argument of `ConfigDataCollector` * removed `ConfigDataCollector::getApplicationName()` * removed `ConfigDataCollector::getApplicationVersion()` * removed support for `Symfony\Component\Templating\EngineInterface` in `HIncludeFragmentRenderer`, use a `Twig\Environment` only * removed `TranslatorListener` in favor of `LocaleAwareListener` * removed `getRootDir()` and `getName()` from `Kernel` and `KernelInterface` * removed `FilterControllerArgumentsEvent`, use `ControllerArgumentsEvent` instead * removed `FilterControllerEvent`, use `ControllerEvent` instead * removed `FilterResponseEvent`, use `ResponseEvent` instead * removed `GetResponseEvent`, use `RequestEvent` instead * removed `GetResponseForControllerResultEvent`, use `ViewEvent` instead * removed `GetResponseForExceptionEvent`, use `ExceptionEvent` instead * removed `PostResponseEvent`, use `TerminateEvent` instead * removed `SaveSessionListener` in favor of `AbstractSessionListener` * removed `Client`, use `HttpKernelBrowser` instead * added method `getProjectDir()` to `KernelInterface` * removed methods `serialize` and `unserialize` from `DataCollector`, store the serialized state in the data property instead * made `ProfilerStorageInterface` internal * removed the second and third argument of `KernelInterface::locateResource` * removed the second and third argument of `FileLocator::__construct` * removed loading resources from `%kernel.root_dir%/Resources` and `%kernel.root_dir%` as fallback directories. * removed class `ExceptionListener`, use `ErrorListener` instead 4.4.0 ----- * The `DebugHandlersListener` class has been marked as `final` * Added new Bundle directory convention consistent with standard skeletons * Deprecated the second and third argument of `KernelInterface::locateResource` * Deprecated the second and third argument of `FileLocator::__construct` * Deprecated loading resources from `%kernel.root_dir%/Resources` and `%kernel.root_dir%` as fallback directories. Resources like service definitions are usually loaded relative to the current directory or with a glob pattern. The fallback directories have never been advocated so you likely do not use those in any app based on the SF Standard or Flex edition. * Marked all dispatched event classes as `@final` * Added `ErrorController` to enable the preview and error rendering mechanism * Getting the container from a non-booted kernel is deprecated. * Marked the `AjaxDataCollector`, `ConfigDataCollector`, `EventDataCollector`, `ExceptionDataCollector`, `LoggerDataCollector`, `MemoryDataCollector`, `RequestDataCollector` and `TimeDataCollector` classes as `@final`. * Marked the `RouterDataCollector::collect()` method as `@final`. * The `DataCollectorInterface::collect()` and `Profiler::collect()` methods third parameter signature will be `\Throwable $exception = null` instead of `\Exception $exception = null` in Symfony 5.0. * Deprecated methods `ExceptionEvent::get/setException()`, use `get/setThrowable()` instead * Deprecated class `ExceptionListener`, use `ErrorListener` instead 4.3.0 ----- * renamed `Client` to `HttpKernelBrowser` * `KernelInterface` doesn't extend `Serializable` anymore * deprecated the `Kernel::serialize()` and `unserialize()` methods * increased the priority of `Symfony\Component\HttpKernel\EventListener\AddRequestFormatsListener` * made `Symfony\Component\HttpKernel\EventListener\LocaleListener` set the default locale early * deprecated `TranslatorListener` in favor of `LocaleAwareListener` * added the registration of all `LocaleAwareInterface` implementations into the `LocaleAwareListener` * made `FileLinkFormatter` final and not implement `Serializable` anymore * the base `DataCollector` doesn't implement `Serializable` anymore, you should store all the serialized state in the data property instead * `DumpDataCollector` has been marked as `final` * added an event listener to prevent search engines from indexing applications in debug mode. * renamed `FilterControllerArgumentsEvent` to `ControllerArgumentsEvent` * renamed `FilterControllerEvent` to `ControllerEvent` * renamed `FilterResponseEvent` to `ResponseEvent` * renamed `GetResponseEvent` to `RequestEvent` * renamed `GetResponseForControllerResultEvent` to `ViewEvent` * renamed `GetResponseForExceptionEvent` to `ExceptionEvent` * renamed `PostResponseEvent` to `TerminateEvent` * added `HttpClientKernel` for handling requests with an `HttpClientInterface` instance * added `trace_header` and `trace_level` configuration options to `HttpCache` 4.2.0 ----- * deprecated `KernelInterface::getRootDir()` and the `kernel.root_dir` parameter * deprecated `KernelInterface::getName()` and the `kernel.name` parameter * deprecated the first and second constructor argument of `ConfigDataCollector` * deprecated `ConfigDataCollector::getApplicationName()` * deprecated `ConfigDataCollector::getApplicationVersion()` 4.1.0 ----- * added orphaned events support to `EventDataCollector` * `ExceptionListener` now logs exceptions at priority `0` (previously logged at `-128`) * Added support for using `service::method` to reference controllers, making it consistent with other cases. It is recommended over the `service:action` syntax with a single colon, which will be deprecated in the future. * Added the ability to profile individual argument value resolvers via the `Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver` 4.0.0 ----- * removed the `DataCollector::varToString()` method, use `DataCollector::cloneVar()` instead * using the `DataCollector::cloneVar()` method requires the VarDumper component * removed the `ValueExporter` class * removed `ControllerResolverInterface::getArguments()` * removed `TraceableControllerResolver::getArguments()` * removed `ControllerResolver::getArguments()` and the ability to resolve arguments * removed the `argument_resolver` service dependency from the `debug.controller_resolver` * removed `LazyLoadingFragmentHandler::addRendererService()` * removed `Psr6CacheClearer::addPool()` * removed `Extension::addClassesToCompile()` and `Extension::getClassesToCompile()` * removed `Kernel::loadClassCache()`, `Kernel::doLoadClassCache()`, `Kernel::setClassCache()`, and `Kernel::getEnvParameters()` * support for the `X-Status-Code` when handling exceptions in the `HttpKernel` has been dropped, use the `HttpKernel::allowCustomResponseCode()` method instead * removed convention-based commands registration * removed the `ChainCacheClearer::add()` method * removed the `CacheaWarmerAggregate::add()` and `setWarmers()` methods * made `CacheWarmerAggregate` and `ChainCacheClearer` classes final 3.4.0 ----- * added a minimalist PSR-3 `Logger` class that writes in `stderr` * made kernels implementing `CompilerPassInterface` able to process the container * deprecated bundle inheritance * added `RebootableInterface` and implemented it in `Kernel` * deprecated commands auto registration * deprecated `EnvParametersResource` * added `Symfony\Component\HttpKernel\Client::catchExceptions()` * deprecated the `ChainCacheClearer::add()` method * deprecated the `CacheaWarmerAggregate::add()` and `setWarmers()` methods * made `CacheWarmerAggregate` and `ChainCacheClearer` classes final * added the possibility to reset the profiler to its initial state * deprecated data collectors without a `reset()` method * deprecated implementing `DebugLoggerInterface` without a `clear()` method 3.3.0 ----- * added `kernel.project_dir` and `Kernel::getProjectDir()` * deprecated `kernel.root_dir` and `Kernel::getRootDir()` * deprecated `Kernel::getEnvParameters()` * deprecated the special `SYMFONY__` environment variables * added the possibility to change the query string parameter used by `UriSigner` * deprecated `LazyLoadingFragmentHandler::addRendererService()` * deprecated `Extension::addClassesToCompile()` and `Extension::getClassesToCompile()` * deprecated `Psr6CacheClearer::addPool()` 3.2.0 ----- * deprecated `DataCollector::varToString()`, use `cloneVar()` instead * changed surrogate capability name in `AbstractSurrogate::addSurrogateCapability` to 'symfony' * Added `ControllerArgumentValueResolverPass` 3.1.0 ----- * deprecated passing objects as URI attributes to the ESI and SSI renderers * deprecated `ControllerResolver::getArguments()` * added `Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface` * added `Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface` as argument to `HttpKernel` * added `Symfony\Component\HttpKernel\Controller\ArgumentResolver` * added `Symfony\Component\HttpKernel\DataCollector\RequestDataCollector::getMethod()` * added `Symfony\Component\HttpKernel\DataCollector\RequestDataCollector::getRedirect()` * added the `kernel.controller_arguments` event, triggered after controller arguments have been resolved 3.0.0 ----- * removed `Symfony\Component\HttpKernel\Kernel::init()` * removed `Symfony\Component\HttpKernel\Kernel::isClassInActiveBundle()` and `Symfony\Component\HttpKernel\KernelInterface::isClassInActiveBundle()` * removed `Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher::setProfiler()` * removed `Symfony\Component\HttpKernel\EventListener\FragmentListener::getLocalIpAddresses()` * removed `Symfony\Component\HttpKernel\EventListener\LocaleListener::setRequest()` * removed `Symfony\Component\HttpKernel\EventListener\RouterListener::setRequest()` * removed `Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelRequest()` * removed `Symfony\Component\HttpKernel\Fragment\FragmentHandler::setRequest()` * removed `Symfony\Component\HttpKernel\HttpCache\Esi::hasSurrogateEsiCapability()` * removed `Symfony\Component\HttpKernel\HttpCache\Esi::addSurrogateEsiCapability()` * removed `Symfony\Component\HttpKernel\HttpCache\Esi::needsEsiParsing()` * removed `Symfony\Component\HttpKernel\HttpCache\HttpCache::getEsi()` * removed `Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel` * removed `Symfony\Component\HttpKernel\DependencyInjection\RegisterListenersPass` * removed `Symfony\Component\HttpKernel\EventListener\ErrorsLoggerListener` * removed `Symfony\Component\HttpKernel\EventListener\EsiListener` * removed `Symfony\Component\HttpKernel\HttpCache\EsiResponseCacheStrategy` * removed `Symfony\Component\HttpKernel\HttpCache\EsiResponseCacheStrategyInterface` * removed `Symfony\Component\HttpKernel\Log\LoggerInterface` * removed `Symfony\Component\HttpKernel\Log\NullLogger` * removed `Symfony\Component\HttpKernel\Profiler::import()` * removed `Symfony\Component\HttpKernel\Profiler::export()` 2.8.0 ----- * deprecated `Profiler::import` and `Profiler::export` 2.7.0 ----- * added the HTTP status code to profiles 2.6.0 ----- * deprecated `Symfony\Component\HttpKernel\EventListener\ErrorsLoggerListener`, use `Symfony\Component\HttpKernel\EventListener\DebugHandlersListener` instead * deprecated unused method `Symfony\Component\HttpKernel\Kernel::isClassInActiveBundle` and `Symfony\Component\HttpKernel\KernelInterface::isClassInActiveBundle` 2.5.0 ----- * deprecated `Symfony\Component\HttpKernel\DependencyInjection\RegisterListenersPass`, use `Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass` instead 2.4.0 ----- * added event listeners for the session * added the KernelEvents::FINISH_REQUEST event 2.3.0 ----- * [BC BREAK] renamed `Symfony\Component\HttpKernel\EventListener\DeprecationLoggerListener` to `Symfony\Component\HttpKernel\EventListener\ErrorsLoggerListener` and changed its constructor * deprecated `Symfony\Component\HttpKernel\Debug\ErrorHandler`, `Symfony\Component\HttpKernel\Debug\ExceptionHandler`, `Symfony\Component\HttpKernel\Exception\FatalErrorException` and `Symfony\Component\HttpKernel\Exception\FlattenException` * deprecated `Symfony\Component\HttpKernel\Kernel::init()` * added the possibility to specify an id an extra attributes to hinclude tags * added the collect of data if a controller is a Closure in the Request collector * pass exceptions from the ExceptionListener to the logger using the logging context to allow for more detailed messages 2.2.0 ----- * [BC BREAK] the path info for sub-request is now always _fragment (or whatever you configured instead of the default) * added Symfony\Component\HttpKernel\EventListener\FragmentListener * added Symfony\Component\HttpKernel\UriSigner * added Symfony\Component\HttpKernel\FragmentRenderer and rendering strategies (in Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface) * added Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel * added ControllerReference to create reference of Controllers (used in the FragmentRenderer class) * [BC BREAK] renamed TimeDataCollector::getTotalTime() to TimeDataCollector::getDuration() * updated the MemoryDataCollector to include the memory used in the kernel.terminate event listeners * moved the Stopwatch classes to a new component * added TraceableControllerResolver * added TraceableEventDispatcher (removed ContainerAwareTraceableEventDispatcher) * added support for WinCache opcode cache in ConfigDataCollector 2.1.0 ----- * [BC BREAK] the charset is now configured via the Kernel::getCharset() method * [BC BREAK] the current locale for the user is not stored anymore in the session * added the HTTP method to the profiler storage * updated all listeners to implement EventSubscriberInterface * added TimeDataCollector * added ContainerAwareTraceableEventDispatcher * moved TraceableEventDispatcherInterface to the EventDispatcher component * added RouterListener, LocaleListener, and StreamedResponseListener * added CacheClearerInterface (and ChainCacheClearer) * added a kernel.terminate event (via TerminableInterface and PostResponseEvent) * added a Stopwatch class * added WarmableInterface * improved extensibility between bundles * added profiler storages for Memcache(d), File-based, MongoDB, Redis * moved Filesystem class to its own component ================================================ FILE: CacheClearer/CacheClearerInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\CacheClearer; /** * CacheClearerInterface. * * @author Dustin Dobervich */ interface CacheClearerInterface { /** * Clears any caches necessary. */ public function clear(string $cacheDir): void; } ================================================ FILE: CacheClearer/ChainCacheClearer.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\CacheClearer; /** * ChainCacheClearer. * * @author Dustin Dobervich * * @final */ class ChainCacheClearer implements CacheClearerInterface { /** * @param iterable $clearers */ public function __construct( private iterable $clearers = [], ) { } public function clear(string $cacheDir): void { foreach ($this->clearers as $clearer) { $clearer->clear($cacheDir); } } } ================================================ FILE: CacheClearer/Psr6CacheClearer.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\CacheClearer; use Psr\Cache\CacheItemPoolInterface; /** * @author Nicolas Grekas */ class Psr6CacheClearer implements CacheClearerInterface { private array $pools = []; /** * @param array $pools */ public function __construct(array $pools = []) { $this->pools = $pools; } public function hasPool(string $name): bool { return isset($this->pools[$name]); } /** * @throws \InvalidArgumentException If the cache pool with the given name does not exist */ public function getPool(string $name): CacheItemPoolInterface { if (!$this->hasPool($name)) { throw new \InvalidArgumentException(\sprintf('Cache pool not found: "%s".', $name)); } return $this->pools[$name]; } /** * @throws \InvalidArgumentException If the cache pool with the given name does not exist */ public function clearPool(string $name): bool { if (!isset($this->pools[$name])) { throw new \InvalidArgumentException(\sprintf('Cache pool not found: "%s".', $name)); } return $this->pools[$name]->clear(); } public function clear(string $cacheDir): void { foreach ($this->pools as $pool) { $pool->clear(); } } } ================================================ FILE: CacheWarmer/CacheWarmer.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\CacheWarmer; /** * Abstract cache warmer that knows how to write a file to the cache. * * @author Fabien Potencier */ abstract class CacheWarmer implements CacheWarmerInterface { protected function writeCacheFile(string $file, $content): void { $tmpFile = @tempnam(\dirname($file), basename($file)); if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $file)) { @chmod($file, 0o666 & ~umask()); return; } throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $file)); } } ================================================ FILE: CacheWarmer/CacheWarmerAggregate.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\CacheWarmer; use Symfony\Component\Console\Style\SymfonyStyle; /** * Aggregates several cache warmers into a single one. * * @author Fabien Potencier * * @final */ class CacheWarmerAggregate implements CacheWarmerInterface { private bool $optionalsEnabled = false; private bool $onlyOptionalsEnabled = false; /** * @param iterable $warmers */ public function __construct( private iterable $warmers = [], private bool $debug = false, private ?string $deprecationLogsFilepath = null, ) { } public function enableOptionalWarmers(): void { $this->optionalsEnabled = true; } public function enableOnlyOptionalWarmers(): void { $this->onlyOptionalsEnabled = $this->optionalsEnabled = true; } public function warmUp(string $cacheDir, ?string $buildDir = null, ?SymfonyStyle $io = null): array { if ($collectDeprecations = $this->debug && !\defined('PHPUNIT_COMPOSER_INSTALL')) { $collectedLogs = []; $previousHandler = set_error_handler(static function ($type, $message, $file, $line) use (&$collectedLogs, &$previousHandler) { if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type) { return $previousHandler ? $previousHandler($type, $message, $file, $line) : false; } if (isset($collectedLogs[$message])) { ++$collectedLogs[$message]['count']; return null; } $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 3); // Clean the trace by removing first frames added by the error handler itself. for ($i = 0; isset($backtrace[$i]); ++$i) { if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) { $backtrace = \array_slice($backtrace, 1 + $i); break; } } $collectedLogs[$message] = [ 'type' => $type, 'message' => $message, 'file' => $file, 'line' => $line, 'trace' => $backtrace, 'count' => 1, ]; return null; }); } $preload = []; try { foreach ($this->warmers as $warmer) { if (!$this->optionalsEnabled && $warmer->isOptional()) { continue; } if ($this->onlyOptionalsEnabled && !$warmer->isOptional()) { continue; } $start = microtime(true); foreach ($warmer->warmUp($cacheDir, $buildDir) as $item) { if (is_dir($item) || (str_starts_with($item, \dirname($cacheDir)) && !is_file($item)) || ($buildDir && str_starts_with($item, \dirname($buildDir)) && !is_file($item))) { throw new \LogicException(\sprintf('"%s::warmUp()" should return a list of files or classes but "%s" is none of them.', $warmer::class, $item)); } $preload[] = $item; } if ($io?->isDebug()) { $io->info(\sprintf('"%s" completed in %0.2fms.', $warmer::class, 1000 * (microtime(true) - $start))); } } } finally { if ($collectDeprecations) { restore_error_handler(); if (is_file($this->deprecationLogsFilepath)) { $previousLogs = unserialize(file_get_contents($this->deprecationLogsFilepath)); if (\is_array($previousLogs)) { $collectedLogs = array_merge($previousLogs, $collectedLogs); } } file_put_contents($this->deprecationLogsFilepath, serialize(array_values($collectedLogs))); } } return array_values(array_unique($preload)); } public function isOptional(): bool { return false; } } ================================================ FILE: CacheWarmer/CacheWarmerInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\CacheWarmer; /** * Interface for classes able to warm up the cache. * * @author Fabien Potencier */ interface CacheWarmerInterface extends WarmableInterface { /** * Checks whether this warmer is optional or not. * * Optional warmers can be ignored on certain conditions. * * A warmer should return true if the cache can be * generated incrementally and on-demand. */ public function isOptional(): bool; } ================================================ FILE: CacheWarmer/WarmableInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\CacheWarmer; /** * Interface for classes that support warming their cache. * * @author Fabien Potencier */ interface WarmableInterface { /** * Warms up the cache. * * @param string $cacheDir Where warm-up artifacts should be stored * @param string|null $buildDir Where read-only artifacts should go; null when called after compile-time * * @return string[] A list of classes or files to preload */ public function warmUp(string $cacheDir, ?string $buildDir = null): array; } ================================================ FILE: Config/FileLocator.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Config; use Symfony\Component\Config\FileLocator as BaseFileLocator; use Symfony\Component\HttpKernel\KernelInterface; /** * FileLocator uses the KernelInterface to locate resources in bundles. * * @author Fabien Potencier */ class FileLocator extends BaseFileLocator { public function __construct( private KernelInterface $kernel, ) { parent::__construct(); } public function locate(string $file, ?string $currentPath = null, bool $first = true): string|array { if (isset($file[0]) && '@' === $file[0]) { $resource = $this->kernel->locateResource($file); return $first ? $resource : [$resource]; } return parent::locate($file, $currentPath, $first); } } ================================================ FILE: Controller/ArgumentResolver/BackedEnumValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Attempt to resolve backed enum cases from request attributes, for a route path parameter, * leading to a 404 Not Found if the attribute value isn't a valid backing value for the enum type. * * @author Maxime Steinhausser */ final class BackedEnumValueResolver implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): iterable { if (!is_subclass_of($argument->getType(), \BackedEnum::class)) { return []; } if ($argument->isVariadic()) { // only target route path parameters, which cannot be variadic. return []; } $name = $argument->getName(); // do not support if no value can be resolved at all // letting the \Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver be used // or \Symfony\Component\HttpKernel\Controller\ArgumentResolver fail with a meaningful error. if (!$request->attributes->has($name)) { return []; } if (null === $value = $request->attributes->get($name)) { return [null]; } if ($value instanceof \BackedEnum) { return [$value]; } /** @var class-string<\BackedEnum> $type */ $type = $argument->getType(); if (!\is_int($value) && !\is_string($value)) { throw new NotFoundHttpException(\sprintf('Could not resolve the "%s $%s" controller argument: expecting an int or string, got "%s".', $type, $name, get_debug_type($value))); } try { return [$type::from($value)]; } catch (\ValueError|\TypeError $e) { throw new NotFoundHttpException(\sprintf('Could not resolve the "%s $%s" controller argument: ', $type, $name).$e->getMessage(), $e); } } } ================================================ FILE: Controller/ArgumentResolver/DateTimeValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Psr\Clock\ClockInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapDateTime; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Convert DateTime instances from request attribute variable. * * @author Benjamin Eberlei * @author Tim Goudriaan */ final class DateTimeValueResolver implements ValueResolverInterface { public function __construct( private readonly ?ClockInterface $clock = null, ) { } public function resolve(Request $request, ArgumentMetadata $argument): array { if (!is_a($argument->getType(), \DateTimeInterface::class, true) || !$request->attributes->has($argument->getName())) { return []; } $value = $request->attributes->get($argument->getName()); $class = \DateTimeInterface::class === $argument->getType() ? \DateTimeImmutable::class : $argument->getType(); if (!$value) { if ($argument->isNullable()) { return [null]; } if (!$this->clock) { return [new $class()]; } $value = $this->clock->now(); } if ($value instanceof \DateTimeInterface) { return [$value instanceof $class ? $value : $class::createFromInterface($value)]; } $format = null; if ($attributes = $argument->getAttributes(MapDateTime::class, ArgumentMetadata::IS_INSTANCEOF)) { $attribute = $attributes[0]; $format = $attribute->format; } if (null !== $format) { $date = $class::createFromFormat($format, $value, $this->clock?->now()->getTimeZone()); if (($class::getLastErrors() ?: ['warning_count' => 0])['warning_count']) { $date = false; } } else { if (false !== filter_var($value, \FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])) { $value = '@'.$value; } try { $date = new $class($value, $this->clock?->now()->getTimeZone()); } catch (\Exception) { $date = false; } } if (!$date) { throw new NotFoundHttpException(\sprintf('Invalid date given for parameter "%s".', $argument->getName())); } return [$date]; } } ================================================ FILE: Controller/ArgumentResolver/DefaultValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; /** * Yields the default value defined in the action signature when no value has been given. * * @author Iltar van der Berg */ final class DefaultValueResolver implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): array { if ($argument->hasDefaultValue()) { return [$argument->getDefaultValue()]; } if (null !== $argument->getType() && $argument->isNullable() && !$argument->isVariadic()) { return [null]; } return []; } } ================================================ FILE: Controller/ArgumentResolver/NotTaggedControllerValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; /** * Provides an intuitive error message when controller fails because it is not registered as a service. * * @author Simeon Kolev */ final class NotTaggedControllerValueResolver implements ValueResolverInterface { public function __construct( private ContainerInterface $container, ) { } public function resolve(Request $request, ArgumentMetadata $argument): array { $controller = $request->attributes->get('_controller'); if (\is_array($controller) && \is_callable($controller, true) && \is_string($controller[0])) { $controller = $controller[0].'::'.$controller[1]; } elseif (!\is_string($controller) || '' === $controller) { return []; } if ('\\' === $controller[0]) { $controller = ltrim($controller, '\\'); } if (!$this->container->has($controller)) { $controller = (false !== $i = strrpos($controller, ':')) ? substr($controller, 0, $i).strtolower(substr($controller, $i)) : $controller.'::__invoke'; } if ($this->container->has($controller)) { return []; } $what = \sprintf('argument $%s of "%s()"', $argument->getName(), $controller); $message = \sprintf('Could not resolve %s, maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?', $what); throw new RuntimeException($message); } } ================================================ FILE: Controller/ArgumentResolver/QueryParameterValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\Uid\AbstractUid; /** * Resolve arguments of type: array, string, int, float, bool, \BackedEnum from query parameters. * * @author Ruud Kamphuis * @author Nicolas Grekas * @author Mateusz Anders * @author Ionut Enache */ final class QueryParameterValueResolver implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): array { if (!$attribute = $argument->getAttributesOfType(MapQueryParameter::class)[0] ?? null) { return []; } $name = $attribute->name ?? $argument->getName(); $validationFailedCode = $attribute->validationFailedStatusCode; if (!$request->query->has($name)) { if ($argument->isNullable() || $argument->hasDefaultValue()) { return []; } throw HttpException::fromStatusCode($validationFailedCode, \sprintf('Missing query parameter "%s".', $name)); } $value = $request->query->all()[$name]; $type = $argument->getType(); if (null === $attribute->filter && 'array' === $type) { if (!$argument->isVariadic()) { return [(array) $value]; } $filtered = array_values(array_filter((array) $value, \is_array(...))); if ($filtered !== $value && !($attribute->flags & \FILTER_NULL_ON_FAILURE)) { throw HttpException::fromStatusCode($validationFailedCode, \sprintf('Invalid query parameter "%s".', $name)); } return $filtered; } $options = [ 'flags' => $attribute->flags | \FILTER_NULL_ON_FAILURE, 'options' => $attribute->options, ]; if ('array' === $type || $argument->isVariadic()) { $value = (array) $value; $options['flags'] |= \FILTER_REQUIRE_ARRAY; } else { $options['flags'] |= \FILTER_REQUIRE_SCALAR; } $uidType = null; if (is_subclass_of($type, AbstractUid::class)) { $uidType = $type; $type = 'uid'; } $enumType = null; $filter = match ($type) { 'array' => \FILTER_DEFAULT, 'string' => isset($attribute->options['regexp']) ? \FILTER_VALIDATE_REGEXP : \FILTER_DEFAULT, 'int' => \FILTER_VALIDATE_INT, 'float' => \FILTER_VALIDATE_FLOAT, 'bool' => \FILTER_VALIDATE_BOOL, 'uid' => \FILTER_DEFAULT, default => match ($enumType = is_subclass_of($type, \BackedEnum::class) ? (new \ReflectionEnum($type))->getBackingType()->getName() : null) { 'int' => \FILTER_VALIDATE_INT, 'string' => \FILTER_DEFAULT, default => throw new \LogicException(\sprintf('#[MapQueryParameter] cannot be used on controller argument "%s$%s" of type "%s"; one of array, string, int, float, bool, uid or \BackedEnum should be used.', $argument->isVariadic() ? '...' : '', $argument->getName(), $type ?? 'mixed')), }, }; $value = filter_var($value, $attribute->filter ?? $filter, $options); if (null !== $enumType && null !== $value) { $enumFrom = static function ($value) use ($type) { if (!\is_string($value) && !\is_int($value)) { return null; } try { return $type::from($value); } catch (\ValueError) { return null; } }; $value = \is_array($value) ? array_map($enumFrom, $value) : $enumFrom($value); } if (null !== $uidType) { $value = \is_array($value) ? array_map([$uidType, 'fromString'], $value) : $uidType::fromString($value); } if (null === $value && !($attribute->flags & \FILTER_NULL_ON_FAILURE)) { throw HttpException::fromStatusCode($validationFailedCode, \sprintf('Invalid query parameter "%s".', $name)); } if (!\is_array($value)) { return [$value]; } $filtered = array_filter($value, static fn ($v) => null !== $v); if ($argument->isVariadic()) { $filtered = array_values($filtered); } if ($filtered !== $value && !($attribute->flags & \FILTER_NULL_ON_FAILURE)) { throw HttpException::fromStatusCode($validationFailedCode, \sprintf('Invalid query parameter "%s".', $name)); } return $argument->isVariadic() ? $filtered : [$filtered]; } } ================================================ FILE: Controller/ArgumentResolver/RequestAttributeValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Yields a non-variadic argument's value from the request attributes. * * @author Iltar van der Berg */ final class RequestAttributeValueResolver implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): array { if ($argument->isVariadic()) { return []; } $name = $argument->getName(); if (!$request->attributes->has($name)) { return []; } $value = $request->attributes->get($name); if (null === $value && $argument->isNullable()) { return [null]; } $type = $argument->getType(); // Skip when no type declaration or complex types; fall back to other resolvers/defaults if (null === $type || str_contains($type, '|') || str_contains($type, '&')) { return [$value]; } if ('string' === $type) { if (!\is_scalar($value) && !$value instanceof \Stringable) { throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); } $value = (string) $value; } elseif ($filter = match ($type) { 'int' => \FILTER_VALIDATE_INT, 'float' => \FILTER_VALIDATE_FLOAT, 'bool' => \FILTER_VALIDATE_BOOL, default => null, }) { if (null === $value = $request->attributes->filter($name, null, $filter, ['flags' => \FILTER_NULL_ON_FAILURE | \FILTER_REQUIRE_SCALAR])) { throw new NotFoundHttpException(\sprintf('The value for the "%s" route parameter is invalid.', $name)); } } return [$value]; } } ================================================ FILE: Controller/ArgumentResolver/RequestHeaderValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\AcceptHeader; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapRequestHeader; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\HttpException; final class RequestHeaderValueResolver implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): array { if (!$attribute = $argument->getAttributesOfType(MapRequestHeader::class)[0] ?? null) { return []; } $type = $argument->getType(); if (!\in_array($type, ['string', 'array', AcceptHeader::class])) { throw new \LogicException(\sprintf('Could not resolve the argument typed "%s". Valid types are "array", "string" or "%s".', $type, AcceptHeader::class)); } $name = $attribute->name ?? strtolower(preg_replace('/[a-z]\K[A-Z]/', '-$0', $argument->getName())); $value = null; if ($request->headers->has($name)) { $value = match ($type) { 'string' => $request->headers->get($name), 'array' => match (strtolower($name)) { 'accept' => $request->getAcceptableContentTypes(), 'accept-charset' => $request->getCharsets(), 'accept-language' => $request->getLanguages(), 'accept-encoding' => $request->getEncodings(), default => $request->headers->all($name), }, AcceptHeader::class => AcceptHeader::fromString($request->headers->get($name)), }; } elseif ($argument->hasDefaultValue()) { $value = $argument->getDefaultValue(); } if (null === $value && 'array' === $type) { $value = []; } if (null === $value && !$argument->isNullable()) { throw HttpException::fromStatusCode($attribute->validationFailedStatusCode, \sprintf('Missing header "%s".', $name)); } return [$value]; } } ================================================ FILE: Controller/ArgumentResolver/RequestPayloadValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Serializer\Exception\InvalidArgumentException as SerializerInvalidArgumentException; use Symfony\Component\Serializer\Exception\NotEncodableValueException; use Symfony\Component\Serializer\Exception\PartialDenormalizationException; use Symfony\Component\Serializer\Exception\UnexpectedPropertyException; use Symfony\Component\Serializer\Exception\UnsupportedFormatException; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Exception\ValidationFailedException; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Konstantin Myakshin * * @psalm-type GroupResolver = \Closure(array, Request, ?object):string|GroupSequence|array * * @final */ class RequestPayloadValueResolver implements ValueResolverInterface, EventSubscriberInterface { /** * @see DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS */ private const CONTEXT_DENORMALIZE = [ 'collect_denormalization_errors' => true, ]; /** * @see DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS */ private const CONTEXT_DESERIALIZE = [ 'collect_denormalization_errors' => true, ]; public function __construct( private readonly SerializerInterface&DenormalizerInterface $serializer, private readonly ?ValidatorInterface $validator = null, private readonly ?TranslatorInterface $translator = null, private string $translationDomain = 'validators', private ?ExpressionLanguage $expressionLanguage = null, ) { } public function resolve(Request $request, ArgumentMetadata $argument): iterable { $attribute = $argument->getAttributesOfType(MapQueryString::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? $argument->getAttributesOfType(MapRequestPayload::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? $argument->getAttributesOfType(MapUploadedFile::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null; if (!$attribute) { return []; } if ($attribute instanceof MapQueryString && $argument->isVariadic()) { throw new \LogicException(\sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName())); } if ($attribute instanceof MapRequestPayload) { if ('array' === $argument->getType()) { if (!$attribute->type) { throw new NearMissValueResolverException(\sprintf('Please set the $type argument of the #[%s] attribute to the type of the objects in the expected array.', MapRequestPayload::class)); } } elseif ($attribute->type && !$argument->isVariadic()) { throw new NearMissValueResolverException(\sprintf('Please set its type to "array" when using argument $type of #[%s].', MapRequestPayload::class)); } } $attribute->metadata = $argument; return [$attribute]; } public function onKernelControllerArguments(ControllerArgumentsEvent $event): void { $arguments = $event->getArguments(); foreach ($arguments as $i => $argument) { if ($argument instanceof MapQueryString) { $payloadMapper = $this->mapQueryString(...); $validationFailedCode = $argument->validationFailedStatusCode; } elseif ($argument instanceof MapRequestPayload) { $payloadMapper = $this->mapRequestPayload(...); $validationFailedCode = $argument->validationFailedStatusCode; } elseif ($argument instanceof MapUploadedFile) { $payloadMapper = $this->mapUploadedFile(...); $validationFailedCode = $argument->validationFailedStatusCode; } else { continue; } $request = $event->getRequest(); if (!$argument->metadata->getType()) { throw new \LogicException(\sprintf('Could not resolve the "$%s" controller argument: argument should be typed.', $argument->metadata->getName())); } if ($this->validator) { $violations = new ConstraintViolationList(); try { $payload = $payloadMapper($request, $argument->metadata, $argument); } catch (PartialDenormalizationException $e) { $trans = $this->translator ? $this->translator->trans(...) : static fn ($m, $p) => strtr($m, $p); foreach ($e->getErrors() as $error) { $parameters = []; $template = 'This value was of an unexpected type.'; if ($expectedTypes = $error->getExpectedTypes()) { $template = 'This value should be of type {{ type }}.'; $parameters['{{ type }}'] = implode('|', $expectedTypes); } if ($error->canUseMessageForUser()) { $parameters['hint'] = $error->getMessage(); } $message = $trans($template, $parameters, $this->translationDomain); $violations->add(new ConstraintViolation($message, $template, $parameters, null, $error->getPath(), null)); } $payload = $e->getData(); } catch (SerializerInvalidArgumentException $e) { $violations->add(new ConstraintViolation($e->getMessage(), $e->getMessage(), [], null, '', null)); $payload = null; } if (null !== $payload && !\count($violations)) { $constraints = $argument->constraints ?? null; if (\is_array($payload) && !empty($constraints) && !$constraints instanceof Assert\All) { $constraints = new Assert\All($constraints); } $groups = $this->resolveValidationGroups($argument->validationGroups ?? null, $event); if ($argument instanceof MapUploadedFile) { $violations->addAll($this->validator->startContext()->atPath($argument->metadata->getName())->validate($payload, $constraints, $groups)->getViolations()); } else { $violations->addAll($this->validator->validate($payload, $constraints, $groups)); } } if (\count($violations)) { throw HttpException::fromStatusCode($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), iterator_to_array($violations))), new ValidationFailedException($payload, $violations)); } } else { try { $payload = $payloadMapper($request, $argument->metadata, $argument); } catch (PartialDenormalizationException $e) { throw HttpException::fromStatusCode($validationFailedCode, implode("\n", array_map(static fn ($e) => $e->getMessage(), $e->getErrors())), $e); } catch (SerializerInvalidArgumentException $e) { throw HttpException::fromStatusCode($validationFailedCode, $e->getMessage(), $e); } } if ($argument->metadata->isVariadic()) { array_splice($arguments, $i, 1, $payload ?? []); continue; } if (null === $payload) { $payload = match (true) { $argument->metadata->hasDefaultValue() => $argument->metadata->getDefaultValue(), $argument->metadata->isNullable() => null, default => throw HttpException::fromStatusCode($validationFailedCode), }; } $arguments[$i] = $payload; } $event->setArguments($arguments); } public static function getSubscribedEvents(): array { return [ KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments', ]; } private function mapQueryString(Request $request, ArgumentMetadata $argument, MapQueryString $attribute): ?object { if (!($data = $request->query->all($attribute->key)) && ($argument->isNullable() || $argument->hasDefaultValue()) && !$attribute->mapWhenEmpty) { return null; } return $this->serializer->denormalize($data, $argument->getType(), 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]); } private function mapRequestPayload(Request $request, ArgumentMetadata $argument, MapRequestPayload $attribute): object|array|null { if ('' === $data = $request->request->all() ?: $request->getContent()) { if ($attribute->mapWhenEmpty) { $data = []; } elseif ($argument->isNullable() || $argument->hasDefaultValue()) { return null; } } if (null === $format = $request->getContentTypeFormat()) { throw new UnsupportedMediaTypeHttpException('Unsupported format.'); } if ($attribute->acceptFormat && !\in_array($format, (array) $attribute->acceptFormat, true)) { throw new UnsupportedMediaTypeHttpException(\sprintf('Unsupported format, expects "%s", but "%s" given.', implode('", "', (array) $attribute->acceptFormat), $format)); } $type = match (true) { $argument->isVariadic() => ($attribute->type ?? $argument->getType()).'[]', 'array' === $argument->getType() && null !== $attribute->type => $attribute->type.'[]', default => $argument->getType(), }; if (\is_array($data)) { $data = $this->mergeParamsAndFiles($data, $request->files->all()); return $this->serializer->denormalize($data, $type, self::hasNonStringScalar($data) ? $format : 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : [])); } if ('form' === $format) { throw new BadRequestHttpException('Request payload contains invalid "form" data.'); } try { return $this->serializer->deserialize($data, $type, $format, self::CONTEXT_DESERIALIZE + $attribute->serializationContext); } catch (UnsupportedFormatException $e) { throw new UnsupportedMediaTypeHttpException(\sprintf('Unsupported format: "%s".', $format), $e); } catch (NotEncodableValueException $e) { throw new BadRequestHttpException(\sprintf('Request payload contains invalid "%s" data.', $format), $e); } catch (UnexpectedPropertyException $e) { throw new BadRequestHttpException(\sprintf('Request payload contains invalid "%s" property.', $e->property), $e); } } private function mapUploadedFile(Request $request, ArgumentMetadata $argument, MapUploadedFile $attribute): UploadedFile|array|null { if ($files = $request->files->get($attribute->name ?? $argument->getName())) { return !\is_array($files) && $argument->isVariadic() ? [$files] : $files; } if ($argument->isNullable() || $argument->hasDefaultValue()) { return null; } return 'array' === $argument->getType() ? [] : null; } private function mergeParamsAndFiles(array $params, array $files): array { $isFilesList = array_is_list($files); foreach ($params as $key => $value) { if (\is_array($value) && \is_array($files[$key] ?? null)) { $params[$key] = $this->mergeParamsAndFiles($value, $files[$key]); unset($files[$key]); } } if (!$isFilesList) { return array_replace($params, $files); } foreach ($files as $value) { $params[] = $value; } return $params; } private function resolveValidationGroups(Expression|string|GroupSequence|\Closure|array|null $validationGroups, ControllerArgumentsEvent $event): string|GroupSequence|array|null { if ($validationGroups instanceof Expression || $validationGroups instanceof \Closure) { $validationGroups = $event->evaluate($validationGroups, $this->expressionLanguage); } if (null === $validationGroups || \is_string($validationGroups) || $validationGroups instanceof GroupSequence) { return $validationGroups; } if (!\is_array($validationGroups)) { throw new \LogicException('The validation groups expression or closure must return a string, an array of strings, or a GroupSequence.'); } foreach ($validationGroups as $group) { if ($group instanceof Expression) { throw new \LogicException('Nested expressions in validation groups are not supported. Use a single Expression or a list of strings (or a GroupSequence) instead.'); } if ($group instanceof \Closure) { throw new \LogicException('Nested closures in validation groups are not supported. Use a single Closure or a list of strings (or a GroupSequence) instead.'); } if ($group instanceof GroupSequence) { throw new \LogicException('GroupSequence cannot be used inside an array of validation groups. Pass the GroupSequence as the top-level validationGroups value instead.'); } if (!\is_string($group)) { throw new \LogicException('Validation groups must be strings.'); } } return $validationGroups; } private static function hasNonStringScalar(array $data): bool { $stack = [$data]; while ($stack) { foreach (array_pop($stack) as $v) { if (\is_array($v)) { $stack[] = $v; } elseif (!\is_string($v)) { return true; } } } return false; } } ================================================ FILE: Controller/ArgumentResolver/RequestValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; /** * Yields the same instance as the request object passed along. * * @author Iltar van der Berg */ final class RequestValueResolver implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): array { if (Request::class === $argument->getType() || is_subclass_of($argument->getType(), Request::class)) { return [$request]; } if (str_ends_with($argument->getType() ?? '', '\\Request')) { throw new NearMissValueResolverException(\sprintf('Looks like you required a Request object with the wrong class name "%s". Did you mean to use "%s" instead?', $argument->getType(), Request::class)); } return []; } } ================================================ FILE: Controller/ArgumentResolver/ServiceValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; /** * Yields a service keyed by _controller and argument name. * * @author Nicolas Grekas */ final class ServiceValueResolver implements ValueResolverInterface { public function __construct( private ContainerInterface $container, ) { } public function resolve(Request $request, ArgumentMetadata $argument): array { $controller = $request->attributes->get('_controller'); if (\is_array($controller) && \is_callable($controller, true) && \is_string($controller[0])) { $controller = $controller[0].'::'.$controller[1]; } elseif (!\is_string($controller) || '' === $controller) { return []; } if ('\\' === $controller[0]) { $controller = ltrim($controller, '\\'); } if (!$this->container->has($controller) && false !== $i = strrpos($controller, ':')) { $controller = substr($controller, 0, $i).strtolower(substr($controller, $i)); } if (!$this->container->has($controller) || !$this->container->get($controller)->has($argument->getName())) { return []; } try { return [$this->container->get($controller)->get($argument->getName())]; } catch (RuntimeException $e) { $what = 'argument $'.$argument->getName(); $message = str_replace(\sprintf('service "%s"', $argument->getName()), $what, $e->getMessage()); $what .= \sprintf(' of "%s()"', $controller); $message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $message); if ($e->getMessage() === $message) { $message = \sprintf('Cannot resolve %s: %s', $what, $message); } throw new NearMissValueResolverException($message, $e->getCode(), $e); } } } ================================================ FILE: Controller/ArgumentResolver/SessionValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; /** * Yields the Session. * * @author Iltar van der Berg */ final class SessionValueResolver implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): array { if (!$request->hasSession()) { return []; } $type = $argument->getType(); if (SessionInterface::class !== $type && !is_subclass_of($type, SessionInterface::class)) { return []; } return $request->getSession() instanceof $type ? [$request->getSession()] : []; } } ================================================ FILE: Controller/ArgumentResolver/TraceableValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Stopwatch\Stopwatch; /** * Provides timing information via the stopwatch. * * @author Iltar van der Berg */ final class TraceableValueResolver implements ValueResolverInterface { public function __construct( private ValueResolverInterface $inner, private Stopwatch $stopwatch, ) { } public function resolve(Request $request, ArgumentMetadata $argument): iterable { $method = $this->inner::class.'::'.__FUNCTION__; $this->stopwatch->start($method, 'controller.argument_value_resolver'); yield from $this->inner->resolve($request, $argument); $this->stopwatch->stop($method); } } ================================================ FILE: Controller/ArgumentResolver/UidValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Uid\AbstractUid; final class UidValueResolver implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): array { if ($argument->isVariadic() || !\is_string($value = $request->attributes->get($argument->getName())) || null === ($uidClass = $argument->getType()) || !is_subclass_of($uidClass, AbstractUid::class, true) ) { return []; } try { return [$uidClass::fromString($value)]; } catch (\InvalidArgumentException $e) { throw new NotFoundHttpException(\sprintf('The uid for the "%s" parameter is invalid.', $argument->getName()), $e); } } } ================================================ FILE: Controller/ArgumentResolver/VariadicValueResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; /** * Yields a variadic argument's values from the request attributes. * * @author Iltar van der Berg */ final class VariadicValueResolver implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): array { if (!$argument->isVariadic() || !$request->attributes->has($argument->getName())) { return []; } $values = $request->attributes->get($argument->getName()); if (!\is_array($values)) { throw new \InvalidArgumentException(\sprintf('The action argument "...$%1$s" is required to be an array, the request attribute "%1$s" contains a type of "%2$s" instead.', $argument->getName(), get_debug_type($values))); } return $values; } } ================================================ FILE: Controller/ArgumentResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller; use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\ValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\SessionValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; use Symfony\Component\HttpKernel\Exception\ResolverNotFoundException; use Symfony\Contracts\Service\ServiceProviderInterface; /** * Responsible for resolving the arguments passed to an action. * * @author Iltar van der Berg */ final class ArgumentResolver implements ArgumentResolverInterface { private ArgumentMetadataFactoryInterface $argumentMetadataFactory; private iterable $argumentValueResolvers; /** * @param iterable $argumentValueResolvers */ public function __construct( ?ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, iterable $argumentValueResolvers = [], private ?ContainerInterface $namedResolvers = null, ) { $this->argumentMetadataFactory = $argumentMetadataFactory ?? new ArgumentMetadataFactory(); $this->argumentValueResolvers = $argumentValueResolvers ?: self::getDefaultArgumentValueResolvers(); } public function getArguments(Request $request, callable $controller, ?\ReflectionFunctionAbstract $reflector = null): array { $arguments = []; foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller, $reflector) as $metadata) { $argumentValueResolvers = $this->argumentValueResolvers; $disabledResolvers = []; if ($this->namedResolvers && $attributes = $metadata->getAttributesOfType(ValueResolver::class, $metadata::IS_INSTANCEOF)) { $resolverName = null; foreach ($attributes as $attribute) { if ($attribute->disabled) { $disabledResolvers[$attribute->resolver] = true; } elseif ($resolverName) { throw new \LogicException(\sprintf('You can only pin one resolver per argument, but argument "$%s" of "%s()" has more.', $metadata->getName(), $metadata->getControllerName())); } else { $resolverName = $attribute->resolver; } } if ($resolverName) { if (!$this->namedResolvers->has($resolverName)) { throw new ResolverNotFoundException($resolverName, $this->namedResolvers instanceof ServiceProviderInterface ? array_keys($this->namedResolvers->getProvidedServices()) : []); } $argumentValueResolvers = [ $this->namedResolvers->get($resolverName), new RequestAttributeValueResolver(), new DefaultValueResolver(), ]; } } $valueResolverExceptions = []; foreach ($argumentValueResolvers as $name => $resolver) { if (isset($disabledResolvers[\is_int($name) ? $resolver::class : $name])) { continue; } try { $count = 0; foreach ($resolver->resolve($request, $metadata) as $argument) { ++$count; $arguments[] = $argument; } } catch (NearMissValueResolverException $e) { $valueResolverExceptions[] = $e; } if (1 < $count && !$metadata->isVariadic()) { throw new \InvalidArgumentException(\sprintf('"%s::resolve()" must yield at most one value for non-variadic arguments.', get_debug_type($resolver))); } if ($count) { // continue to the next controller argument continue 2; } } $reasons = array_map(static fn (NearMissValueResolverException $e) => $e->getMessage(), $valueResolverExceptions); if (!$reasons) { $reasons[] = 'Either the argument is nullable and no null value has been provided, no default value has been provided or there is a non-optional argument after this one.'; } $reasonCounter = 1; if (\count($reasons) > 1) { foreach ($reasons as $i => $reason) { $reasons[$i] = $reasonCounter.') '.$reason; ++$reasonCounter; } } throw new \RuntimeException(\sprintf('Controller "%s" requires the "$%s" argument that could not be resolved. '.($reasonCounter > 1 ? 'Possible reasons: ' : '').'%s', $metadata->getControllerName(), $metadata->getName(), implode(' ', $reasons))); } return $arguments; } /** * @return iterable */ public static function getDefaultArgumentValueResolvers(): iterable { return [ new RequestAttributeValueResolver(), new RequestValueResolver(), new SessionValueResolver(), new DefaultValueResolver(), new VariadicValueResolver(), ]; } } ================================================ FILE: Controller/ArgumentResolverInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller; use Symfony\Component\HttpFoundation\Request; /** * An ArgumentResolverInterface instance knows how to determine the * arguments for a specific action. * * @author Fabien Potencier */ interface ArgumentResolverInterface { /** * Returns the arguments to pass to the controller. * * @throws \RuntimeException When no value could be provided for a required argument */ public function getArguments(Request $request, callable $controller, ?\ReflectionFunctionAbstract $reflector = null): array; } ================================================ FILE: Controller/ContainerControllerResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Container; /** * A controller resolver searching for a controller in a psr-11 container when using the "service::method" notation. * * @author Fabien Potencier * @author Maxime Steinhausser */ class ContainerControllerResolver extends ControllerResolver { public function __construct( protected ContainerInterface $container, ?LoggerInterface $logger = null, ) { parent::__construct($logger); } protected function instantiateController(string $class): object { $class = ltrim($class, '\\'); if ($this->container->has($class)) { return $this->container->get($class); } try { return parent::instantiateController($class); } catch (\Error $e) { } $this->throwExceptionIfControllerWasRemoved($class, $e); if ($e instanceof \ArgumentCountError) { throw new \InvalidArgumentException(\sprintf('Controller "%s" has required constructor arguments and does not exist in the container. Did you forget to define the controller as a service?', $class), 0, $e); } throw new \InvalidArgumentException(\sprintf('Controller "%s" does neither exist as service nor as class.', $class), 0, $e); } private function throwExceptionIfControllerWasRemoved(string $controller, \Throwable $previous): void { if ($this->container instanceof Container && isset($this->container->getRemovedIds()[$controller])) { throw new \InvalidArgumentException(\sprintf('Controller "%s" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?', $controller), 0, $previous); } } } ================================================ FILE: Controller/ControllerReference.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller; use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface; /** * Acts as a marker and a data holder for a Controller. * * Some methods in Symfony accept both a URI (as a string) or a controller as * an argument. In the latter case, instead of passing an array representing * the controller, you can use an instance of this class. * * @author Fabien Potencier * * @see FragmentRendererInterface */ class ControllerReference { public array $attributes = []; public array $query = []; /** * @param string $controller The controller name * @param array $attributes An array of parameters to add to the Request attributes * @param array $query An array of parameters to add to the Request query string */ public function __construct( public string $controller, array $attributes = [], array $query = [], ) { $this->attributes = $attributes; $this->query = $query; } } ================================================ FILE: Controller/ControllerResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\AsController; /** * This implementation uses the '_controller' request attribute to determine * the controller to execute. * * @author Fabien Potencier * @author Tobias Schultze */ class ControllerResolver implements ControllerResolverInterface { private array $allowedControllerTypes = []; private array $allowedControllerAttributes = [AsController::class => AsController::class]; public function __construct( private ?LoggerInterface $logger = null, ) { } /** * @param array $types * @param array $attributes */ public function allowControllers(array $types = [], array $attributes = []): void { foreach ($types as $type) { $this->allowedControllerTypes[$type] = $type; } foreach ($attributes as $attribute) { $this->allowedControllerAttributes[$attribute] = $attribute; } } /** * @throws BadRequestException when the request has attribute "_check_controller_is_allowed" set to true and the controller is not allowed */ public function getController(Request $request): callable|false { if (!$controller = $request->attributes->get('_controller')) { $this->logger?->warning('Unable to look for the controller as the "_controller" parameter is missing.'); return false; } if (\is_array($controller)) { if (isset($controller[0]) && \is_string($controller[0]) && isset($controller[1])) { try { $controller[0] = $this->instantiateController($controller[0]); } catch (\Error|\LogicException $e) { if (\is_callable($controller)) { return $this->checkController($request, $controller); } throw $e; } } if (!\is_callable($controller)) { throw new \InvalidArgumentException(\sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$this->getControllerError($controller)); } return $this->checkController($request, $controller); } if (\is_object($controller)) { if (!\is_callable($controller)) { throw new \InvalidArgumentException(\sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$this->getControllerError($controller)); } return $this->checkController($request, $controller); } if (\function_exists($controller)) { return $this->checkController($request, $controller); } try { $callable = $this->createController($controller); } catch (\InvalidArgumentException $e) { throw new \InvalidArgumentException(\sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$e->getMessage(), 0, $e); } if (!\is_callable($callable)) { throw new \InvalidArgumentException(\sprintf('The controller for URI "%s" is not callable: ', $request->getPathInfo()).$this->getControllerError($callable)); } return $this->checkController($request, $callable); } /** * Returns a callable for the given controller. * * @throws \InvalidArgumentException When the controller cannot be created */ protected function createController(string $controller): callable { if (!str_contains($controller, '::')) { $controller = $this->instantiateController($controller); if (!\is_callable($controller)) { throw new \InvalidArgumentException($this->getControllerError($controller)); } return $controller; } [$class, $method] = explode('::', $controller, 2); try { $controller = [$this->instantiateController($class), $method]; } catch (\Error|\LogicException $e) { try { if ((new \ReflectionMethod($class, $method))->isStatic()) { return $class.'::'.$method; } } catch (\ReflectionException) { throw $e; } throw $e; } if (!\is_callable($controller)) { throw new \InvalidArgumentException($this->getControllerError($controller)); } return $controller; } /** * Returns an instantiated controller. */ protected function instantiateController(string $class): object { return new $class(); } private function getControllerError(mixed $callable): string { if (\is_string($callable)) { if (str_contains($callable, '::')) { $callable = explode('::', $callable, 2); } else { return \sprintf('Function "%s" does not exist.', $callable); } } if (\is_object($callable)) { $availableMethods = $this->getClassMethodsWithoutMagicMethods($callable); $alternativeMsg = $availableMethods ? \sprintf(' or use one of the available methods: "%s"', implode('", "', $availableMethods)) : ''; return \sprintf('Controller class "%s" cannot be called without a method name. You need to implement "__invoke"%s.', get_debug_type($callable), $alternativeMsg); } if (!\is_array($callable)) { return \sprintf('Invalid type for controller given, expected string, array or object, got "%s".', get_debug_type($callable)); } if (!isset($callable[0]) || !isset($callable[1]) || 2 !== \count($callable)) { return 'Invalid array callable, expected [controller, method].'; } [$controller, $method] = $callable; if (\is_string($controller) && !class_exists($controller)) { return \sprintf('Class "%s" does not exist.', $controller); } $className = \is_object($controller) ? get_debug_type($controller) : $controller; if (method_exists($controller, $method)) { return \sprintf('Method "%s" on class "%s" should be public and non-abstract.', $method, $className); } $collection = $this->getClassMethodsWithoutMagicMethods($controller); $alternatives = []; foreach ($collection as $item) { $lev = levenshtein($method, $item); if ($lev <= \strlen($method) / 3 || str_contains($item, $method)) { $alternatives[] = $item; } } asort($alternatives); $message = \sprintf('Expected method "%s" on class "%s"', $method, $className); if (\count($alternatives) > 0) { $message .= \sprintf(', did you mean "%s"?', implode('", "', $alternatives)); } else { $message .= \sprintf('. Available methods: "%s".', implode('", "', $collection)); } return $message; } private function getClassMethodsWithoutMagicMethods($classOrObject): array { $methods = get_class_methods($classOrObject); return array_filter($methods, static fn (string $method) => 0 !== strncmp($method, '__', 2)); } private function checkController(Request $request, callable $controller): callable { if (!$request->attributes->get('_check_controller_is_allowed', false)) { return $controller; } $r = null; if (\is_array($controller)) { [$class, $name] = $controller; $name = (\is_string($class) ? $class : $class::class).'::'.$name; } elseif (\is_object($controller) && !$controller instanceof \Closure) { $class = $controller; $name = $class::class.'::__invoke'; } else { $r = new \ReflectionFunction($controller); $name = $r->name; if ($r->isAnonymous()) { $name = $class = \Closure::class; } elseif ($class = $r->getClosureCalledClass()) { $class = $class->name; $name = $class.'::'.$name; } } if ($class) { foreach ($this->allowedControllerTypes as $type) { if (is_a($class, $type, true)) { return $controller; } } } $r ??= new \ReflectionClass($class); foreach ($r->getAttributes() as $attribute) { if (isset($this->allowedControllerAttributes[$attribute->getName()])) { return $controller; } } if (str_contains($name, '@anonymous')) { $name = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', static fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $name); } throw new BadRequestException(\sprintf('Callable "%s()" is not allowed as a controller. Did you miss tagging it with "#[AsController]" or registering its type with "%s::allowControllers()"?', $name, self::class)); } } ================================================ FILE: Controller/ControllerResolverInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller; use Symfony\Component\HttpFoundation\Request; /** * A ControllerResolverInterface implementation knows how to determine the * controller to execute based on a Request object. * * A Controller can be any valid PHP callable. * * @author Fabien Potencier */ interface ControllerResolverInterface { /** * Returns the Controller instance associated with a Request. * * As several resolvers can exist for a single application, a resolver must * return false when it is not able to determine the controller. * * The resolver must only throw an exception when it should be able to load a * controller but cannot because of some errors made by the developer. * * @return callable|false A PHP callable representing the Controller, * or false if this resolver is not able to determine the controller * * @throws \LogicException If a controller was found based on the request but it is not callable */ public function getController(Request $request): callable|false; } ================================================ FILE: Controller/ErrorController.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller; use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Renders error or exception pages from a given FlattenException. * * @author Yonel Ceruto * @author Matthias Pigulla */ class ErrorController { public function __construct( private HttpKernelInterface $kernel, private string|object|array|null $controller, private ErrorRendererInterface $errorRenderer, ) { } public function __invoke(\Throwable $exception): Response { $exception = $this->errorRenderer->render($exception); return new Response($exception->getAsString(), $exception->getStatusCode(), $exception->getHeaders()); } public function preview(Request $request, int $code): Response { /* * This Request mimics the parameters set by * \Symfony\Component\HttpKernel\EventListener\ErrorListener::duplicateRequest, with * the additional "showException" flag. */ $subRequest = $request->duplicate(null, null, [ '_controller' => $this->controller, 'exception' => new HttpException($code, 'This is a sample exception.'), 'logger' => null, 'showException' => false, ]); return $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST); } } ================================================ FILE: Controller/TraceableArgumentResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Stopwatch\Stopwatch; /** * @author Fabien Potencier */ class TraceableArgumentResolver implements ArgumentResolverInterface { public function __construct( private ArgumentResolverInterface $resolver, private Stopwatch $stopwatch, ) { } public function getArguments(Request $request, callable $controller, ?\ReflectionFunctionAbstract $reflector = null): array { $e = $this->stopwatch->start('controller.get_arguments'); try { return $this->resolver->getArguments($request, $controller, $reflector); } finally { $e->stop(); } } } ================================================ FILE: Controller/TraceableControllerResolver.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Stopwatch\Stopwatch; /** * @author Fabien Potencier */ class TraceableControllerResolver implements ControllerResolverInterface { public function __construct( private ControllerResolverInterface $resolver, private Stopwatch $stopwatch, ) { } public function getController(Request $request): callable|false { $e = $this->stopwatch->start('controller.get_callable'); try { return $this->resolver->getController($request); } finally { $e->stop(); } } } ================================================ FILE: Controller/ValueResolverInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; /** * Responsible for resolving the value of an argument based on its metadata. * * @author Nicolas Grekas */ interface ValueResolverInterface { /** * Returns the possible value(s). */ public function resolve(Request $request, ArgumentMetadata $argument): iterable; } ================================================ FILE: ControllerMetadata/ArgumentMetadata.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\ControllerMetadata; /** * Responsible for storing metadata of an argument. * * @author Iltar van der Berg */ class ArgumentMetadata { public const IS_INSTANCEOF = 2; /** * @param object[] $attributes */ public function __construct( private string $name, private ?string $type, private bool $isVariadic, private bool $hasDefaultValue, private mixed $defaultValue, private bool $isNullable = false, private array $attributes = [], private string $controllerName = 'n/a', ) { $this->isNullable = $isNullable || null === $type || ($hasDefaultValue && null === $defaultValue); } /** * Returns the name as given in PHP, $foo would yield "foo". */ public function getName(): string { return $this->name; } /** * Returns the type of the argument. */ public function getType(): ?string { return $this->type; } /** * Returns whether the argument is defined as "...$variadic". */ public function isVariadic(): bool { return $this->isVariadic; } /** * Returns whether the argument has a default value. * * Implies whether an argument is optional. */ public function hasDefaultValue(): bool { return $this->hasDefaultValue; } /** * Returns whether the argument accepts null values. */ public function isNullable(): bool { return $this->isNullable; } /** * Returns the default value of the argument. * * @throws \LogicException if no default value is present; {@see self::hasDefaultValue()} */ public function getDefaultValue(): mixed { if (!$this->hasDefaultValue) { throw new \LogicException(\sprintf('Argument $%s does not have a default value. Use "%s::hasDefaultValue()" to avoid this exception.', $this->name, __CLASS__)); } return $this->defaultValue; } /** * @param class-string $name * @param self::IS_INSTANCEOF|0 $flags * * @return array */ public function getAttributes(?string $name = null, int $flags = 0): array { if (!$name) { return $this->attributes; } return $this->getAttributesOfType($name, $flags); } /** * @template T of object * * @param class-string $name * @param self::IS_INSTANCEOF|0 $flags * * @return array */ public function getAttributesOfType(string $name, int $flags = 0): array { $attributes = []; if ($flags & self::IS_INSTANCEOF) { foreach ($this->attributes as $attribute) { if ($attribute instanceof $name) { $attributes[] = $attribute; } } } else { foreach ($this->attributes as $attribute) { if ($attribute::class === $name) { $attributes[] = $attribute; } } } return $attributes; } public function getControllerName(): string { return $this->controllerName; } } ================================================ FILE: ControllerMetadata/ArgumentMetadataFactory.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\ControllerMetadata; /** * Builds {@see ArgumentMetadata} objects based on the given Controller. * * @author Iltar van der Berg */ final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface { public function createArgumentMetadata(string|object|array $controller, ?\ReflectionFunctionAbstract $reflector = null): array { $arguments = []; $reflector ??= new \ReflectionFunction($controller(...)); $controllerName = $this->getPrettyName($reflector); foreach ($reflector->getParameters() as $param) { $attributes = []; foreach ($param->getAttributes() as $reflectionAttribute) { if (class_exists($reflectionAttribute->getName())) { $attributes[] = $reflectionAttribute->newInstance(); } } $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull(), $attributes, $controllerName); } return $arguments; } /** * Returns an associated type to the given parameter if available. */ private function getType(\ReflectionParameter $parameter): ?string { if (!$type = $parameter->getType()) { return null; } $name = $type instanceof \ReflectionNamedType ? $type->getName() : (string) $type; return match (strtolower($name)) { 'self' => $parameter->getDeclaringClass()?->name, 'parent' => get_parent_class($parameter->getDeclaringClass()?->name ?? '') ?: null, default => $name, }; } private function getPrettyName(\ReflectionFunctionAbstract $r): string { $name = $r->name; if ($r instanceof \ReflectionMethod) { return $r->class.'::'.$name; } if ($r->isAnonymous() || !$class = $r->getClosureCalledClass()) { return $name; } return $class->name.'::'.$name; } } ================================================ FILE: ControllerMetadata/ArgumentMetadataFactoryInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\ControllerMetadata; /** * Builds method argument data. * * @author Iltar van der Berg */ interface ArgumentMetadataFactoryInterface { /** * @return ArgumentMetadata[] */ public function createArgumentMetadata(string|object|array $controller, ?\ReflectionFunctionAbstract $reflector = null): array; } ================================================ FILE: DataCollector/AjaxDataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** * @author Bart van den Burg * * @final */ class AjaxDataCollector extends DataCollector { public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { // all collecting is done client side } public function reset(): void { // all collecting is done client side } public function getName(): string { return 'ajax'; } } ================================================ FILE: DataCollector/ConfigDataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Runtime\RunnerInterface; use Symfony\Component\VarDumper\Caster\ClassStub; use Symfony\Component\VarDumper\Cloner\Data; /** * @author Fabien Potencier * * @final */ class ConfigDataCollector extends DataCollector implements LateDataCollectorInterface { private KernelInterface $kernel; /** * Sets the Kernel associated with this Request. */ public function setKernel(KernelInterface $kernel): void { $this->kernel = $kernel; } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { $eom = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE); $eol = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE); $xdebugMode = getenv('XDEBUG_MODE') ?: \ini_get('xdebug.mode'); $this->data = [ 'token' => $response->headers->get('X-Debug-Token'), 'symfony_version' => Kernel::VERSION, 'symfony_minor_version' => \sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), 'symfony_lts' => 4 === Kernel::MINOR_VERSION, 'symfony_state' => $this->determineSymfonyState(), 'symfony_eom' => $eom->format('F Y'), 'symfony_eol' => $eol->format('F Y'), 'env' => isset($this->kernel) ? $this->kernel->getEnvironment() : 'n/a', 'debug' => isset($this->kernel) ? $this->kernel->isDebug() : 'n/a', 'php_version' => \PHP_VERSION, 'php_architecture' => \PHP_INT_SIZE * 8, 'php_intl_locale' => class_exists(\Locale::class, false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a', 'php_timezone' => date_default_timezone_get(), 'xdebug_enabled' => \extension_loaded('xdebug'), 'xdebug_status' => \extension_loaded('xdebug') ? ($xdebugMode && 'off' !== $xdebugMode ? 'Enabled ('.$xdebugMode.')' : 'Not enabled') : 'Not installed', 'apcu_enabled' => \extension_loaded('apcu') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL), 'apcu_status' => \extension_loaded('apcu') ? (filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed', 'zend_opcache_enabled' => \extension_loaded('Zend OPcache') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL), 'zend_opcache_status' => \extension_loaded('Zend OPcache') ? (filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) ? 'Enabled' : 'Not enabled') : 'Not installed', 'bundles' => [], 'sapi_name' => \PHP_SAPI, 'runner_class' => $this->determineRunnerClass(), ]; if (isset($this->kernel)) { foreach ($this->kernel->getBundles() as $name => $bundle) { $this->data['bundles'][$name] = new ClassStub($bundle::class); } } if (preg_match('~^(\d+(?:\.\d+)*)(.+)?$~', $this->data['php_version'], $matches) && isset($matches[2])) { $this->data['php_version'] = $matches[1]; $this->data['php_version_extra'] = $matches[2]; } } public function lateCollect(): void { $this->data = $this->cloneVar($this->data); } /** * Gets the token. */ public function getToken(): ?string { return $this->data['token']; } /** * Gets the Symfony version. */ public function getSymfonyVersion(): string { return $this->data['symfony_version']; } /** * Returns the state of the current Symfony release * as one of: unknown, dev, stable, eom, eol. */ public function getSymfonyState(): string { return $this->data['symfony_state']; } /** * Returns the minor Symfony version used (without patch numbers of extra * suffix like "RC", "beta", etc.). */ public function getSymfonyMinorVersion(): string { return $this->data['symfony_minor_version']; } public function isSymfonyLts(): bool { return $this->data['symfony_lts']; } /** * Returns the human readable date when this Symfony version ends its * maintenance period. */ public function getSymfonyEom(): string { return $this->data['symfony_eom']; } /** * Returns the human readable date when this Symfony version reaches its * "end of life" and won't receive bugs or security fixes. */ public function getSymfonyEol(): string { return $this->data['symfony_eol']; } /** * Gets the PHP version. */ public function getPhpVersion(): string { return $this->data['php_version']; } /** * Gets the PHP version extra part. */ public function getPhpVersionExtra(): ?string { return $this->data['php_version_extra'] ?? null; } public function getPhpArchitecture(): int { return $this->data['php_architecture']; } public function getPhpIntlLocale(): string { return $this->data['php_intl_locale']; } public function getPhpTimezone(): string { return $this->data['php_timezone']; } /** * Gets the environment. */ public function getEnv(): string { return $this->data['env']; } /** * Returns true if the debug is enabled. * * @return bool|string true if debug is enabled, false otherwise or a string if no kernel was set */ public function isDebug(): bool|string { return $this->data['debug']; } /** * Returns true if the Xdebug is enabled. */ public function hasXdebug(): bool { return $this->data['xdebug_enabled']; } public function getXdebugStatus(): string { return $this->data['xdebug_status']; } /** * Returns true if the function xdebug_info is available. */ public function hasXdebugInfo(): bool { return \function_exists('xdebug_info'); } /** * Returns true if APCu is enabled. */ public function hasApcu(): bool { return $this->data['apcu_enabled']; } public function getApcuStatus(): string { return $this->data['apcu_status']; } /** * Returns true if Zend OPcache is enabled. */ public function hasZendOpcache(): bool { return $this->data['zend_opcache_enabled']; } public function getZendOpcacheStatus(): string { return $this->data['zend_opcache_status']; } public function getBundles(): array|Data { return $this->data['bundles']; } /** * Gets the PHP SAPI name. */ public function getSapiName(): string { return $this->data['sapi_name']; } public function getRunnerClass(): ?string { return $this->data['runner_class']; } public function getName(): string { return 'config'; } private function determineSymfonyState(): string { $now = new \DateTimeImmutable(); $eom = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE)->modify('last day of this month'); $eol = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE)->modify('last day of this month'); if ($now > $eol) { $versionState = 'eol'; } elseif ($now > $eom) { $versionState = 'eom'; } elseif ('' !== Kernel::EXTRA_VERSION) { $versionState = 'dev'; } else { $versionState = 'stable'; } return $versionState; } private function determineRunnerClass(): ?string { $stack = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); for ($frame = end($stack); $frame; $frame = prev($stack)) { if (!$class = $frame['class'] ?? null) { continue; } if (is_a($class, RunnerInterface::class, true)) { return $class; } } return null; } } ================================================ FILE: DataCollector/DataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\VarDumper\Caster\CutStub; use Symfony\Component\VarDumper\Caster\ReflectionCaster; use Symfony\Component\VarDumper\Cloner\ClonerInterface; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\Stub; use Symfony\Component\VarDumper\Cloner\VarCloner; /** * DataCollector. * * Children of this class must store the collected data in the data property. * * @author Fabien Potencier * @author Bernhard Schussek */ abstract class DataCollector implements DataCollectorInterface { protected array|Data $data = []; private ClonerInterface $cloner; /** * Converts the variable into a serializable Data instance. * * This array can be displayed in the template using * the VarDumper component. */ protected function cloneVar(mixed $var): Data { if ($var instanceof Data) { return $var; } if (!isset($this->cloner)) { $this->cloner = new VarCloner(); $this->cloner->setMaxItems(-1); $this->cloner->addCasters($this->getCasters()); } return $this->cloner->cloneVar($var); } /** * @return callable[] The casters to add to the cloner */ protected function getCasters(): array { return [ '*' => static function ($v, array $a, Stub $s, $isNested) { if (!$v instanceof Stub) { $b = $a; foreach ($a as $k => $v) { if (!\is_object($v) || $v instanceof \DateTimeInterface || $v instanceof Stub) { continue; } try { $a[$k] = $s = new CutStub($v); if ($b[$k] === $s) { // we've hit a non-typed reference $a[$k] = $v; } } catch (\TypeError $e) { // we've hit a typed reference } } } return $a; }, ] + ReflectionCaster::UNSET_CLOSURE_FILE_INFO; } public function __serialize(): array { return ['data' => $this->data]; } public function __unserialize(array $data): void { $this->data = $data['data'] ?? $data["\0*\0data"]; } public function reset(): void { $this->data = []; } } ================================================ FILE: DataCollector/DataCollectorInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Contracts\Service\ResetInterface; /** * DataCollectorInterface. * * @author Fabien Potencier */ interface DataCollectorInterface extends ResetInterface { /** * Collects data for the given Request and Response. */ public function collect(Request $request, Response $response, ?\Throwable $exception = null): void; /** * Returns the name of the collector. */ public function getName(): string; } ================================================ FILE: DataCollector/DumpDataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider; use Symfony\Component\VarDumper\Dumper\DataDumperInterface; use Symfony\Component\VarDumper\Dumper\HtmlDumper; use Symfony\Component\VarDumper\Server\Connection; /** * @author Nicolas Grekas * * @final */ class DumpDataCollector extends DataCollector implements DataDumperInterface { private string|FileLinkFormatter|false $fileLinkFormat; private int $dataCount = 0; private bool $isCollected = true; private int $clonesCount = 0; private int $clonesIndex = 0; private array $rootRefs; private string $charset; private mixed $sourceContextProvider; private bool $webMode; public function __construct( private ?Stopwatch $stopwatch = null, string|FileLinkFormatter|null $fileLinkFormat = null, ?string $charset = null, private ?RequestStack $requestStack = null, private DataDumperInterface|Connection|null $dumper = null, ?bool $webMode = null, ) { $fileLinkFormat = $fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); $this->fileLinkFormat = $fileLinkFormat instanceof FileLinkFormatter && false === $fileLinkFormat->format('', 0) ? false : $fileLinkFormat; $this->charset = $charset ?: \ini_get('php.output_encoding') ?: \ini_get('default_charset') ?: 'UTF-8'; $this->webMode = $webMode ?? !\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true); // All clones share these properties by reference: $this->rootRefs = [ &$this->data, &$this->dataCount, &$this->isCollected, &$this->clonesCount, ]; $this->sourceContextProvider = $dumper instanceof Connection && isset($dumper->getContextProviders()['source']) ? $dumper->getContextProviders()['source'] : new SourceContextProvider($this->charset); } public function __clone() { $this->clonesIndex = ++$this->clonesCount; } public function dump(Data $data): ?string { $this->stopwatch?->start('dump'); ['name' => $name, 'file' => $file, 'line' => $line, 'file_excerpt' => $fileExcerpt] = $this->sourceContextProvider->getContext(); if (!$this->dumper || $this->dumper instanceof Connection && !$this->dumper->write($data)) { $this->isCollected = false; } $context = $data->getContext(); $label = $context['label'] ?? ''; unset($context['label']); $data = $data->withContext($context); if ($this->dumper && !$this->dumper instanceof Connection) { $this->doDump($this->dumper, $data, $name, $file, $line, $label); } if (!$this->dataCount) { $this->data = []; } $this->data[] = compact('data', 'name', 'file', 'line', 'fileExcerpt', 'label'); ++$this->dataCount; $this->stopwatch?->stop('dump'); return null; } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { if (!$this->dataCount) { $this->data = []; } // Sub-requests and programmatic calls stay in the collected profile. if ($this->dumper || ($this->requestStack && $this->requestStack->getMainRequest() !== $request) || $request->isXmlHttpRequest() || $request->headers->has('Origin')) { return; } // In all other conditions that remove the web debug toolbar, dumps are written on the output. if (!$this->requestStack || !$response->headers->has('X-Debug-Token') || $response->isRedirection() || ($response->headers->has('Content-Type') && !str_contains($response->headers->get('Content-Type') ?? '', 'html')) || 'html' !== $request->getRequestFormat() || false === strripos($response->getContent(), '') ) { if ($response->headers->has('Content-Type') && str_contains($response->headers->get('Content-Type') ?? '', 'html')) { $dumper = new HtmlDumper('php://output', $this->charset); } else { $dumper = new CliDumper('php://output', $this->charset); } $dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); foreach ($this->data as $dump) { $this->doDump($dumper, $dump['data'], $dump['name'], $dump['file'], $dump['line'], $dump['label'] ?? ''); } } } public function reset(): void { $this->stopwatch?->reset(); parent::reset(); $this->dataCount = 0; $this->isCollected = true; $this->clonesCount = 0; $this->clonesIndex = 0; } public function __serialize(): array { if (!$this->dataCount) { $this->data = []; } if ($this->clonesCount !== $this->clonesIndex) { return []; } $this->data[] = $this->fileLinkFormat; $this->data[] = $this->charset; $this->dataCount = 0; $this->isCollected = true; return ['data' => $this->data]; } public function __unserialize(array $data): void { $this->data = array_pop($data) ?? []; $charset = array_pop($this->data); $fileLinkFormat = array_pop($this->data); $this->dataCount = \count($this->data); foreach ($this->data as $dump) { if (!\is_string($dump['name']) || !\is_string($dump['file']) || !\is_int($dump['line'])) { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } } self::__construct($this->stopwatch ?? null, \is_string($fileLinkFormat) || $fileLinkFormat instanceof FileLinkFormatter ? $fileLinkFormat : null, \is_string($charset) ? $charset : null); } public function getDumpsCount(): int { return $this->dataCount; } public function getDumps(string $format, int $maxDepthLimit = -1, int $maxItemsPerDepth = -1): array { $data = fopen('php://memory', 'r+'); if ('html' === $format) { $dumper = new HtmlDumper($data, $this->charset); $dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); } else { throw new \InvalidArgumentException(\sprintf('Invalid dump format: "%s".', $format)); } $dumps = []; if (!$this->dataCount) { return $this->data = []; } foreach ($this->data as $dump) { $dumper->dump($dump['data']->withMaxDepth($maxDepthLimit)->withMaxItemsPerDepth($maxItemsPerDepth)); $dump['data'] = stream_get_contents($data, -1, 0); ftruncate($data, 0); rewind($data); $dumps[] = $dump; } return $dumps; } public function getName(): string { return 'dump'; } public function __destruct() { if (0 === $this->clonesCount-- && !$this->isCollected && $this->dataCount) { $this->clonesCount = 0; $this->isCollected = true; $h = headers_list(); $i = \count($h); array_unshift($h, 'Content-Type: '.\ini_get('default_mimetype')); while (0 !== stripos($h[$i], 'Content-Type:')) { --$i; } if ($this->webMode) { $dumper = new HtmlDumper('php://output', $this->charset); } else { $dumper = new CliDumper('php://output', $this->charset); } $dumper->setDisplayOptions(['fileLinkFormat' => $this->fileLinkFormat]); foreach ($this->data as $i => $dump) { $this->data[$i] = null; $this->doDump($dumper, $dump['data'], $dump['name'], $dump['file'], $dump['line'], $dump['label'] ?? ''); } $this->data = []; $this->dataCount = 0; } } private function doDump(DataDumperInterface $dumper, Data $data, string $name, string $file, int $line, string $label): void { if ($dumper instanceof CliDumper) { $contextDumper = function ($name, $file, $line, $fmt, $label) { $this->line = '' !== $label ? $this->style('meta', $label).' in ' : ''; if ($this instanceof HtmlDumper) { if ($file) { $s = $this->style('meta', '%s'); $f = strip_tags($this->style('', $file)); $name = strip_tags($this->style('', $name)); if ($fmt && $link = \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line)) { $name = \sprintf(''.$s.'', strip_tags($this->style('', $link)), $f, $name); } else { $name = \sprintf(''.$s.'', $f, $name); } } else { $name = $this->style('meta', $name); } $this->line .= $name.' on line '.$this->style('meta', $line).':'; } else { $this->line .= $this->style('meta', $name).' on line '.$this->style('meta', $line).':'; } $this->dumpLine(0); }; $contextDumper = $contextDumper->bindTo($dumper, $dumper); $contextDumper($name, $file, $line, $this->fileLinkFormat, $label); } else { $cloner = new VarCloner(); $dumper->dump($cloner->cloneVar(('' !== $label ? $label.' in ' : '').$name.' on line '.$line.':')); } $dumper->dump($data); } } ================================================ FILE: DataCollector/EventDataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Symfony\Contracts\Service\ResetInterface; /** * @author Fabien Potencier * * @see TraceableEventDispatcher * * @final */ class EventDataCollector extends DataCollector implements LateDataCollectorInterface { /** @var iterable */ private iterable $dispatchers; private ?Request $currentRequest = null; /** * @param iterable|EventDispatcherInterface|null $dispatchers */ public function __construct( iterable|EventDispatcherInterface|null $dispatchers = null, private ?RequestStack $requestStack = null, private string $defaultDispatcher = 'event_dispatcher', ) { if ($dispatchers instanceof EventDispatcherInterface) { $dispatchers = [$this->defaultDispatcher => $dispatchers]; } $this->dispatchers = $dispatchers ?? []; } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { $this->currentRequest = $this->requestStack && $this->requestStack->getMainRequest() !== $request ? $request : null; $this->data = []; } public function reset(): void { parent::reset(); foreach ($this->dispatchers as $dispatcher) { if ($dispatcher instanceof ResetInterface) { $dispatcher->reset(); } } } public function lateCollect(): void { foreach ($this->dispatchers as $name => $dispatcher) { if (!$dispatcher instanceof TraceableEventDispatcher) { continue; } $this->setCalledListeners($dispatcher->getCalledListeners($this->currentRequest), $name); $this->setNotCalledListeners($dispatcher->getNotCalledListeners($this->currentRequest), $name); $this->setOrphanedEvents($dispatcher->getOrphanedEvents($this->currentRequest), $name); } $this->data = $this->cloneVar($this->data); } public function getData(): array|Data { return $this->data; } /** * @see TraceableEventDispatcher */ public function setCalledListeners(array $listeners, ?string $dispatcher = null): void { $this->data[$dispatcher ?? $this->defaultDispatcher]['called_listeners'] = $listeners; } /** * @see TraceableEventDispatcher */ public function getCalledListeners(?string $dispatcher = null): array|Data { return $this->data[$dispatcher ?? $this->defaultDispatcher]['called_listeners'] ?? []; } /** * @see TraceableEventDispatcher */ public function setNotCalledListeners(array $listeners, ?string $dispatcher = null): void { $this->data[$dispatcher ?? $this->defaultDispatcher]['not_called_listeners'] = $listeners; } /** * @see TraceableEventDispatcher */ public function getNotCalledListeners(?string $dispatcher = null): array|Data { return $this->data[$dispatcher ?? $this->defaultDispatcher]['not_called_listeners'] ?? []; } /** * @param array $events An array of orphaned events * * @see TraceableEventDispatcher */ public function setOrphanedEvents(array $events, ?string $dispatcher = null): void { $this->data[$dispatcher ?? $this->defaultDispatcher]['orphaned_events'] = $events; } /** * @see TraceableEventDispatcher */ public function getOrphanedEvents(?string $dispatcher = null): array|Data { return $this->data[$dispatcher ?? $this->defaultDispatcher]['orphaned_events'] ?? []; } public function getName(): string { return 'events'; } } ================================================ FILE: DataCollector/ExceptionDataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** * @author Fabien Potencier * * @final */ class ExceptionDataCollector extends DataCollector { public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { if (null !== $exception) { $this->data = [ 'exception' => FlattenException::createWithDataRepresentation($exception), ]; } } public function hasException(): bool { return isset($this->data['exception']); } public function getException(): \Exception|FlattenException { return $this->data['exception']; } public function getMessage(): string { return $this->data['exception']->getMessage(); } public function getCode(): int|string { return $this->data['exception']->getCode(); } public function getStatusCode(): int { return $this->data['exception']->getStatusCode(); } public function getTrace(): array { return $this->data['exception']->getTrace(); } public function getName(): string { return 'exception'; } } ================================================ FILE: DataCollector/LateDataCollectorInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; /** * LateDataCollectorInterface. * * @author Fabien Potencier */ interface LateDataCollectorInterface { /** * Collects data as late as possible. */ public function lateCollect(): void; } ================================================ FILE: DataCollector/LoggerDataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; use Symfony\Component\VarDumper\Cloner\Data; /** * @author Fabien Potencier * * @final */ class LoggerDataCollector extends DataCollector implements LateDataCollectorInterface { private ?DebugLoggerInterface $logger; private ?Request $currentRequest = null; private ?array $processedLogs = null; public function __construct( ?object $logger = null, private ?string $containerPathPrefix = null, private ?RequestStack $requestStack = null, ) { $this->logger = DebugLoggerConfigurator::getDebugLogger($logger); } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { $this->currentRequest = $this->requestStack && $this->requestStack->getMainRequest() !== $request ? $request : null; } public function lateCollect(): void { if ($this->logger) { $containerDeprecationLogs = $this->getContainerDeprecationLogs(); $this->data = $this->computeErrorsCount($containerDeprecationLogs); // get compiler logs later (only when they are needed) to improve performance $this->data['compiler_logs'] = []; $this->data['compiler_logs_filepath'] = $this->containerPathPrefix.'Compiler.log'; $this->data['logs'] = $this->sanitizeLogs(array_merge($this->logger->getLogs($this->currentRequest), $containerDeprecationLogs)); $this->data = $this->cloneVar($this->data); } $this->currentRequest = null; } public function getLogs(): Data|array { return $this->data['logs'] ?? []; } public function getProcessedLogs(): array { if (null !== $this->processedLogs) { return $this->processedLogs; } $rawLogs = $this->getLogs(); if ([] === $rawLogs) { return $this->processedLogs = $rawLogs; } $logs = []; foreach ($this->getLogs()->getValue() as $rawLog) { $rawLogData = $rawLog->getValue(); if ($rawLogData['priority']->getValue() > 300) { $logType = 'error'; } elseif (isset($rawLogData['scream']) && false === $rawLogData['scream']->getValue()) { $logType = 'deprecation'; } elseif (isset($rawLogData['scream']) && true === $rawLogData['scream']->getValue()) { $logType = 'silenced'; } else { $logType = 'regular'; } $logs[] = [ 'type' => $logType, 'errorCount' => $rawLog['errorCount'] ?? 1, 'timestamp' => $rawLogData['timestamp_rfc3339']->getValue(), 'priority' => $rawLogData['priority']->getValue(), 'priorityName' => $rawLogData['priorityName']->getValue(), 'channel' => $rawLogData['channel']->getValue(), 'message' => $rawLogData['message'], 'context' => $rawLogData['context'], ]; } // sort logs from oldest to newest usort($logs, static fn ($logA, $logB) => $logA['timestamp'] <=> $logB['timestamp']); return $this->processedLogs = $logs; } public function getFilters(): array { $filters = [ 'channel' => [], 'priority' => [ 'Debug' => 100, 'Info' => 200, 'Notice' => 250, 'Warning' => 300, 'Error' => 400, 'Critical' => 500, 'Alert' => 550, 'Emergency' => 600, ], ]; $allChannels = []; foreach ($this->getProcessedLogs() as $log) { if ('' === trim($log['channel'] ?? '')) { continue; } $allChannels[] = $log['channel']; } $channels = array_unique($allChannels); sort($channels); $filters['channel'] = $channels; return $filters; } public function getPriorities(): Data|array { return $this->data['priorities'] ?? []; } public function countErrors(): int { return $this->data['error_count'] ?? 0; } public function countDeprecations(): int { return $this->data['deprecation_count'] ?? 0; } public function countWarnings(): int { return $this->data['warning_count'] ?? 0; } public function countScreams(): int { return $this->data['scream_count'] ?? 0; } public function getCompilerLogs(): Data { return $this->cloneVar($this->getContainerCompilerLogs($this->data['compiler_logs_filepath'] ?? null)); } public function getName(): string { return 'logger'; } private function getContainerDeprecationLogs(): array { if (null === $this->containerPathPrefix || !is_file($file = $this->containerPathPrefix.'Deprecations.log')) { return []; } if ('' === $logContent = trim(file_get_contents($file))) { return []; } $bootTime = filemtime($file); $logs = []; foreach (unserialize($logContent) as $log) { $log['context'] = ['exception' => new SilencedErrorContext($log['type'], $log['file'], $log['line'], $log['trace'], $log['count'])]; $log['timestamp'] = $bootTime; $log['timestamp_rfc3339'] = (new \DateTimeImmutable())->setTimestamp($bootTime)->format(\DateTimeInterface::RFC3339_EXTENDED); $log['priority'] = 100; $log['priorityName'] = 'DEBUG'; $log['channel'] = null; $log['scream'] = false; unset($log['type'], $log['file'], $log['line'], $log['trace'], $log['count']); $logs[] = $log; } return $logs; } private function getContainerCompilerLogs(?string $compilerLogsFilepath = null): array { if (!$compilerLogsFilepath || !is_file($compilerLogsFilepath)) { return []; } $logs = []; foreach (file($compilerLogsFilepath, \FILE_IGNORE_NEW_LINES) as $log) { $log = explode(': ', $log, 2); if (!isset($log[1]) || !preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)++$/', $log[0])) { $log = ['Unknown Compiler Pass', implode(': ', $log)]; } $logs[$log[0]][] = ['message' => $log[1]]; } return $logs; } private function sanitizeLogs(array $logs): array { $sanitizedLogs = []; $silencedLogs = []; foreach ($logs as $log) { if (!$this->isSilencedOrDeprecationErrorLog($log)) { $sanitizedLogs[] = $log; continue; } $message = '_'.$log['message']; $exception = $log['context']['exception']; if ($exception instanceof SilencedErrorContext) { if (isset($silencedLogs[$id = spl_object_id($exception)])) { continue; } $silencedLogs[$id] = true; if (!isset($sanitizedLogs[$message])) { $sanitizedLogs[$message] = $log + [ 'errorCount' => 0, 'scream' => true, ]; } $sanitizedLogs[$message]['errorCount'] += $exception->count; continue; } $errorId = hash('xxh128', "{$exception->getSeverity()}/{$exception->getLine()}/{$exception->getFile()}\0{$message}", true); if (isset($sanitizedLogs[$errorId])) { ++$sanitizedLogs[$errorId]['errorCount']; } else { $log += [ 'errorCount' => 1, 'scream' => false, ]; $sanitizedLogs[$errorId] = $log; } } return array_values($sanitizedLogs); } private function isSilencedOrDeprecationErrorLog(array $log): bool { if (!isset($log['context']['exception'])) { return false; } $exception = $log['context']['exception']; if ($exception instanceof SilencedErrorContext) { return true; } if ($exception instanceof \ErrorException && \in_array($exception->getSeverity(), [\E_DEPRECATED, \E_USER_DEPRECATED], true)) { return true; } return false; } private function computeErrorsCount(array $containerDeprecationLogs): array { $silencedLogs = []; $count = [ 'error_count' => $this->logger->countErrors($this->currentRequest), 'deprecation_count' => 0, 'warning_count' => 0, 'scream_count' => 0, 'priorities' => [], ]; foreach ($this->logger->getLogs($this->currentRequest) as $log) { if (isset($count['priorities'][$log['priority']])) { ++$count['priorities'][$log['priority']]['count']; } else { $count['priorities'][$log['priority']] = [ 'count' => 1, 'name' => $log['priorityName'], ]; } if ('WARNING' === $log['priorityName']) { ++$count['warning_count']; } if ($this->isSilencedOrDeprecationErrorLog($log)) { $exception = $log['context']['exception']; if ($exception instanceof SilencedErrorContext) { if (isset($silencedLogs[$id = spl_object_id($exception)])) { continue; } $silencedLogs[$id] = true; $count['scream_count'] += $exception->count; } else { ++$count['deprecation_count']; } } } foreach ($containerDeprecationLogs as $deprecationLog) { $count['deprecation_count'] += $deprecationLog['context']['exception']->count; } ksort($count['priorities']); return $count; } } ================================================ FILE: DataCollector/MemoryDataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** * @author Fabien Potencier * * @final */ class MemoryDataCollector extends DataCollector implements LateDataCollectorInterface { public function __construct() { $this->reset(); } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { $this->updateMemoryUsage(); } public function reset(): void { $this->data = [ 'memory' => 0, 'memory_limit' => $this->convertToBytes(\ini_get('memory_limit')), ]; } public function lateCollect(): void { $this->updateMemoryUsage(); } public function getMemory(): int { return $this->data['memory']; } public function getMemoryLimit(): int|float { return $this->data['memory_limit']; } public function updateMemoryUsage(): void { $this->data['memory'] = memory_get_peak_usage(true); } public function getName(): string { return 'memory'; } private function convertToBytes(string $memoryLimit): int|float { if ('-1' === $memoryLimit) { return -1; } $memoryLimit = strtolower($memoryLimit); $max = strtolower(ltrim($memoryLimit, '+')); if (str_starts_with($max, '0x')) { $max = \intval($max, 16); } elseif (str_starts_with($max, '0')) { $max = \intval($max, 8); } else { $max = (int) $max; } switch (substr($memoryLimit, -1)) { case 't': $max *= 1024; // no break case 'g': $max *= 1024; // no break case 'm': $max *= 1024; // no break case 'k': $max *= 1024; } return $max; } } ================================================ FILE: DataCollector/RequestDataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\SessionBagInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Process\Process; use Symfony\Component\VarDumper\Cloner\Data; /** * @author Fabien Potencier * * @final */ class RequestDataCollector extends DataCollector implements EventSubscriberInterface, LateDataCollectorInterface { /** * @var \SplObjectStorage */ private \SplObjectStorage $controllers; private array $sessionUsages = []; public function __construct( private ?RequestStack $requestStack = null, ) { $this->controllers = new \SplObjectStorage(); } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { // attributes are serialized and as they can be anything, they need to be converted to strings. $attributes = []; $route = ''; foreach ($request->attributes->all() as $key => $value) { if ('_route' === $key) { $route = \is_object($value) ? $value->getPath() : $value; $attributes[$key] = $route; } else { $attributes[$key] = $value; } } $content = $request->getContent(); $sessionMetadata = []; $sessionAttributes = []; $flashes = []; if (!$request->attributes->getBoolean('_stateless') && $request->hasSession()) { $session = $request->getSession(); if ($session->isStarted()) { $sessionMetadata['Created'] = date(\DATE_RFC822, $session->getMetadataBag()->getCreated()); $sessionMetadata['Last used'] = date(\DATE_RFC822, $session->getMetadataBag()->getLastUsed()); $sessionMetadata['Lifetime'] = $session->getMetadataBag()->getLifetime(); $sessionAttributes = $session->all(); $flashes = $session->getFlashBag()->peekAll(); } } $statusCode = $response->getStatusCode(); $responseCookies = []; foreach ($response->headers->getCookies() as $cookie) { $responseCookies[$cookie->getName()] = $cookie; } $dotenvVars = []; foreach (explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? '') as $name) { if ('' !== $name && isset($_ENV[$name])) { $dotenvVars[$name] = $_ENV[$name]; } } $this->data = [ 'method' => $request->getMethod(), 'format' => $request->getRequestFormat(), 'content_type' => $response->headers->get('Content-Type', 'text/html'), 'status_text' => Response::$statusTexts[$statusCode] ?? '', 'status_code' => $statusCode, 'request_query' => $request->query->all(), 'request_request' => $request->request->all(), 'request_files' => $request->files->all(), 'request_headers' => $request->headers->all(), 'request_server' => $request->server->all(), 'request_cookies' => $request->cookies->all(), 'request_attributes' => $attributes, 'route' => $route, 'response_headers' => $response->headers->all(), 'response_cookies' => $responseCookies, 'session_metadata' => $sessionMetadata, 'session_attributes' => $sessionAttributes, 'session_usages' => array_values($this->sessionUsages), 'stateless_check' => $this->requestStack?->getMainRequest()?->attributes->get('_stateless') ?? false, 'flashes' => $flashes, 'path_info' => $request->getPathInfo(), 'controller' => 'n/a', 'locale' => $request->getLocale(), 'dotenv_vars' => $dotenvVars, ]; if (isset($this->data['request_headers']['php-auth-pw'])) { $this->data['request_headers']['php-auth-pw'] = '******'; } if (isset($this->data['request_server']['PHP_AUTH_PW'])) { $this->data['request_server']['PHP_AUTH_PW'] = '******'; } if (isset($this->data['request_request']['_password'])) { $encodedPassword = rawurlencode($this->data['request_request']['_password']); $content = str_replace('_password='.$encodedPassword, '_password=******', $content); $this->data['request_request']['_password'] = '******'; } $this->data['content'] = $content; $this->data['curlCommand'] = $this->computeCurlCommand($request, $content); foreach ($this->data as $key => $value) { if (!\is_array($value)) { continue; } if ('request_headers' === $key || 'response_headers' === $key) { $this->data[$key] = array_map(static fn ($v) => isset($v[0]) && !isset($v[1]) ? $v[0] : $v, $value); } } if (isset($this->controllers[$request])) { $this->data['controller'] = $this->parseController($this->controllers[$request]); unset($this->controllers[$request]); } if ($request->attributes->has('_redirected') && $redirectCookie = $request->cookies->get('sf_redirect')) { $this->data['redirect'] = json_decode($redirectCookie, true); $response->headers->clearCookie('sf_redirect'); } if ($response->isRedirect()) { $response->headers->setCookie(new Cookie( 'sf_redirect', json_encode([ 'token' => $response->headers->get('x-debug-token'), 'route' => $request->attributes->get('_route', 'n/a'), 'method' => $request->getMethod(), 'controller' => $this->parseController($request->attributes->get('_controller')), 'status_code' => $statusCode, 'status_text' => Response::$statusTexts[$statusCode], ]), 0, '/', null, $request->isSecure(), true, false, 'lax' )); } $this->data['identifier'] = $this->data['route'] ?: (\is_array($this->data['controller']) ? $this->data['controller']['class'].'::'.$this->data['controller']['method'].'()' : $this->data['controller']); if ($response->headers->has('x-previous-debug-token')) { $this->data['forward_token'] = $response->headers->get('x-previous-debug-token'); } } public function lateCollect(): void { $this->data = $this->cloneVar($this->data); } public function reset(): void { parent::reset(); $this->controllers = new \SplObjectStorage(); $this->sessionUsages = []; } public function getMethod(): string { return $this->data['method']; } public function getPathInfo(): string { return $this->data['path_info']; } public function getRequestRequest(): ParameterBag { return new ParameterBag($this->data['request_request']->getValue()); } public function getRequestQuery(): ParameterBag { return new ParameterBag($this->data['request_query']->getValue()); } public function getRequestFiles(): ParameterBag { return new ParameterBag($this->data['request_files']->getValue()); } public function getRequestHeaders(): ParameterBag { return new ParameterBag($this->data['request_headers']->getValue()); } public function getRequestServer(bool $raw = false): ParameterBag { return new ParameterBag($this->data['request_server']->getValue($raw)); } public function getRequestCookies(bool $raw = false): ParameterBag { return new ParameterBag($this->data['request_cookies']->getValue($raw)); } public function getRequestAttributes(): ParameterBag { return new ParameterBag($this->data['request_attributes']->getValue()); } public function getResponseHeaders(): ParameterBag { return new ParameterBag($this->data['response_headers']->getValue()); } public function getResponseCookies(): ParameterBag { return new ParameterBag($this->data['response_cookies']->getValue()); } public function getSessionMetadata(): array { return $this->data['session_metadata']->getValue(); } public function getSessionAttributes(): array { return $this->data['session_attributes']->getValue(); } public function getStatelessCheck(): bool { return $this->data['stateless_check']; } public function getSessionUsages(): Data|array { return $this->data['session_usages']; } public function getFlashes(): array { return $this->data['flashes']->getValue(); } /** * @return string|resource */ public function getContent() { return $this->data['content']; } public function isJsonRequest(): bool { return 1 === preg_match('{^application/(?:\w+\++)*json$}i', $this->data['request_headers']['content-type']); } public function getPrettyJson(): ?string { $decoded = json_decode($this->getContent()); return \JSON_ERROR_NONE === json_last_error() ? json_encode($decoded, \JSON_PRETTY_PRINT) : null; } public function getContentType(): string { return $this->data['content_type']; } public function getStatusText(): string { return $this->data['status_text']; } public function getStatusCode(): int { return $this->data['status_code']; } public function getFormat(): string { return $this->data['format']; } public function getLocale(): string { return $this->data['locale']; } public function getDotenvVars(): ParameterBag { return new ParameterBag($this->data['dotenv_vars']->getValue()); } /** * Gets the route name. * * The _route request attributes is automatically set by the Router Matcher. */ public function getRoute(): string { return $this->data['route']; } public function getIdentifier(): string { return $this->data['identifier']; } /** * Gets the route parameters. * * The _route_params request attributes is automatically set by the RouterListener. */ public function getRouteParams(): array { return isset($this->data['request_attributes']['_route_params']) ? $this->data['request_attributes']['_route_params']->getValue() : []; } /** * Gets the parsed controller. * * @return array|string|Data The controller as a string or array of data * with keys 'class', 'method', 'file' and 'line' */ public function getController(): array|string|Data { return $this->data['controller']; } /** * Gets the previous request attributes. * * @return array|Data|false A legacy array of data from the previous redirection response * or false otherwise */ public function getRedirect(): array|Data|false { return $this->data['redirect'] ?? false; } public function getForwardToken(): ?string { return $this->data['forward_token'] ?? null; } public function onKernelController(ControllerEvent $event): void { $this->controllers[$event->getRequest()] = $event->getController(); } public function onKernelResponse(ResponseEvent $event): void { if (!$event->isMainRequest()) { return; } if ($event->getRequest()->cookies->has('sf_redirect')) { $event->getRequest()->attributes->set('_redirected', true); } } public static function getSubscribedEvents(): array { return [ KernelEvents::CONTROLLER => 'onKernelController', KernelEvents::RESPONSE => 'onKernelResponse', ]; } public function getName(): string { return 'request'; } public function collectSessionUsage(): void { $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); $traceEndIndex = \count($trace) - 1; for ($i = $traceEndIndex; $i > 0; --$i) { if (null !== ($class = $trace[$i]['class'] ?? null) && (is_subclass_of($class, SessionInterface::class) || is_subclass_of($class, SessionBagInterface::class))) { $traceEndIndex = $i; break; } } if ((\count($trace) - 1) === $traceEndIndex) { return; } // Remove part of the backtrace that belongs to session only array_splice($trace, 0, $traceEndIndex); // Merge identical backtraces generated by internal call reports $name = \sprintf('%s:%s', $trace[1]['class'] ?? $trace[0]['file'], $trace[0]['line']); if (!\array_key_exists($name, $this->sessionUsages)) { $this->sessionUsages[$name] = [ 'name' => $name, 'file' => $trace[0]['file'], 'line' => $trace[0]['line'], 'trace' => $trace, ]; } } /** * @return array|string An array of controller data or a simple string */ private function parseController(array|object|string|null $controller): array|string { if (\is_string($controller) && str_contains($controller, '::')) { $controller = explode('::', $controller); } if (\is_array($controller)) { try { $r = new \ReflectionMethod($controller[0], $controller[1]); return [ 'class' => \is_object($controller[0]) ? get_debug_type($controller[0]) : $controller[0], 'method' => $controller[1], 'file' => $r->getFileName(), 'line' => $r->getStartLine(), ]; } catch (\ReflectionException) { if (\is_callable($controller)) { // using __call or __callStatic return [ 'class' => \is_object($controller[0]) ? get_debug_type($controller[0]) : $controller[0], 'method' => $controller[1], 'file' => 'n/a', 'line' => 'n/a', ]; } } } if ($controller instanceof \Closure) { $r = new \ReflectionFunction($controller); $controller = [ 'class' => $r->getName(), 'method' => null, 'file' => $r->getFileName(), 'line' => $r->getStartLine(), ]; if ($r->isAnonymous()) { return $controller; } $controller['method'] = $r->name; if ($class = $r->getClosureCalledClass()) { $controller['class'] = $class->name; } else { return $r->name; } return $controller; } if (\is_object($controller)) { $r = new \ReflectionClass($controller); return [ 'class' => $r->getName(), 'method' => null, 'file' => $r->getFileName(), 'line' => $r->getStartLine(), ]; } return \is_string($controller) ? $controller : 'n/a'; } private function computeCurlCommand(Request $request, ?string $content): string { $command = ['curl', '--compressed']; $method = $request->getMethod(); if (Request::METHOD_HEAD === $method) { $command[] = '--head'; } elseif (Request::METHOD_GET !== $method) { $command[] = \sprintf('--request %s', $method); } $command[] = \sprintf('--url %s', escapeshellarg($request->getUri())); foreach ($request->headers->all() as $name => $values) { if (\in_array(strtolower($name), ['host', 'cookie'], true)) { continue; } $command[] = '--header '.escapeshellarg(ucwords($name, '-').': '.implode(', ', $values)); } if ($request->cookies->all()) { $cookies = []; foreach ($request->cookies->all() as $name => $value) { $cookies[] = urlencode($name).'='.urlencode($value); } $command[] = '--cookie '.escapeshellarg(implode('; ', $cookies)); } if ($content && \in_array($method, [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE], true)) { $command[] = '--data-raw '.$this->escapePayload($content); } return implode(" \\\n ", $command); } public function getCurlCommand(): string { return $this->data['curlCommand'] ?? ''; } private function escapePayload(string $payload): string { static $useProcess; if ($useProcess ??= \function_exists('proc_open') && class_exists(Process::class)) { return substr((new Process(['', $payload]))->getCommandLine(), 3); } if ('\\' === \DIRECTORY_SEPARATOR) { return '"'.str_replace('"', '""', $payload).'"'; } return "'".str_replace("'", "'\\''", $payload)."'"; } } ================================================ FILE: DataCollector/RouterDataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ControllerEvent; /** * @author Fabien Potencier */ class RouterDataCollector extends DataCollector { /** * @var \SplObjectStorage */ protected \SplObjectStorage $controllers; public function __construct() { $this->reset(); } /** * @final */ public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { if ($response instanceof RedirectResponse) { $this->data['redirect'] = true; $this->data['url'] = $response->getTargetUrl(); if ($this->controllers->offsetExists($request)) { $this->data['route'] = $this->guessRoute($request, $this->controllers[$request]); } } unset($this->controllers[$request]); } public function reset(): void { $this->controllers = new \SplObjectStorage(); $this->data = [ 'redirect' => false, 'url' => null, 'route' => null, ]; } protected function guessRoute(Request $request, string|object|array $controller): string { return 'n/a'; } /** * Remembers the controller associated to each request. */ public function onKernelController(ControllerEvent $event): void { $this->controllers[$event->getRequest()] = $event->getController(); } /** * @return bool Whether this request will result in a redirect */ public function getRedirect(): bool { return $this->data['redirect']; } public function getTargetUrl(): ?string { return $this->data['url']; } public function getTargetRoute(): ?string { return $this->data['route']; } public function getName(): string { return 'router'; } } ================================================ FILE: DataCollector/TimeDataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\Stopwatch\StopwatchEvent; /** * @author Fabien Potencier * * @final */ class TimeDataCollector extends DataCollector implements LateDataCollectorInterface { public function __construct( private readonly ?KernelInterface $kernel = null, private readonly ?Stopwatch $stopwatch = null, ) { $this->data = ['events' => [], 'stopwatch_installed' => false, 'start_time' => 0]; } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { if (null !== $this->kernel) { $startTime = $this->kernel->getStartTime(); } else { $startTime = $request->server->get('REQUEST_TIME_FLOAT'); } $this->data = [ 'token' => $request->attributes->get('_stopwatch_token'), 'start_time' => $startTime * 1000, 'events' => [], 'stopwatch_installed' => class_exists(Stopwatch::class, false), ]; } public function reset(): void { $this->data = ['events' => [], 'stopwatch_installed' => false, 'start_time' => 0]; $this->stopwatch?->reset(); } public function lateCollect(): void { if (null !== $this->stopwatch && isset($this->data['token'])) { $this->setEvents($this->stopwatch->getSectionEvents($this->data['token'])); } unset($this->data['token']); } /** * @param StopwatchEvent[] $events The request events */ public function setEvents(array $events): void { foreach ($events as $event) { $event->ensureStopped(); } $this->data['events'] = $events; } /** * @return StopwatchEvent[] */ public function getEvents(): array { return $this->data['events']; } /** * Gets the request elapsed time. */ public function getDuration(): float { if (!isset($this->data['events']['__section__'])) { return 0; } $lastEvent = $this->data['events']['__section__']; return $lastEvent->getOrigin() + $lastEvent->getDuration() - $this->getStartTime(); } /** * Gets the initialization time. * * This is the time spent until the beginning of the request handling. */ public function getInitTime(): float { if (!isset($this->data['events']['__section__'])) { return 0; } return $this->data['events']['__section__']->getOrigin() - $this->getStartTime(); } public function getStartTime(): float { return $this->data['start_time']; } public function isStopwatchInstalled(): bool { return $this->data['stopwatch_installed']; } public function getName(): string { return 'time'; } } ================================================ FILE: Debug/ErrorHandlerConfigurator.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Debug; use Psr\Log\LoggerInterface; use Symfony\Component\ErrorHandler\ErrorHandler; /** * Configures the error handler. * * @final * * @internal */ class ErrorHandlerConfigurator { private array|int|null $levels; private ?int $throwAt; /** * @param array|int|null $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants * @param int|null $throwAt Thrown errors in a bit field of E_* constants, or null to keep the current value * @param bool $scream Enables/disables screaming mode, where even silenced errors are logged * @param bool $scope Enables/disables scoping mode */ public function __construct( private ?LoggerInterface $logger = null, array|int|null $levels = \E_ALL, ?int $throwAt = \E_ALL, private bool $scream = true, private bool $scope = true, private ?LoggerInterface $deprecationLogger = null, ) { $this->levels = $levels ?? \E_ALL; $this->throwAt = \is_int($throwAt) ? $throwAt : (null === $throwAt ? null : ($throwAt ? \E_ALL : null)); } /** * Configures the error handler. */ public function configure(ErrorHandler $handler): void { if ($this->logger || $this->deprecationLogger) { $this->setDefaultLoggers($handler); if (\is_array($this->levels)) { $levels = 0; foreach ($this->levels as $type => $log) { $levels |= $type; } } else { $levels = $this->levels; } if ($this->scream) { $handler->screamAt($levels); } if ($this->scope) { $handler->scopeAt($levels & ~\E_USER_DEPRECATED & ~\E_DEPRECATED); } else { $handler->scopeAt(0, true); } $this->logger = $this->deprecationLogger = $this->levels = null; } if (null !== $this->throwAt) { $handler->throwAt($this->throwAt, true); } } private function setDefaultLoggers(ErrorHandler $handler): void { if (\is_array($this->levels)) { $levelsDeprecatedOnly = []; $levelsWithoutDeprecated = []; foreach ($this->levels as $type => $log) { if (\E_DEPRECATED == $type || \E_USER_DEPRECATED == $type) { $levelsDeprecatedOnly[$type] = $log; } else { $levelsWithoutDeprecated[$type] = $log; } } } else { $levelsDeprecatedOnly = $this->levels & (\E_DEPRECATED | \E_USER_DEPRECATED); $levelsWithoutDeprecated = $this->levels & ~\E_DEPRECATED & ~\E_USER_DEPRECATED; } $defaultLoggerLevels = $this->levels; if ($this->deprecationLogger && $levelsDeprecatedOnly) { $handler->setDefaultLogger($this->deprecationLogger, $levelsDeprecatedOnly); $defaultLoggerLevels = $levelsWithoutDeprecated; } if ($this->logger && $defaultLoggerLevels) { $handler->setDefaultLogger($this->logger, $defaultLoggerLevels); } } } ================================================ FILE: Debug/TraceableEventDispatcher.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Debug; use Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher as BaseTraceableEventDispatcher; use Symfony\Component\HttpKernel\KernelEvents; /** * Collects some data about event listeners. * * This event dispatcher delegates the dispatching to another one. * * @author Fabien Potencier */ class TraceableEventDispatcher extends BaseTraceableEventDispatcher { protected function beforeDispatch(string $eventName, object $event): void { if ($this->disabled?->__invoke()) { return; } switch ($eventName) { case KernelEvents::REQUEST: $event->getRequest()->attributes->set('_stopwatch_token', bin2hex(random_bytes(3))); $this->stopwatch->openSection(); break; case KernelEvents::VIEW: case KernelEvents::RESPONSE: // stop only if a controller has been executed if ($this->stopwatch->isStarted('controller')) { $this->stopwatch->stop('controller'); } break; case KernelEvents::TERMINATE: $sectionId = $event->getRequest()->attributes->get('_stopwatch_token'); if (null === $sectionId) { break; } // There is a very special case when using built-in AppCache class as kernel wrapper, in the case // of an ESI request leading to a `stale` response [B] inside a `fresh` cached response [A]. // In this case, `$token` contains the [B] debug token, but the open `stopwatch` section ID // is equal to the [A] debug token. Trying to reopen section with the [B] token throws an exception // which must be caught. try { $this->stopwatch->openSection($sectionId); } catch (\LogicException) { } break; } } protected function afterDispatch(string $eventName, object $event): void { if ($this->disabled?->__invoke()) { return; } switch ($eventName) { case KernelEvents::CONTROLLER_ARGUMENTS: $this->stopwatch->start('controller', 'section'); break; case KernelEvents::RESPONSE: $sectionId = $event->getRequest()->attributes->get('_stopwatch_token'); if (null === $sectionId) { break; } try { $this->stopwatch->stopSection($sectionId); } catch (\LogicException) { // The stop watch service might have been reset in the meantime } break; case KernelEvents::TERMINATE: // In the special case described in the `preDispatch` method above, the `$token` section // does not exist, then closing it throws an exception which must be caught. $sectionId = $event->getRequest()->attributes->get('_stopwatch_token'); if (null === $sectionId) { break; } try { $this->stopwatch->stopSection($sectionId); } catch (\LogicException) { } break; } } } ================================================ FILE: Debug/VirtualRequestStack.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Debug; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; /** * A stack able to deal with virtual requests. * * @internal * * @author Jules Pietri */ final class VirtualRequestStack extends RequestStack { public function __construct( private readonly RequestStack $decorated, ) { } public function push(Request $request): void { if ($request->attributes->has('_virtual_type')) { if ($this->decorated->getCurrentRequest()) { throw new \LogicException('Cannot mix virtual and HTTP requests.'); } parent::push($request); return; } $this->decorated->push($request); } public function pop(): ?Request { return $this->decorated->pop() ?? parent::pop(); } public function getCurrentRequest(): ?Request { return $this->decorated->getCurrentRequest() ?? parent::getCurrentRequest(); } public function getMainRequest(): ?Request { return $this->decorated->getMainRequest() ?? parent::getMainRequest(); } public function getParentRequest(): ?Request { return $this->decorated->getParentRequest() ?? parent::getParentRequest(); } } ================================================ FILE: DependencyInjection/ConfigurableExtension.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; /** * This extension sub-class provides first-class integration with the * Config/Definition Component. * * You can use this as base class if * * a) you use the Config/Definition component for configuration, * b) your configuration class is named "Configuration", and * c) the configuration class resides in the DependencyInjection sub-folder. * * @author Johannes M. Schmitt */ abstract class ConfigurableExtension extends Extension { final public function load(array $configs, ContainerBuilder $container): void { $this->loadInternal($this->processConfiguration($this->getConfiguration($configs, $container), $configs), $container); } /** * Configures the passed container according to the merged configuration. */ abstract protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void; } ================================================ FILE: DependencyInjection/ControllerArgumentValueResolverPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver; use Symfony\Component\Stopwatch\Stopwatch; /** * Gathers and configures the argument value resolvers. * * @author Iltar van der Berg */ class ControllerArgumentValueResolverPass implements CompilerPassInterface { use PriorityTaggedServiceTrait; public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('argument_resolver')) { return; } $definitions = $container->getDefinitions(); $namedResolvers = $this->findAndSortTaggedServices(new TaggedIteratorArgument('controller.targeted_value_resolver', 'name', needsIndexes: true), $container); $resolvers = $this->findAndSortTaggedServices(new TaggedIteratorArgument('controller.argument_value_resolver', 'name', needsIndexes: true), $container); foreach ($resolvers as $name => $resolver) { if ($definitions[(string) $resolver]->hasTag('controller.targeted_value_resolver')) { unset($resolvers[$name]); } else { $namedResolvers[$name] ??= clone $resolver; } } if ($container->getParameter('kernel.debug') && class_exists(Stopwatch::class) && $container->has('debug.stopwatch')) { foreach ($resolvers as $name => $resolver) { $resolvers[$name] = new Reference('.debug.value_resolver.'.$resolver); $container->register('.debug.value_resolver.'.$resolver, TraceableValueResolver::class) ->setArguments([$resolver, new Reference('debug.stopwatch')]); } foreach ($namedResolvers as $name => $resolver) { $namedResolvers[$name] = new Reference('.debug.value_resolver.'.$resolver); $container->register('.debug.value_resolver.'.$resolver, TraceableValueResolver::class) ->setArguments([$resolver, new Reference('debug.stopwatch')]); } } $container ->getDefinition('argument_resolver') ->replaceArgument(1, new IteratorArgument(array_values($resolvers))) ->setArgument(2, new ServiceLocatorArgument($namedResolvers)) ; } } ================================================ FILE: DependencyInjection/ControllerAttributesListenerPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\KernelEvents; /** * Collects attribute listeners and registers them for ControllerAttributesListener. * * @author Nicolas Grekas */ class ControllerAttributesListenerPass implements CompilerPassInterface { private const ATTRIBUTE_EVENTS = [ KernelEvents::CONTROLLER, KernelEvents::CONTROLLER_ARGUMENTS, KernelEvents::VIEW, KernelEvents::RESPONSE, KernelEvents::EXCEPTION, KernelEvents::FINISH_REQUEST, ]; public function process(ContainerBuilder $container): void { if (!$container->has('event_dispatcher') || !$container->hasDefinition('kernel.controller_attributes_listener')) { return; } $dispatcherDefinition = $container->findDefinition('event_dispatcher'); $attributesWithListeners = []; foreach ($dispatcherDefinition->getMethodCalls() as [$method, $arguments]) { if ('addListener' !== $method || !\is_string($eventName = $arguments[0] ?? null)) { continue; } foreach (self::ATTRIBUTE_EVENTS as $kernelEvent) { if ('.' === ($eventName[\strlen($kernelEvent)] ?? null) && str_starts_with($eventName, $kernelEvent)) { $attributesWithListeners[$kernelEvent][substr($eventName, \strlen($kernelEvent) + 1)] = true; break; } } } $container->getDefinition('kernel.controller_attributes_listener')->replaceArgument(0, $attributesWithListeners); } } ================================================ FILE: DependencyInjection/Extension.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Component\DependencyInjection\Extension\Extension as BaseExtension; trigger_deprecation('symfony/http-kernel', '8.1', 'The "%s" class is deprecated, use "%s" instead.', Extension::class, BaseExtension::class); /** * Allow adding classes to the class cache. * * @author Fabien Potencier * * @deprecated since Symfony 8.1; use Symfony\Component\DependencyInjection\Extension\Extension instead */ abstract class Extension extends BaseExtension { } ================================================ FILE: DependencyInjection/FragmentRendererPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface; /** * Adds services tagged kernel.fragment_renderer as HTTP content rendering strategies. * * @author Fabien Potencier */ class FragmentRendererPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('fragment.handler')) { return; } $definition = $container->getDefinition('fragment.handler'); $renderers = []; foreach ($container->findTaggedServiceIds('kernel.fragment_renderer', true) as $id => $tags) { $def = $container->getDefinition($id); $class = $container->getParameterBag()->resolveValue($def->getClass()); if (!$r = $container->getReflectionClass($class)) { throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); } if (!$r->isSubclassOf(FragmentRendererInterface::class)) { throw new InvalidArgumentException(\sprintf('Service "%s" must implement interface "%s".', $id, FragmentRendererInterface::class)); } foreach ($tags as $tag) { $renderers[$tag['alias']] = new Reference($id); } } $definition->replaceArgument(0, ServiceLocatorTagPass::register($container, $renderers)); } } ================================================ FILE: DependencyInjection/LazyLoadingFragmentHandler.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Fragment\FragmentHandler; /** * Lazily loads fragment renderers from the dependency injection container. * * @author Fabien Potencier */ class LazyLoadingFragmentHandler extends FragmentHandler { /** * @var array */ private array $initialized = []; public function __construct( private ContainerInterface $container, RequestStack $requestStack, bool $debug = false, ) { parent::__construct($requestStack, [], $debug); } public function render(string|ControllerReference $uri, string $renderer = 'inline', array $options = []): ?string { if (!isset($this->initialized[$renderer]) && $this->container->has($renderer)) { $this->addRenderer($this->container->get($renderer)); $this->initialized[$renderer] = true; } return parent::render($uri, $renderer, $options); } } ================================================ FILE: DependencyInjection/LoggerPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Log\Logger; /** * Registers the default logger if necessary. * * @author Kévin Dunglas */ class LoggerPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if (!$container->has(LoggerInterface::class)) { $container->setAlias(LoggerInterface::class, 'logger'); } if ($container->has('logger')) { return; } if ($debug = $container->getParameter('kernel.debug')) { $debug = $container->hasParameter('kernel.runtime_mode.web') ? $container->getParameter('kernel.runtime_mode.web') : !\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true); } $container->register('logger', Logger::class) ->setArguments([null, null, null, new Reference(RequestStack::class), $debug]); } } ================================================ FILE: DependencyInjection/MergeExtensionConfigurationPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\MergeExtensionConfigurationPass as BaseMergeExtensionConfigurationPass; use Symfony\Component\DependencyInjection\ContainerBuilder; /** * Ensures certain extensions are always loaded. * * @author Kris Wallsmith */ class MergeExtensionConfigurationPass extends BaseMergeExtensionConfigurationPass { /** * @param string[] $extensions */ public function __construct( private array $extensions, ) { } public function process(ContainerBuilder $container): void { foreach ($this->extensions as $extension) { if (!\count($container->getExtensionConfig($extension))) { $container->loadFromExtension($extension, []); } } parent::process($container); } } ================================================ FILE: DependencyInjection/RegisterControllerArgumentLocatorsPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\Attribute\AutowireCallable; use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\VarExporter\ProxyHelper; /** * Creates the service-locators required by ServiceValueResolver. * * @author Nicolas Grekas */ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('argument_resolver.service') && !$container->hasDefinition('argument_resolver.not_tagged_controller')) { return; } $parameterBag = $container->getParameterBag(); $controllers = []; $controllerClasses = []; $publicAliases = []; foreach ($container->getAliases() as $id => $alias) { if ($alias->isPublic()) { $publicAliases[(string) $alias][] = $id; } } foreach ($container->findTaggedServiceIds('controller.service_arguments', true) as $id => $tags) { $def = $container->getDefinition($id); $def->setPublic(true); $def->setLazy(false); $class = $def->getClass(); $autowire = $def->isAutowired(); $bindings = $def->getBindings(); // resolve service class, taking parent definitions into account while ($def instanceof ChildDefinition) { $def = $container->findDefinition($def->getParent()); $class = $class ?: $def->getClass(); $bindings += $def->getBindings(); } $class = $parameterBag->resolveValue($class); if (!$r = $container->getReflectionClass($class)) { throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); } $controllerClasses[] = $class; // get regular public methods $methods = []; $arguments = []; foreach ($r->getMethods(\ReflectionMethod::IS_PUBLIC) as $r) { if ('setContainer' === $r->name) { continue; } if (!$r->isConstructor() && !$r->isDestructor() && !$r->isAbstract()) { $methods[strtolower($r->name)] = [$r, $r->getParameters()]; } } // validate and collect explicit per-actions and per-arguments service references foreach ($tags as $attributes) { if (!isset($attributes['action']) && !isset($attributes['argument']) && !isset($attributes['id'])) { $autowire = true; continue; } foreach (['action', 'argument', 'id'] as $k) { if (!isset($attributes[$k][0])) { throw new InvalidArgumentException(\sprintf('Missing "%s" attribute on tag "controller.service_arguments" %s for service "%s".', $k, json_encode($attributes, \JSON_UNESCAPED_UNICODE), $id)); } } if (!isset($methods[$action = strtolower($attributes['action'])])) { throw new InvalidArgumentException(\sprintf('Invalid "action" attribute on tag "controller.service_arguments" for service "%s": no public "%s()" method found on class "%s".', $id, $attributes['action'], $class)); } [$r, $parameters] = $methods[$action]; $found = false; foreach ($parameters as $p) { if ($attributes['argument'] === $p->name) { if (!isset($arguments[$r->name][$p->name])) { $arguments[$r->name][$p->name] = $attributes['id']; } $found = true; break; } } if (!$found) { throw new InvalidArgumentException(\sprintf('Invalid "controller.service_arguments" tag for service "%s": method "%s()" has no "%s" argument on class "%s".', $id, $r->name, $attributes['argument'], $class)); } } foreach ($methods as [$r, $parameters]) { /** @var \ReflectionMethod $r */ // create a per-method map of argument-names to service/type-references $args = []; $erroredIds = 0; foreach ($parameters as $p) { /** @var \ReflectionParameter $p */ $type = preg_replace('/(^|[(|&])\\\\/', '\1', $target = ltrim(ProxyHelper::exportType($p) ?? '', '?')); $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; $autowireAttributes = null; $parsedName = $p->name; $k = null; if (isset($arguments[$r->name][$p->name])) { $target = $arguments[$r->name][$p->name]; if ('?' !== $target[0]) { $invalidBehavior = ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE; } elseif ('' === $target = substr($target, 1)) { throw new InvalidArgumentException(\sprintf('A "controller.service_arguments" tag must have non-empty "id" attributes for service "%s".', $id)); } elseif ($p->allowsNull() && !$p->isOptional()) { $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; } } elseif (isset($bindings[$bindingName = $type.' $'.$name = Target::parseName($p, $k, $parsedName)]) || isset($bindings[$bindingName = $type.' $'.$parsedName]) || isset($bindings[$bindingName = '$'.$name]) || isset($bindings[$bindingName = $type]) ) { $binding = $bindings[$bindingName]; [$bindingValue, $bindingId, , $bindingType, $bindingFile] = $binding->getValues(); $binding->setValues([$bindingValue, $bindingId, true, $bindingType, $bindingFile]); $args[$p->name] = $bindingValue; continue; } elseif (!$autowire || (!($autowireAttributes = $p->getAttributes(Autowire::class, \ReflectionAttribute::IS_INSTANCEOF)) && (!$type || '\\' !== $target[0]))) { continue; } elseif (!$autowireAttributes && is_subclass_of($type, \UnitEnum::class)) { // do not attempt to register enum typed arguments if not already present in bindings continue; } elseif (!$p->allowsNull()) { $invalidBehavior = ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE; } if (Request::class === $type || SessionInterface::class === $type || Response::class === $type) { continue; } if ($autowireAttributes) { $attribute = $autowireAttributes[0]->newInstance(); $value = $parameterBag->resolveValue($attribute->value); if ($attribute instanceof AutowireCallable) { $args[$p->name] = $attribute->buildDefinition($value, $type, $p); } elseif ($value instanceof Reference) { $args[$p->name] = $type ? new TypedReference($value, $type, $invalidBehavior, $p->name) : new Reference($value, $invalidBehavior); } else { $args[$p->name] = new Reference('.value.'.$container->hash($value)); $container->register((string) $args[$p->name], 'mixed') ->setFactory('current') ->addArgument([$value]); } continue; } if ($type && !$p->isOptional() && !$p->allowsNull() && !class_exists($type) && !interface_exists($type, false)) { $message = \sprintf('Cannot determine controller argument for "%s::%s()": the $%s argument is type-hinted with the non-existent class or interface: "%s".', $class, $r->name, $p->name, $type); // see if the type-hint lives in the same namespace as the controller if (0 === strncmp($type, $class, strrpos($class, '\\'))) { $message .= ' Did you forget to add a use statement?'; } $container->register($erroredId = '.errored.'.$container->hash($message), $type) ->addError($message); $args[$p->name] = new Reference($erroredId, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE); ++$erroredIds; } else { $targetAttribute = null; $name = Target::parseName($p, $targetAttribute); $target = preg_replace('/(^|[(|&])\\\\/', '\\1', $target); $args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, $name, $targetAttribute ? [$targetAttribute] : []) : new Reference($target, $invalidBehavior); } } // register the maps as a per-method service-locators if ($args) { $controllers[$id.'::'.$r->name] = ServiceLocatorTagPass::register($container, $args, \count($args) !== $erroredIds ? $id.'::'.$r->name.'()' : null); foreach ($publicAliases[$id] ?? [] as $alias) { $controllers[$alias.'::'.$r->name] = clone $controllers[$id.'::'.$r->name]; } } } } $controllerLocatorRef = ServiceLocatorTagPass::register($container, $controllers); if ($container->hasDefinition('argument_resolver.service')) { $container->getDefinition('argument_resolver.service') ->replaceArgument(0, $controllerLocatorRef); } if ($container->hasDefinition('argument_resolver.not_tagged_controller')) { $container->getDefinition('argument_resolver.not_tagged_controller') ->replaceArgument(0, $controllerLocatorRef); } $container->setAlias('argument_resolver.controller_locator', (string) $controllerLocatorRef); if ($container->hasDefinition('controller_resolver')) { $container->getDefinition('controller_resolver') ->addMethodCall('allowControllers', [array_unique($controllerClasses)]); } } } ================================================ FILE: DependencyInjection/RegisterLocaleAwareServicesPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; /** * Register all services that have the "kernel.locale_aware" tag into the listener. * * @author Pierre Bobiet */ class RegisterLocaleAwareServicesPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if (!$container->hasDefinition('locale_aware_listener')) { return; } $services = []; foreach ($container->findTaggedServiceIds('kernel.locale_aware') as $id => $tags) { $services[] = new Reference($id); } if (!$services) { $container->removeDefinition('locale_aware_listener'); return; } $container ->getDefinition('locale_aware_listener') ->setArgument(0, new IteratorArgument($services)) ; } } ================================================ FILE: DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; /** * Removes empty service-locators registered for ServiceValueResolver. * * @author Nicolas Grekas */ class RemoveEmptyControllerArgumentLocatorsPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { $controllerLocator = $container->findDefinition('argument_resolver.controller_locator'); $controllers = $controllerLocator->getArgument(0); foreach ($controllers as $controller => $argumentRef) { $argumentLocator = $container->getDefinition((string) $argumentRef->getValues()[0]); if ($argumentLocator->getFactory()) { $argumentLocator = $container->getDefinition($argumentLocator->getFactory()[0]); } if (!$argumentLocator->getArgument(0)) { // remove empty argument locators $reason = \sprintf('Removing service-argument resolver for controller "%s": no corresponding services exist for the referenced types.', $controller); } else { // any methods listed for call-at-instantiation cannot be actions $reason = false; [$id, $action] = explode('::', $controller); if ($container->hasAlias($id)) { continue; } $controllerDef = $container->getDefinition($id); foreach ($controllerDef->getMethodCalls() as [$method]) { if (0 === strcasecmp($action, $method)) { $reason = \sprintf('Removing method "%s" of service "%s" from controller candidates: the method is called at instantiation, thus cannot be an action.', $action, $id); break; } } if (!$reason) { // see Symfony\Component\HttpKernel\Controller\ContainerControllerResolver $controllers[$id.':'.$action] = $argumentRef; if ('__invoke' === $action) { $controllers[$id] = $argumentRef; } continue; } } unset($controllers[$controller]); $container->log($this, $reason); } $controllerLocator->replaceArgument(0, $controllers); } } ================================================ FILE: DependencyInjection/ResettableServicePass.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; /** * @author Alexander M. Turek */ class ResettableServicePass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if (!$container->has('services_resetter')) { return; } $services = $methods = []; foreach ($container->findTaggedServiceIds('kernel.reset', true) as $id => $tags) { $services[$id] = new Reference($id, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE); foreach ($tags as $attributes) { if (!isset($attributes['method'])) { throw new RuntimeException(\sprintf('Tag "kernel.reset" requires the "method" attribute to be set on service "%s".', $id)); } if (!isset($methods[$id])) { $methods[$id] = []; } if ('ignore' === ($attributes['on_invalid'] ?? null)) { $attributes['method'] = '?'.$attributes['method']; } $methods[$id][] = $attributes['method']; } } if (!$services) { $container->removeAlias('services_resetter'); $container->removeDefinition('services_resetter'); return; } $container->findDefinition('services_resetter') ->setArgument(0, new IteratorArgument($services)) ->setArgument(1, $methods); } } ================================================ FILE: DependencyInjection/ServicesResetter.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use ProxyManager\Proxy\LazyLoadingInterface; use Symfony\Component\VarExporter\LazyObjectInterface; /** * Resets provided services. * * @author Alexander M. Turek * @author Nicolas Grekas */ final class ServicesResetter implements ServicesResetterInterface { /** * @param \Traversable $resettableServices * @param array $resetMethods */ public function __construct( private \Traversable $resettableServices, private array $resetMethods, ) { } public function reset(): void { foreach ($this->resettableServices as $id => $service) { if ($service instanceof LazyObjectInterface && !$service->isLazyObjectInitialized(true)) { continue; } if ($service instanceof LazyLoadingInterface && !$service->isProxyInitialized()) { continue; } if (new \ReflectionClass($service)->isUninitializedLazyObject($service)) { continue; } foreach ((array) $this->resetMethods[$id] as $resetMethod) { if ('?' === $resetMethod[0] && !method_exists($service, $resetMethod = substr($resetMethod, 1))) { continue; } $service->$resetMethod(); } } } } ================================================ FILE: DependencyInjection/ServicesResetterInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\DependencyInjection; use Symfony\Contracts\Service\ResetInterface; /** * Resets provided services. */ interface ServicesResetterInterface extends ResetInterface { } ================================================ FILE: Event/ControllerArgumentsEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Event; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Allows filtering of controller arguments. * * You can call getController() to retrieve the controller and getArguments * to retrieve the current arguments. With setArguments() you can replace * arguments that are used to call the controller. * * Arguments set in the event must be compatible with the signature of the * controller. * * @author Christophe Coevoet */ final class ControllerArgumentsEvent extends KernelEvent { private ControllerEvent $controllerEvent; private array $namedArguments; public function __construct( HttpKernelInterface $kernel, callable|ControllerEvent $controller, private array $arguments, Request $request, ?int $requestType, ) { parent::__construct($kernel, $request, $requestType); if (!$controller instanceof ControllerEvent) { $controller = new ControllerEvent($kernel, $controller, $request, $requestType); } $this->controllerEvent = $controller; } public function getController(): callable { return $this->controllerEvent->getController(); } /** * @param list|null $attributes */ public function setController(callable $controller, ?array $attributes = null): void { $this->controllerEvent->setController($controller, $attributes); unset($this->namedArguments); } /** * @return list */ public function getArguments(): array { return $this->arguments; } /** * @param list $arguments */ public function setArguments(array $arguments): void { $this->arguments = $arguments; unset($this->namedArguments); } /** * @return array */ public function getNamedArguments(): array { if (isset($this->namedArguments)) { return $this->namedArguments; } $namedArguments = []; $arguments = $this->arguments; foreach ($this->controllerEvent->getControllerReflector()->getParameters() as $i => $param) { if ($param->isVariadic()) { $namedArguments[$param->name] = \array_slice($arguments, $i); break; } if (\array_key_exists($i, $arguments)) { $namedArguments[$param->name] = $arguments[$i]; } elseif ($param->isDefaultvalueAvailable()) { $namedArguments[$param->name] = $param->getDefaultValue(); } } return $this->namedArguments = $namedArguments; } /** * @template T of object * * @param class-string|'*'|null $className * * @return ($className is null ? array> : ($className is '*' ? list : list)) */ public function getAttributes(?string $className = null): array { return $this->controllerEvent->getAttributes($className); } public function evaluate(mixed $value, ?ExpressionLanguage $expressionLanguage): mixed { if (!$value instanceof \Closure && !$value instanceof Expression) { return $value; } return $this->controllerEvent->evaluate($value, $expressionLanguage, $this->getNamedArguments()); } } ================================================ FILE: Event/ControllerArgumentsMetadata.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Event; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; /** * Provides read-only access to controller metadata. * * @author Nicolas Grekas */ class ControllerArgumentsMetadata extends ControllerMetadata { public function __construct( ControllerEvent $controllerEvent, private ControllerArgumentsEvent $controllerArgumentsEvent, ) { parent::__construct($controllerEvent); } /** * @return list */ public function getArguments(): array { return $this->controllerArgumentsEvent->getArguments(); } /** * @return array */ public function getNamedArguments(): array { return $this->controllerArgumentsEvent->getNamedArguments(); } public function evaluate(mixed $value, ?ExpressionLanguage $expressionLanguage): mixed { return $this->controllerArgumentsEvent->evaluate($value, $expressionLanguage); } } ================================================ FILE: Event/ControllerAttributeEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Event; use Psr\EventDispatcher\StoppableEventInterface; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; /** * Event dispatched for each controller attribute. * * @template T of object * * @author Nicolas Grekas */ final class ControllerAttributeEvent implements StoppableEventInterface { private string|array|object|null $controller; /** * @param T $attribute */ public function __construct( /** @var T */ public readonly object $attribute, public readonly KernelEvent $kernelEvent, private readonly ?ExpressionLanguage $expressionLanguage = null, ) { $this->controller = match (true) { $kernelEvent instanceof ControllerEvent => $kernelEvent->getController(), $kernelEvent instanceof ControllerArgumentsEvent => $kernelEvent->getController(), default => null, }; } public function isPropagationStopped(): bool { $event = $this->kernelEvent; if ($event->isPropagationStopped()) { return true; } if (!$this->controller) { return false; } $controller = match (true) { $event instanceof ControllerEvent => $event->getController(), $event instanceof ControllerArgumentsEvent => $event->getController(), }; return $controller instanceof \Closure ? $controller != $this->controller : $controller !== $this->controller; } public function evaluate(mixed $value, ?ExpressionLanguage $expressionLanguage = null): mixed { if (!$value instanceof \Closure && !$value instanceof Expression) { return $value; } $event = $this->kernelEvent; $expressionLanguage ??= $this->expressionLanguage; return match (true) { $event instanceof ControllerEvent => $event->evaluate($value, $expressionLanguage), $event instanceof ControllerArgumentsEvent => $event->evaluate($value, $expressionLanguage), ($m = $event->controllerMetadata ?? null) instanceof ControllerMetadata => $m->evaluate($value, $expressionLanguage), }; } } ================================================ FILE: Event/ControllerEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Event; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Allows filtering of a controller callable. * * You can call getController() to retrieve the current controller. With * setController() you can set a new controller that is used in the processing * of the request. * * @author Bernhard Schussek */ final class ControllerEvent extends KernelEvent { private string|array|object $controller; private \ReflectionFunctionAbstract $controllerReflector; public function __construct(HttpKernelInterface $kernel, callable $controller, Request $request, ?int $requestType) { parent::__construct($kernel, $request, $requestType); $this->setController($controller); } public function getController(): callable { return $this->controller; } public function getControllerReflector(): \ReflectionFunctionAbstract { return $this->controllerReflector; } /** * @param list|null $attributes */ public function setController(callable $controller, ?array $attributes = null): void { if (null !== $attributes) { if (!array_is_list($flattenAttributes = $attributes)) { trigger_deprecation('symfony/http-kernel', '8.1', 'Passing an array of attributes grouped by class name to "%s()" is deprecated. Pass a flat list of attributes instead.', __METHOD__); $flattenAttributes = []; foreach ($attributes as $attributes) { foreach (\is_array($attributes) ? $attributes : [$attributes] as $attribute) { $flattenAttributes[] = $attribute; } } } $this->getRequest()->attributes->set('_controller_attributes', $flattenAttributes); } if (isset($this->controller) && ($controller instanceof \Closure ? $controller == $this->controller : $controller === $this->controller)) { $this->controller = $controller; return; } if (null === $attributes) { $this->getRequest()->attributes->remove('_controller_attributes'); } $this->controllerReflector = match (true) { \is_array($controller) && method_exists(...$controller) => new \ReflectionMethod(...$controller), \is_string($controller) && str_contains($controller, '::') => new \ReflectionMethod(...explode('::', $controller, 2)), default => new \ReflectionFunction($controller(...)), }; $this->controller = $controller; } /** * @template T of object * * @param class-string|'*'|null $className * * @return ($className is null ? array> : ($className is '*' ? list : list)) */ public function getAttributes(?string $className = null): array { if (null === $attributes = $this->getRequest()->attributes->get('_controller_attributes')) { $class = match (true) { \is_array($this->controller) && method_exists(...$this->controller) => new \ReflectionClass($this->controller[0]), \is_string($this->controller) && false !== $i = strpos($this->controller, '::') => new \ReflectionClass(substr($this->controller, 0, $i)), $this->controllerReflector instanceof \ReflectionFunction => $this->controllerReflector->isAnonymous() ? null : $this->controllerReflector->getClosureCalledClass(), }; $attributes = []; foreach (array_merge($class?->getAttributes() ?? [], $this->controllerReflector->getAttributes()) as $attribute) { if (class_exists($attribute->getName())) { $attributes[] = $attribute->newInstance(); } } $this->getRequest()->attributes->set('_controller_attributes', $attributes); } if ('*' === $className) { return $attributes; } if (null !== $className) { return array_values(array_filter($attributes, static fn ($attr) => $attr instanceof $className)); } $grouped = []; foreach ($attributes as $attribute) { $grouped[$attribute::class][] = $attribute; } return $grouped; } public function evaluate(mixed $value, ?ExpressionLanguage $expressionLanguage, array $args = []): mixed { if (!$value instanceof \Closure && !$value instanceof Expression) { return $value; } $controller = $this->getController(); $controller = match (true) { \is_object($controller) && !$controller instanceof \Closure => $controller, \is_array($controller) && \is_object($controller[0]) => $controller[0], default => null, }; if ($value instanceof \Closure) { return $value($args, $this->getRequest(), $controller); } if (!$expressionLanguage) { throw new \LogicException('Cannot evaluate Expression for controllers since no ExpressionLanguage service was configured.'); } return $expressionLanguage->evaluate($value, [ 'request' => $this->getRequest(), 'args' => $args, 'this' => $controller, ]); } } ================================================ FILE: Event/ControllerMetadata.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Event; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; /** * Provides read-only access to controller metadata. * * @author Nicolas Grekas */ class ControllerMetadata { public function __construct( private ControllerEvent $controllerEvent, ) { } public function getController(): callable { return $this->controllerEvent->getController(); } public function getReflector(): \ReflectionFunctionAbstract { return $this->controllerEvent->getControllerReflector(); } /** * @template T of object * * @param class-string|'*'|null $className * * @return ($className is null ? array> : ($className is '*' ? list : list)) */ public function getAttributes(?string $className = null): array { return $this->controllerEvent->getAttributes($className); } public function evaluate(mixed $value, ?ExpressionLanguage $expressionLanguage): mixed { return $this->controllerEvent->evaluate($value, $expressionLanguage); } } ================================================ FILE: Event/ExceptionEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Event; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Allows to create a response for a thrown exception. * * Call setResponse() to set the response that will be returned for the * current request. The propagation of this event is stopped as soon as a * response is set. * * You can also call setThrowable() to replace the thrown exception. This * exception will be thrown if no response is set during processing of this * event. * * @author Bernhard Schussek */ final class ExceptionEvent extends RequestEvent { private \Throwable $throwable; private bool $allowCustomResponseCode = false; public function __construct( HttpKernelInterface $kernel, Request $request, int $requestType, \Throwable $e, private bool $isKernelTerminating = false, public readonly ?ControllerMetadata $controllerMetadata = null, ) { parent::__construct($kernel, $request, $requestType); $this->setThrowable($e); } public function getThrowable(): \Throwable { return $this->throwable; } /** * Replaces the thrown exception. * * This exception will be thrown if no response is set in the event. */ public function setThrowable(\Throwable $exception): void { $this->throwable = $exception; } /** * Mark the event as allowing a custom response code. */ public function allowCustomResponseCode(): void { $this->allowCustomResponseCode = true; } /** * Returns true if the event allows a custom response code. */ public function isAllowingCustomResponseCode(): bool { return $this->allowCustomResponseCode; } public function isKernelTerminating(): bool { return $this->isKernelTerminating; } } ================================================ FILE: Event/FinishRequestEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Event; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Triggered whenever a request is fully processed. * * @author Benjamin Eberlei */ final class FinishRequestEvent extends KernelEvent { public function __construct( HttpKernelInterface $kernel, Request $request, ?int $requestType, public readonly ?ControllerMetadata $controllerMetadata = null, ) { parent::__construct($kernel, $request, $requestType); } } ================================================ FILE: Event/KernelEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Event; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Contracts\EventDispatcher\Event; /** * Base class for events dispatched in the HttpKernel component. * * @author Bernhard Schussek */ class KernelEvent extends Event { /** * @param int $requestType The request type the kernel is currently processing; one of * HttpKernelInterface::MAIN_REQUEST or HttpKernelInterface::SUB_REQUEST */ public function __construct( private HttpKernelInterface $kernel, private Request $request, private ?int $requestType, ) { } /** * Returns the kernel in which this event was thrown. */ public function getKernel(): HttpKernelInterface { return $this->kernel; } /** * Returns the request the kernel is currently processing. */ public function getRequest(): Request { return $this->request; } /** * Returns the request type the kernel is currently processing. * * @return int One of HttpKernelInterface::MAIN_REQUEST and * HttpKernelInterface::SUB_REQUEST */ public function getRequestType(): int { return $this->requestType; } /** * Checks if this is the main request. */ public function isMainRequest(): bool { return HttpKernelInterface::MAIN_REQUEST === $this->requestType; } } ================================================ FILE: Event/RequestEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Event; use Symfony\Component\HttpFoundation\Response; /** * Allows to create a response for a request. * * Call setResponse() to set the response that will be returned for the * current request. The propagation of this event is stopped as soon as a * response is set. * * @author Bernhard Schussek */ class RequestEvent extends KernelEvent { private ?Response $response = null; /** * Returns the response object. */ public function getResponse(): ?Response { return $this->response; } /** * Sets a response and stops event propagation. */ public function setResponse(Response $response): void { $this->response = $response; $this->stopPropagation(); } /** * Returns whether a response was set. * * @psalm-assert-if-true !null $this->getResponse() */ public function hasResponse(): bool { return null !== $this->response; } } ================================================ FILE: Event/ResponseEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Event; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Allows to filter a Response object. * * You can call getResponse() to retrieve the current response. With * setResponse() you can set a new response that will be returned to the * browser. * * @author Bernhard Schussek */ final class ResponseEvent extends KernelEvent { public function __construct( HttpKernelInterface $kernel, Request $request, int $requestType, private Response $response, public readonly ?ControllerArgumentsMetadata $controllerMetadata = null, ) { parent::__construct($kernel, $request, $requestType); } public function getResponse(): Response { return $this->response; } public function setResponse(Response $response): void { $this->response = $response; } } ================================================ FILE: Event/TerminateEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Event; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Allows to execute logic after a response was sent. * * Since it's only triggered on main requests, the `getRequestType()` method * will always return the value of `HttpKernelInterface::MAIN_REQUEST`. * * @author Jordi Boggiano */ final class TerminateEvent extends KernelEvent { public function __construct( HttpKernelInterface $kernel, Request $request, private Response $response, ) { parent::__construct($kernel, $request, HttpKernelInterface::MAIN_REQUEST); } public function getResponse(): Response { return $this->response; } } ================================================ FILE: Event/ViewEvent.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Event; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Allows to create a response for the return value of a controller. * * Call setResponse() to set the response that will be returned for the * current request. The propagation of this event is stopped as soon as a * response is set. * * @author Bernhard Schussek */ final class ViewEvent extends RequestEvent { public readonly ?ControllerArgumentsMetadata $controllerMetadata; /** * @deprecated since Symfony 8.1, use $controllerMetadata instead */ public private(set) ?ControllerArgumentsEvent $controllerArgumentsEvent { get { trigger_deprecation('symfony/http-kernel', '8.1', 'Accessing the "controllerArgumentsEvent" property of the "%s" class is deprecated. Use "controllerMetadata" instead.', __CLASS__); if (!$m = $this->controllerMetadata) { return null; } return $this->controllerArgumentsEvent ??= new ControllerArgumentsEvent($this->getKernel(), \Closure::bind(fn () => $this->controllerEvent, $m, ControllerMetadata::class)(), $m->getArguments(), $this->getRequest(), $this->getRequestType()); } } public function __construct( HttpKernelInterface $kernel, Request $request, int $requestType, private mixed $controllerResult, ControllerArgumentsMetadata|ControllerArgumentsEvent|null $controllerMetadata = null, ) { if ($controllerMetadata instanceof ControllerArgumentsEvent) { trigger_deprecation('symfony/http-kernel', '8.1', 'Passing a ControllerArgumentsEvent to the ViewEvent constructor is deprecated. Pass a ControllerArgumentsMetadata instance instead.'); $this->controllerArgumentsEvent = $controllerMetadata; $controllerEvent = \Closure::bind(fn () => $this->controllerEvent, $controllerMetadata, ControllerArgumentsEvent::class)(); $controllerMetadata = new ControllerArgumentsMetadata($controllerEvent, $controllerMetadata); } $this->controllerMetadata = $controllerMetadata; parent::__construct($kernel, $request, $requestType); } public function getControllerResult(): mixed { return $this->controllerResult; } public function setControllerResult(mixed $controllerResult): void { $this->controllerResult = $controllerResult; } } ================================================ FILE: EventListener/AbstractSessionListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Psr\Container\ContainerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionUtils; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Exception\UnexpectedSessionUsageException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Contracts\Service\ResetInterface; /** * Sets the session onto the request on the "kernel.request" event and saves * it on the "kernel.response" event. * * In addition, if the session has been started it overrides the Cache-Control * header in such a way that all caching is disabled in that case. * If you have a scenario where caching responses with session information in * them makes sense, you can disable this behaviour by setting the header * AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER on the response. * * @author Johannes M. Schmitt * @author Tobias Schultze */ abstract class AbstractSessionListener implements EventSubscriberInterface, ResetInterface { public const NO_AUTO_CACHE_CONTROL_HEADER = 'Symfony-Session-NoAutoCacheControl'; /** * @param array $sessionOptions * * @internal */ public function __construct( private ?ContainerInterface $container = null, private bool $debug = false, private array $sessionOptions = [], ) { } /** * @internal */ public function onKernelRequest(RequestEvent $event): void { if (!$event->isMainRequest()) { return; } $request = $event->getRequest(); if (!$request->hasSession()) { $request->setSessionFactory(function () use ($request) { // Prevent calling `$this->getSession()` twice in case the Request (and the below factory) is cloned static $sess; if (!$sess) { $sess = $this->getSession(); $request->setSession($sess); /* * For supporting sessions in php runtime with runners like roadrunner or swoole, the session * cookie needs to be read from the cookie bag and set on the session storage. * * Do not set it when a native php session is active. */ if ($sess && !$sess->isStarted() && \PHP_SESSION_ACTIVE !== session_status()) { $sessionId = $sess->getId() ?: $request->cookies->get($sess->getName(), ''); $sess->setId($sessionId); } } return $sess; }); } } /** * @internal */ public function onKernelResponse(ResponseEvent $event): void { if (!$event->isMainRequest()) { return; } $response = $event->getResponse(); $autoCacheControl = !$response->headers->has(self::NO_AUTO_CACHE_CONTROL_HEADER); // Always remove the internal header if present $response->headers->remove(self::NO_AUTO_CACHE_CONTROL_HEADER); if (!$event->getRequest()->hasSession(true)) { return; } $session = $event->getRequest()->getSession(); if ($session->isStarted()) { /* * Saves the session, in case it is still open, before sending the response/headers. * * This ensures several things in case the developer did not save the session explicitly: * * * If a session save handler without locking is used, it ensures the data is available * on the next request, e.g. after a redirect. PHPs auto-save at script end via * session_register_shutdown is executed after fastcgi_finish_request. So in this case * the data could be missing the next request because it might not be saved the moment * the new request is processed. * * A locking save handler (e.g. the native 'files') circumvents concurrency problems like * the one above. But by saving the session before long-running things in the terminate event, * we ensure the session is not blocked longer than needed. * * When regenerating the session ID no locking is involved in PHPs session design. See * https://bugs.php.net/61470 for a discussion. So in this case, the session must * be saved anyway before sending the headers with the new session ID. Otherwise session * data could get lost again for concurrent requests with the new ID. One result could be * that you get logged out after just logging in. * * This listener should be executed as one of the last listeners, so that previous listeners * can still operate on the open session. This prevents the overhead of restarting it. * Listeners after closing the session can still work with the session as usual because * Symfonys session implementation starts the session on demand. So writing to it after * it is saved will just restart it. */ $session->save(); /* * For supporting sessions in php runtime with runners like roadrunner or swoole the session * cookie need to be written on the response object and should not be written by PHP itself. */ $sessionName = $session->getName(); $sessionId = $session->getId(); $sessionOptions = $this->getSessionOptions($this->sessionOptions); $sessionCookiePath = $sessionOptions['cookie_path'] ?? '/'; $sessionCookieDomain = $sessionOptions['cookie_domain'] ?? null; $sessionCookieSecure = $sessionOptions['cookie_secure'] ?? false; $sessionCookieHttpOnly = $sessionOptions['cookie_httponly'] ?? true; $sessionCookieSameSite = $sessionOptions['cookie_samesite'] ?? Cookie::SAMESITE_LAX; $sessionUseCookies = $sessionOptions['use_cookies'] ?? true; SessionUtils::popSessionCookie($sessionName, $sessionId); if ($sessionUseCookies) { $request = $event->getRequest(); $requestSessionCookieId = $request->cookies->get($sessionName); $isSessionEmpty = ($session instanceof Session ? $session->isEmpty() : !$session->all()) && empty($_SESSION); // checking $_SESSION to keep compatibility with native sessions if ($requestSessionCookieId && $isSessionEmpty) { // PHP internally sets the session cookie value to "deleted" when setcookie() is called with empty string $value argument // which happens in \Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler::destroy // when the session gets invalidated (for example on logout) so we must handle this case here too // otherwise we would send two Set-Cookie headers back with the response SessionUtils::popSessionCookie($sessionName, 'deleted'); $response->headers->clearCookie( $sessionName, $sessionCookiePath, $sessionCookieDomain, $sessionCookieSecure, $sessionCookieHttpOnly, $sessionCookieSameSite ); } elseif ($sessionId !== $requestSessionCookieId && !$isSessionEmpty) { $expire = 0; $lifetime = $sessionOptions['cookie_lifetime'] ?? null; if ($lifetime) { $expire = time() + $lifetime; } $response->headers->setCookie( Cookie::create( $sessionName, $sessionId, $expire, $sessionCookiePath, $sessionCookieDomain, $sessionCookieSecure, $sessionCookieHttpOnly, false, $sessionCookieSameSite ) ); } } } if ($session instanceof Session ? 0 === $session->getUsageIndex() : !$session->isStarted()) { return; } if ($autoCacheControl) { $maxAge = $response->headers->hasCacheControlDirective('public') ? 0 : (int) $response->getMaxAge(); $response ->setExpires(new \DateTimeImmutable('+'.$maxAge.' seconds')) ->setPrivate() ->setMaxAge($maxAge) ->headers->addCacheControlDirective('must-revalidate'); } if (!$event->getRequest()->attributes->get('_stateless', false)) { return; } if ($this->debug) { throw new UnexpectedSessionUsageException('Session was used while the request was declared stateless.'); } if ($this->container->has('logger')) { $this->container->get('logger')->warning('Session was used while the request was declared stateless.'); } } /** * @internal */ public function onSessionUsage(): void { if (!$this->debug) { return; } if ($this->container?->has('session_collector')) { $this->container->get('session_collector')(); } if (!$requestStack = $this->container?->has('request_stack') ? $this->container->get('request_stack') : null) { return; } $stateless = false; $clonedRequestStack = clone $requestStack; while (null !== ($request = $clonedRequestStack->pop()) && !$stateless) { $stateless = $request->attributes->get('_stateless'); } if (!$stateless) { return; } if (!$session = $requestStack->getCurrentRequest()->getSession()) { return; } if ($session->isStarted()) { $session->save(); } throw new UnexpectedSessionUsageException('Session was used while the request was declared stateless.'); } /** * @internal */ public static function getSubscribedEvents(): array { return [ KernelEvents::REQUEST => ['onKernelRequest', 128], // low priority to come after regular response listeners KernelEvents::RESPONSE => ['onKernelResponse', -1000], ]; } /** * @internal */ public function reset(): void { if (\PHP_SESSION_ACTIVE === session_status()) { session_abort(); } session_unset(); $_SESSION = []; if (!headers_sent()) { // session id can only be reset when no headers were so we check for headers_sent first session_id(''); } } /** * Gets the session object. * * @internal */ abstract protected function getSession(): ?SessionInterface; private function getSessionOptions(array $sessionOptions): array { $mergedSessionOptions = []; foreach (session_get_cookie_params() as $key => $value) { $mergedSessionOptions['cookie_'.$key] = $value; } foreach ($sessionOptions as $key => $value) { // do the same logic as in the NativeSessionStorage if ('cookie_secure' === $key && 'auto' === $value) { continue; } $mergedSessionOptions[$key] = $value; } return $mergedSessionOptions; } } ================================================ FILE: EventListener/AddRequestFormatsListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; /** * Adds configured formats to each request. * * @author Gildas Quemener * * @final */ class AddRequestFormatsListener implements EventSubscriberInterface { public function __construct( private array $formats, ) { } /** * Adds request formats. */ public function onKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); foreach ($this->formats as $format => $mimeTypes) { $request->setFormat($format, $mimeTypes); } } public static function getSubscribedEvents(): array { return [KernelEvents::REQUEST => ['onKernelRequest', 100]]; } } ================================================ FILE: EventListener/CacheAttributeListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpFoundation\HeaderBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\Cache; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerAttributeEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; /** * Handles HTTP cache headers configured via the Cache attribute. * * @author Fabien Potencier */ class CacheAttributeListener implements EventSubscriberInterface { public function __construct( private ?ExpressionLanguage $expressionLanguage = null, ) { } public function onKernelControllerAttribute(ControllerAttributeEvent $event): void { $cache = $event->attribute; $kernelEvent = $event->kernelEvent; $request = $event->kernelEvent->getRequest(); if ($kernelEvent instanceof ControllerArgumentsEvent) { if (null !== $variables = $this->getVariables($cache, $request, $kernelEvent)) { $cache->variables = $variables; } $this->processAttributeBeforeController($cache, $request, $kernelEvent); return; } if ($kernelEvent instanceof ResponseEvent) { $response = $kernelEvent->getResponse(); // http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-12#section-3.1 if (!\in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 304, 404, 410], true)) { return; } $this->processAttributeAfterController($cache, $request, $response); return; } } /** * @internal since Symfony 8.1, use onKernelControllerAttribute() instead */ public function onKernelControllerArguments(ControllerArgumentsEvent $event): void { $request = $event->getRequest(); /** @var Cache[] $attributes */ if (!$attributes = $request->attributes->get('_cache') ?? $event->getAttributes(Cache::class)) { return; } $request->attributes->set('_cache', $attributes); $variables = null; foreach ($attributes as $cache) { if (null !== $variables ??= $this->getVariables($cache, $request, $event)) { $cache->variables = $variables; } $this->processAttributeBeforeController($cache, $request, $event); } } /** * @internal since Symfony 8.1, use onKernelControllerAttribute() instead */ public function onKernelResponse(ResponseEvent $event): void { $request = $event->getRequest(); /** @var Cache[] $attributes */ if (!\is_array($attributes = $request->attributes->get('_cache'))) { return; } $response = $event->getResponse(); // http://tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-12#section-3.1 if (!\in_array($response->getStatusCode(), [200, 203, 300, 301, 302, 304, 404, 410], true)) { return; } $hasVary = null; $hasCacheControlDirective = null; for ($i = \count($attributes) - 1; 0 <= $i; --$i) { $this->processAttributeAfterController($attributes[$i], $request, $response, $hasVary, $hasCacheControlDirective); } } public static function getSubscribedEvents(): array { if (!class_exists(ControllerAttributesListener::class, false)) { return [ KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 10], KernelEvents::RESPONSE => ['onKernelResponse', -10], ]; } return [ KernelEvents::CONTROLLER_ARGUMENTS.'.'.Cache::class => 'onKernelControllerAttribute', KernelEvents::RESPONSE.'.'.Cache::class => 'onKernelControllerAttribute', ]; } public function reset(): void { } private function processAttributeBeforeController(Cache $cache, Request $request, ControllerArgumentsEvent $event): void { if (!\is_bool($cache->if)) { if (!\is_bool($if = $this->evaluate($cache->if, $cache->variables))) { throw new \TypeError(\sprintf('The value of the "$if" option of the "%s" attribute must evaluate to a boolean, "%s" given.', Cache::class, get_debug_type($if))); } $cache->if = $if; } if (!$cache->if) { return; } $response = null; if (null !== $cache->lastModified && !$cache->lastModified instanceof \DateTimeInterface) { $lastModified = $this->evaluate($cache->lastModified, $cache->variables); ($response ??= new Response())->setLastModified($lastModified); $cache->lastModified = $lastModified; } if (null !== $cache->etag) { $etag = hash('sha256', $this->evaluate($cache->etag, $cache->variables)); ($response ??= new Response())->setEtag($etag); $cache->etag = $etag; } if ($response?->isNotModified($request)) { $event->setController(static fn () => $response); $event->stopPropagation(); } } private function processAttributeAfterController(Cache $cache, Request $request, Response $response, ?bool &$hasVary = null, ?callable &$hasCacheControlDirective = null): void { if (!$cache->if) { return; } // Check if the response has a Vary header that should be considered, ignoring cases where // it's only 'Accept-Language' and the request has the '_vary_by_language' attribute $hasVary ??= ['Accept-Language'] === $response->getVary() ? !$request->attributes->get('_vary_by_language') : $response->hasVary(); // Check if cache-control directive was set manually in cacheControl (not auto computed) $hasCacheControlDirective ??= new class($response->headers) extends HeaderBag { public function __construct(private parent $headerBag) { } public function __invoke(string $key): bool { return \array_key_exists($key, $this->headerBag->cacheControl); } }; if (null !== $cache->lastModified && !$response->headers->has('Last-Modified')) { $response->setLastModified($cache->lastModified); } if (null !== $cache->etag && !$response->headers->has('ETag')) { $response->setEtag($cache->etag); } if (null !== $cache->smaxage && !$hasCacheControlDirective('s-maxage')) { $response->setSharedMaxAge($this->toSeconds($cache->smaxage)); } if ($cache->mustRevalidate) { $response->headers->addCacheControlDirective('must-revalidate'); } if (null !== $cache->maxage && !$hasCacheControlDirective('max-age')) { $response->setMaxAge($this->toSeconds($cache->maxage)); } if (null !== $cache->maxStale && !$hasCacheControlDirective('max-stale')) { $response->headers->addCacheControlDirective('max-stale', $this->toSeconds($cache->maxStale)); } if (null !== $cache->staleWhileRevalidate && !$hasCacheControlDirective('stale-while-revalidate')) { $response->headers->addCacheControlDirective('stale-while-revalidate', $this->toSeconds($cache->staleWhileRevalidate)); } if (null !== $cache->staleIfError && !$hasCacheControlDirective('stale-if-error')) { $response->headers->addCacheControlDirective('stale-if-error', $this->toSeconds($cache->staleIfError)); } if (null !== $cache->expires && !$response->headers->has('Expires')) { $response->setExpires(new \DateTimeImmutable('@'.strtotime($cache->expires, time()))); } if (!$hasVary && $cache->vary) { $response->setVary($cache->vary, false); } $hasPublicOrPrivateCacheControlDirective = \is_bool($cache->public) && ($hasCacheControlDirective('public') || $hasCacheControlDirective('private')); if (true === $cache->public && !$hasPublicOrPrivateCacheControlDirective) { $response->setPublic(); } if (false === $cache->public && !$hasPublicOrPrivateCacheControlDirective) { $response->setPrivate(); } if (true === $cache->noStore) { $response->headers->addCacheControlDirective('no-store'); } if (false === $cache->noStore) { $response->headers->removeCacheControlDirective('no-store'); } } private function getVariables(Cache $cache, Request $request, ControllerArgumentsEvent $event): ?array { if (\is_bool($cache->if) && null === $cache->lastModified && null === $cache->etag) { return null; } $controller = $event->getController(); $controller = match (true) { \is_object($controller) && !$controller instanceof \Closure => $controller, \is_array($controller) && \is_object($controller[0]) => $controller[0], default => null, }; return array_merge([ 'request' => $request, 'args' => $arguments = $event->getNamedArguments(), 'this' => $controller, ], $request->attributes->all(), $arguments); } private function evaluate(string|Expression|\Closure $closureOrExpression, array $variables): mixed { if ($closureOrExpression instanceof \Closure) { return $closureOrExpression($variables['args'], $variables['request'], $variables['this']); } return $this->getExpressionLanguage()->evaluate($closureOrExpression, $variables); } private function getExpressionLanguage(): ExpressionLanguage { return $this->expressionLanguage ??= class_exists(ExpressionLanguage::class) ? new ExpressionLanguage() : throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".'); } private function toSeconds(int|string $time): int { if (!is_numeric($time)) { $now = time(); $time = strtotime($time, $now) - $now; } return $time; } } ================================================ FILE: EventListener/ControllerAttributesListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerAttributeEvent; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\KernelEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; // Help opcache.preload discover always-needed symbols class_exists(ControllerAttributeEvent::class); class_exists(ExpressionLanguage::class); /** * Dispatches events for controller attributes. * * @author Nicolas Grekas */ class ControllerAttributesListener implements EventSubscriberInterface { /** * @param array> $attributesWithListenersByEvent */ public function __construct( private readonly array $attributesWithListenersByEvent, private ?ExpressionLanguage $expressionLanguage = null, ) { $this->expressionLanguage ??= class_exists(ExpressionLanguage::class, false) ? new ExpressionLanguage() : null; } private static array $attributeHierarchyCache = []; public function beforeController(ControllerEvent|ControllerArgumentsEvent $event, string $eventName, EventDispatcherInterface $dispatcher): void { $controller = $event->getController(); dispatch_attributes: foreach ($event->getAttributes('*') as $attribute) { if (!$attributeEventNames = $this->getAttributeEventNames($attribute, $eventName)) { continue; } foreach ($attributeEventNames as $attributeEventName) { $dispatcher->dispatch(new ControllerAttributeEvent($attribute, $event, $this->expressionLanguage), $attributeEventName); if ($event->isPropagationStopped()) { return; } } $c = $event->getController(); if ($c instanceof \Closure ? $c != $controller : $c !== $controller) { $controller = $c; goto dispatch_attributes; } } } public function afterController(KernelEvent $event, string $eventName, EventDispatcherInterface $dispatcher): void { $attributes = $event->controllerMetadata?->getAttributes('*') ?? []; for ($i = \count($attributes) - 1; $i >= 0; --$i) { $attribute = $attributes[$i]; $attributeEventNames = $this->getAttributeEventNames($attribute, $eventName); for ($j = \count($attributeEventNames) - 1; $j >= 0; --$j) { $dispatcher->dispatch(new ControllerAttributeEvent($attribute, $event, $this->expressionLanguage), $attributeEventNames[$j]); if ($event->isPropagationStopped()) { return; } } } } private function getAttributeEventNames(object $attribute, string $eventName): array { if (!$attributesWithListeners = $this->attributesWithListenersByEvent[$eventName] ?? []) { return []; } $names = []; $class = $attribute::class; $hierarchy = self::$attributeHierarchyCache[$class] ??= [$class => $class] + class_parents($class) + class_implements($class); foreach ($hierarchy as $class) { if (isset($attributesWithListeners[$class])) { $names[] = $eventName.'.'.$class; } } return $names; } public static function getSubscribedEvents(): array { return [ KernelEvents::CONTROLLER => ['beforeController', -10000], KernelEvents::CONTROLLER_ARGUMENTS => ['beforeController', -10000], KernelEvents::VIEW => ['afterController', 10000], KernelEvents::RESPONSE => ['afterController', 10000], KernelEvents::EXCEPTION => ['afterController', 10000], KernelEvents::FINISH_REQUEST => ['afterController', 10000], ]; } } ================================================ FILE: EventListener/DebugHandlersListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\KernelEvent; use Symfony\Component\HttpKernel\KernelEvents; /** * Sets an exception handler. * * @author Nicolas Grekas * * @final * * @internal */ class DebugHandlersListener implements EventSubscriberInterface { private string|object|null $earlyHandler; private ?\Closure $exceptionHandler; private bool $webMode; private bool $firstCall = true; private bool $hasTerminatedWithException = false; /** * @param callable|null $exceptionHandler A handler that must support \Throwable instances that will be called on Exception */ public function __construct(?callable $exceptionHandler = null, ?bool $webMode = null) { $handler = set_exception_handler('var_dump'); $this->earlyHandler = \is_array($handler) ? $handler[0] : null; restore_exception_handler(); $this->exceptionHandler = null === $exceptionHandler ? null : $exceptionHandler(...); $this->webMode = $webMode ?? !\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true); } /** * Configures the error handler. */ public function configure(?object $event = null): void { if ($event instanceof ConsoleEvent && $this->webMode) { return; } if (!$event instanceof KernelEvent ? !$this->firstCall : !$event->isMainRequest()) { return; } $this->firstCall = $this->hasTerminatedWithException = false; $hasRun = null; if (!$this->exceptionHandler) { if ($event instanceof KernelEvent) { if (method_exists($kernel = $event->getKernel(), 'terminateWithException')) { $request = $event->getRequest(); $hasRun = &$this->hasTerminatedWithException; $this->exceptionHandler = static function (\Throwable $e) use ($kernel, $request, &$hasRun) { if ($hasRun) { throw $e; } $hasRun = true; $kernel->terminateWithException($e, $request); }; } } elseif ($event instanceof ConsoleEvent && $app = $event->getCommand()->getApplication()) { $output = $event->getOutput(); if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $this->exceptionHandler = static function (\Throwable $e) use ($app, $output) { $app->renderThrowable($e, $output); }; } } if ($this->exceptionHandler) { $handler = set_exception_handler('var_dump'); $handler = \is_array($handler) ? $handler[0] : null; restore_exception_handler(); if (!$handler instanceof ErrorHandler) { $handler = $this->earlyHandler; } if ($handler instanceof ErrorHandler) { $handler->setExceptionHandler($this->exceptionHandler); if (null !== $hasRun) { $throwAt = $handler->throwAt(0) | \E_ERROR | \E_CORE_ERROR | \E_COMPILE_ERROR | \E_USER_ERROR | \E_RECOVERABLE_ERROR | \E_PARSE; $loggers = []; foreach ($handler->setLoggers([]) as $type => $log) { if ($type & $throwAt) { $loggers[$type] = [null, $log[1]]; } } // Assume $kernel->terminateWithException() will log uncaught exceptions appropriately $handler->setLoggers($loggers); } } $this->exceptionHandler = null; } } public static function getSubscribedEvents(): array { $events = [KernelEvents::REQUEST => ['configure', 2048]]; if (\defined('Symfony\Component\Console\ConsoleEvents::COMMAND')) { $events[ConsoleEvents::COMMAND] = ['configure', 2048]; } return $events; } } ================================================ FILE: EventListener/DisallowRobotsIndexingListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; /** * Ensures that the application is not indexed by search engines. * * @author Gary PEGEOT */ class DisallowRobotsIndexingListener implements EventSubscriberInterface { private const HEADER_NAME = 'X-Robots-Tag'; public function onResponse(ResponseEvent $event): void { if (!$event->getResponse()->headers->has(static::HEADER_NAME)) { $event->getResponse()->headers->set(static::HEADER_NAME, 'noindex'); } } public static function getSubscribedEvents(): array { return [ KernelEvents::RESPONSE => ['onResponse', -255], ]; } } ================================================ FILE: EventListener/DumpListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\VarDumper\Cloner\ClonerInterface; use Symfony\Component\VarDumper\Dumper\DataDumperInterface; use Symfony\Component\VarDumper\Server\Connection; use Symfony\Component\VarDumper\VarDumper; /** * Configures dump() handler. * * @author Nicolas Grekas */ class DumpListener implements EventSubscriberInterface { /** * @param ?DataDumperInterface $profilerDumper The dumper to use when CLI profiling is enabled. * If null, the default $dumper will be used instead. */ public function __construct( private ClonerInterface $cloner, private DataDumperInterface $dumper, private ?Connection $connection = null, private ?DataDumperInterface $profilerDumper = null, ) { } public function configure(?ConsoleCommandEvent $event = null): void { $input = $event?->getInput(); $cloner = $this->cloner; $dumper = !$this->profilerDumper || !$input?->hasOption('profile') || !$input?->getOption('profile') ? $this->dumper : $this->profilerDumper; $connection = $this->connection; VarDumper::setHandler(static function ($var, ?string $label = null) use ($cloner, $dumper, $connection) { $data = $cloner->cloneVar($var); if (null !== $label) { $data = $data->withContext(['label' => $label]); } if (!$connection || !$connection->write($data)) { $dumper->dump($data); } }); } public static function getSubscribedEvents(): array { if (!class_exists(ConsoleEvents::class)) { return []; } // Register early to have a working dump() as early as possible return [ConsoleEvents::COMMAND => ['configure', 1024]]; } } ================================================ FILE: EventListener/ErrorListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\WithHttpStatus; use Symfony\Component\HttpKernel\Attribute\WithLogLevel; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Log\DebugLoggerConfigurator; /** * @author Fabien Potencier */ class ErrorListener implements EventSubscriberInterface { /** * @param array|null, log_channel: string|null}> $exceptionsMapping */ public function __construct( protected string|object|array|null $controller, protected ?LoggerInterface $logger = null, protected bool $debug = false, protected array $exceptionsMapping = [], protected array $loggers = [], ) { } public function logKernelException(ExceptionEvent $event): void { $throwable = $event->getThrowable(); $logLevel = $this->resolveLogLevel($throwable); $logChannel = $this->resolveLogChannel($throwable); foreach ($this->exceptionsMapping as $class => $config) { if (!$throwable instanceof $class || !$config['status_code']) { continue; } if (!$throwable instanceof HttpExceptionInterface || $throwable->getStatusCode() !== $config['status_code']) { $headers = $throwable instanceof HttpExceptionInterface ? $throwable->getHeaders() : []; $throwable = HttpException::fromStatusCode($config['status_code'], $throwable->getMessage(), $throwable, $headers); $event->setThrowable($throwable); } break; } // There's no specific status code defined in the configuration for this exception if (!$throwable instanceof HttpExceptionInterface && $withHttpStatus = $this->getInheritedAttribute($throwable::class, WithHttpStatus::class)) { $throwable = HttpException::fromStatusCode($withHttpStatus->statusCode, $throwable->getMessage(), $throwable, $withHttpStatus->headers); $event->setThrowable($throwable); } $e = FlattenException::createFromThrowable($throwable); $this->logException($throwable, \sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), basename($e->getFile()), $e->getLine()), $logLevel, $logChannel); } public function onKernelException(ExceptionEvent $event): void { if (null === $this->controller) { return; } if (!$this->debug && $event->isKernelTerminating()) { return; } $throwable = $event->getThrowable(); $exceptionHandler = set_exception_handler('var_dump'); restore_exception_handler(); if (\is_array($exceptionHandler) && $exceptionHandler[0] instanceof ErrorHandler) { $throwable = $exceptionHandler[0]->enhanceError($event->getThrowable()); } $request = $this->duplicateRequest($throwable, $event->getRequest()); try { $response = $event->getKernel()->handle($request, HttpKernelInterface::SUB_REQUEST, false); } catch (\Exception $e) { $f = FlattenException::createFromThrowable($e); $this->logException($e, \sprintf('Exception thrown when handling an exception (%s: %s at %s line %s)', $f->getClass(), $f->getMessage(), basename($e->getFile()), $e->getLine())); $prev = $e; do { if ($throwable === $wrapper = $prev) { throw $e; } } while ($prev = $wrapper->getPrevious()); $prev = new \ReflectionProperty($wrapper instanceof \Exception ? \Exception::class : \Error::class, 'previous'); $prev->setValue($wrapper, $throwable); throw $e; } $event->setResponse($response); if ($this->debug) { $event->getRequest()->attributes->set('_remove_csp_headers', true); } } public function removeCspHeader(ResponseEvent $event): void { if ($this->debug && $event->getRequest()->attributes->get('_remove_csp_headers', false)) { $event->getResponse()->headers->remove('Content-Security-Policy'); } } public function onControllerArguments(ControllerArgumentsEvent $event): void { $e = $event->getRequest()->attributes->get('exception'); if (!$e instanceof \Throwable || false === $k = array_search($e, $event->getArguments(), true)) { return; } $r = new \ReflectionFunction($event->getController()(...)); $r = $r->getParameters()[$k] ?? null; if ($r && (!($r = $r->getType()) instanceof \ReflectionNamedType || FlattenException::class === $r->getName())) { $arguments = $event->getArguments(); $arguments[$k] = FlattenException::createFromThrowable($e); $event->setArguments($arguments); } } public static function getSubscribedEvents(): array { return [ KernelEvents::CONTROLLER_ARGUMENTS => 'onControllerArguments', KernelEvents::EXCEPTION => [ ['logKernelException', 0], ['onKernelException', -128], ], KernelEvents::RESPONSE => ['removeCspHeader', -128], ]; } protected function logException(\Throwable $exception, string $message, ?string $logLevel = null, ?string $logChannel = null): void { $logChannel ??= $this->resolveLogChannel($exception); $logLevel ??= $this->resolveLogLevel($exception); if (!$logger = $this->getLogger($logChannel)) { return; } $logger->log($logLevel, $message, ['exception' => $exception]); } /** * Resolves the level to be used when logging the exception. */ private function resolveLogLevel(\Throwable $throwable): string { foreach ($this->exceptionsMapping as $class => $config) { if ($throwable instanceof $class && $config['log_level']) { return $config['log_level']; } } if ($withLogLevel = $this->getInheritedAttribute($throwable::class, WithLogLevel::class)) { return $withLogLevel->level; } if (!$throwable instanceof HttpExceptionInterface || $throwable->getStatusCode() >= 500) { return LogLevel::CRITICAL; } return LogLevel::ERROR; } private function resolveLogChannel(\Throwable $throwable): ?string { foreach ($this->exceptionsMapping as $class => $config) { if ($throwable instanceof $class && isset($config['log_channel'])) { return $config['log_channel']; } } return null; } /** * Clones the request for the exception. */ protected function duplicateRequest(\Throwable $exception, Request $request): Request { $attributes = [ '_controller' => $this->controller, 'exception' => $exception, 'logger' => DebugLoggerConfigurator::getDebugLogger($this->getLogger($this->resolveLogChannel($exception))), ]; $request = $request->duplicate(null, null, $attributes); $request->setMethod('GET'); return $request; } /** * @template T * * @param class-string $attribute * * @return T|null */ private function getInheritedAttribute(string $class, string $attribute): ?object { $class = new \ReflectionClass($class); $interfaces = []; $attributeReflector = null; $parentInterfaces = []; $ownInterfaces = []; do { if ($attributes = $class->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { $attributeReflector = $attributes[0]; $parentInterfaces = class_implements($class->name); break; } $interfaces[] = class_implements($class->name); } while ($class = $class->getParentClass()); while ($interfaces) { $ownInterfaces = array_diff_key(array_pop($interfaces), $parentInterfaces); $parentInterfaces += $ownInterfaces; foreach ($ownInterfaces as $interface) { $class = new \ReflectionClass($interface); if ($attributes = $class->getAttributes($attribute, \ReflectionAttribute::IS_INSTANCEOF)) { $attributeReflector = $attributes[0]; } } } return $attributeReflector?->newInstance(); } private function getLogger(?string $logChannel): ?LoggerInterface { return $logChannel ? $this->loggers[$logChannel] ?? $this->logger : $this->logger; } } ================================================ FILE: EventListener/FragmentListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\KernelEvents; /** * Handles content fragments represented by special URIs. * * All URL paths starting with /_fragment are handled as * content fragments by this listener. * * Throws an AccessDeniedHttpException exception if the request * is not signed or if it is not an internal sub-request. * * @author Fabien Potencier * * @final */ class FragmentListener implements EventSubscriberInterface { /** * @param string $fragmentPath The path that triggers this listener */ public function __construct( private UriSigner $signer, private string $fragmentPath = '/_fragment', ) { } /** * Fixes request attributes when the path is '/_fragment'. * * @throws AccessDeniedHttpException if the request does not come from a trusted IP */ public function onKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); if ($this->fragmentPath !== rawurldecode($request->getPathInfo())) { return; } if ($request->attributes->has('_controller')) { // Is a sub-request: no need to parse _path but it should still be removed from query parameters as below. $request->query->remove('_path'); return; } if ($event->isMainRequest()) { $this->validateRequest($request); } parse_str($request->query->get('_path', ''), $attributes); $attributes['_check_controller_is_allowed'] = true; $request->attributes->add($attributes); $request->attributes->set('_route_params', array_replace($request->attributes->get('_route_params', []), $attributes)); $request->query->remove('_path'); } protected function validateRequest(Request $request): void { // is the Request safe? if (!$request->isMethodSafe()) { throw new AccessDeniedHttpException(); } // is the Request signed? if ($this->signer->checkRequest($request)) { return; } throw new AccessDeniedHttpException(); } public static function getSubscribedEvents(): array { return [ KernelEvents::REQUEST => [['onKernelRequest', 48]], ]; } } ================================================ FILE: EventListener/IsSignatureValidAttributeListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\HttpKernel\Attribute\IsSignatureValid; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerAttributeEvent; use Symfony\Component\HttpKernel\KernelEvents; /** * Handles the IsSignatureValid attribute. * * @author Santiago San Martin */ class IsSignatureValidAttributeListener implements EventSubscriberInterface { public function __construct( private readonly UriSigner $uriSigner, ) { } public function onKernelControllerAttribute(ControllerAttributeEvent $event): void { $kernelEvent = $event->kernelEvent; if (!$kernelEvent instanceof ControllerArgumentsEvent) { return; } $this->processAttribute($event->attribute, $kernelEvent->getRequest()); } /** * @internal since Symfony 8.1, use onKernelControllerAttribute() instead */ public function onKernelControllerArguments(ControllerArgumentsEvent $event): void { $request = $event->getRequest(); foreach ($event->getAttributes(IsSignatureValid::class) as $attribute) { $this->processAttribute($attribute, $request); } } private function processAttribute(IsSignatureValid $attribute, Request $request): void { $methods = array_map('strtoupper', $attribute->methods); if ($methods && !\in_array($request->getMethod(), $methods, true)) { return; } $this->uriSigner->verify($request); } public static function getSubscribedEvents(): array { if (!class_exists(ControllerAttributesListener::class, false)) { return [ KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 30], ]; } return [ KernelEvents::CONTROLLER_ARGUMENTS.'.'.IsSignatureValid::class => 'onKernelControllerAttribute', ]; } } ================================================ FILE: EventListener/LocaleAwareListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Contracts\Translation\LocaleAwareInterface; /** * Pass the current locale to the provided services. * * @author Pierre Bobiet */ class LocaleAwareListener implements EventSubscriberInterface { /** * @param iterable $localeAwareServices */ public function __construct( private iterable $localeAwareServices, private RequestStack $requestStack, ) { } public function onKernelRequest(RequestEvent $event): void { $this->setLocale($event->getRequest()->getLocale(), $event->getRequest()->getDefaultLocale()); } public function onKernelFinishRequest(FinishRequestEvent $event): void { if (null === $parentRequest = $this->requestStack->getParentRequest()) { foreach ($this->localeAwareServices as $service) { $service->setLocale($event->getRequest()->getDefaultLocale()); } return; } $this->setLocale($parentRequest->getLocale(), $parentRequest->getDefaultLocale()); } public static function getSubscribedEvents(): array { return [ // must be registered after the Locale listener KernelEvents::REQUEST => [['onKernelRequest', 15]], KernelEvents::FINISH_REQUEST => [['onKernelFinishRequest', -15]], ]; } private function setLocale(string $locale, string $defaultLocale): void { foreach ($this->localeAwareServices as $service) { try { $service->setLocale($locale); } catch (\InvalidArgumentException) { $service->setLocale($defaultLocale); } } } } ================================================ FILE: EventListener/LocaleListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\KernelEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Routing\RequestContextAwareInterface; /** * Initializes the locale based on the current request. * * @author Fabien Potencier * * @final */ class LocaleListener implements EventSubscriberInterface { public function __construct( private RequestStack $requestStack, private string $defaultLocale = 'en', private ?RequestContextAwareInterface $router = null, private bool $useAcceptLanguageHeader = false, private array $enabledLocales = [], ) { $this->enabledLocales = $enabledLocales ? array_values(array_unique(array_merge([$defaultLocale], array_filter($enabledLocales)))) : []; } public function setDefaultLocale(KernelEvent $event): void { $event->getRequest()->setDefaultLocale($this->defaultLocale); $this->setRouterLocale($this->defaultLocale); } public function onKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $this->setLocale($request); $this->setRouterLocale($request->getLocale()); } public function onKernelFinishRequest(FinishRequestEvent $event): void { $this->setRouterLocale($this->requestStack->getParentRequest()?->getLocale() ?? $this->defaultLocale); } private function setLocale(Request $request): void { if ($locale = $request->attributes->get('_locale')) { $request->setLocale($locale); } elseif ($this->useAcceptLanguageHeader) { if ($request->getLanguages() && $preferredLanguage = $request->getPreferredLanguage($this->enabledLocales)) { $request->setLocale($preferredLanguage); } $request->attributes->set('_vary_by_language', true); } } private function setRouterLocale(string $locale): void { $this->router?->getContext()->setParameter('_locale', $locale); } public static function getSubscribedEvents(): array { return [ KernelEvents::REQUEST => [ ['setDefaultLocale', 100], // must be registered after the Router to have access to the _locale ['onKernelRequest', 16], ], KernelEvents::FINISH_REQUEST => [['onKernelFinishRequest', 0]], ]; } } ================================================ FILE: EventListener/ProfilerListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestMatcherInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Profiler\Profile; use Symfony\Component\HttpKernel\Profiler\Profiler; /** * ProfilerListener collects data for the current request by listening to the kernel events. * * @author Fabien Potencier * * @final */ class ProfilerListener implements EventSubscriberInterface { private ?\Throwable $exception = null; /** @var \SplObjectStorage */ private \SplObjectStorage $profiles; /** @var \SplObjectStorage */ private \SplObjectStorage $parents; /** * @param bool $onlyException True if the profiler only collects data when an exception occurs, false otherwise * @param bool $onlyMainRequests True if the profiler only collects data when the request is the main request, false otherwise */ public function __construct( private Profiler $profiler, private RequestStack $requestStack, private ?RequestMatcherInterface $matcher = null, private bool $onlyException = false, private bool $onlyMainRequests = false, private ?string $collectParameter = null, ) { $this->profiles = new \SplObjectStorage(); $this->parents = new \SplObjectStorage(); } /** * Handles the onKernelException event. */ public function onKernelException(ExceptionEvent $event): void { if ($this->onlyMainRequests && !$event->isMainRequest()) { return; } $this->exception = $event->getThrowable(); } /** * Handles the onKernelResponse event. */ public function onKernelResponse(ResponseEvent $event): void { if ($this->onlyMainRequests && !$event->isMainRequest()) { return; } if ($this->onlyException && null === $this->exception) { return; } $request = $event->getRequest(); if (null !== $this->collectParameter && null !== $collectParameterValue = $request->attributes->get($this->collectParameter) ?? $request->query->get($this->collectParameter) ?? $request->request->get($this->collectParameter)) { filter_var($collectParameterValue, \FILTER_VALIDATE_BOOL) ? $this->profiler->enable() : $this->profiler->disable(); } $exception = $this->exception; $this->exception = null; if (null !== $this->matcher && !$this->matcher->matches($request)) { return; } $session = !$request->attributes->getBoolean('_stateless') && $request->hasPreviousSession() ? $request->getSession() : null; if ($session instanceof Session) { $usageIndexValue = $usageIndexReference = &$session->getUsageIndex(); $usageIndexReference = \PHP_INT_MIN; } try { if (!$profile = $this->profiler->collect($request, $event->getResponse(), $exception)) { return; } } finally { if ($session instanceof Session) { $usageIndexReference = $usageIndexValue; } } $this->profiles[$request] = $profile; $this->parents[$request] = $this->requestStack->getParentRequest(); } public function onKernelTerminate(TerminateEvent $event): void { // attach children to parents foreach ($this->profiles as $request) { if (null !== $parentRequest = $this->parents[$request]) { if (isset($this->profiles[$parentRequest])) { $this->profiles[$parentRequest]->addChild($this->profiles[$request]); } } } // save profiles foreach ($this->profiles as $request) { $this->profiler->saveProfile($this->profiles[$request]); } $this->reset(); } public function reset(): void { $this->profiles = new \SplObjectStorage(); $this->parents = new \SplObjectStorage(); $this->exception = null; } public static function getSubscribedEvents(): array { return [ KernelEvents::RESPONSE => ['onKernelResponse', -100], KernelEvents::EXCEPTION => ['onKernelException', 0], KernelEvents::TERMINATE => ['onKernelTerminate', -1024], ]; } } ================================================ FILE: EventListener/ResponseListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; /** * ResponseListener fixes the Response headers based on the Request. * * @author Fabien Potencier * * @final */ class ResponseListener implements EventSubscriberInterface { public function __construct( private string $charset, private bool $addContentLanguageHeader = false, ) { } /** * Filters the Response. */ public function onKernelResponse(ResponseEvent $event): void { if (!$event->isMainRequest()) { return; } $response = $event->getResponse(); if (null === $response->getCharset()) { $response->setCharset($this->charset); } if ($this->addContentLanguageHeader && !$response->isInformational() && !$response->isEmpty() && !$response->headers->has('Content-Language')) { $response->headers->set('Content-Language', $event->getRequest()->getLocale()); } if ($event->getRequest()->attributes->get('_vary_by_language')) { $response->setVary('Accept-Language', false); } $response->prepare($event->getRequest()); } public static function getSubscribedEvents(): array { return [ KernelEvents::RESPONSE => 'onKernelResponse', ]; } } ================================================ FILE: EventListener/RouterListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\NoConfigurationException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Matcher\RequestMatcherInterface; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RequestContextAwareInterface; /** * Initializes the context from the request and sets request attributes based on a matching route. * * @author Fabien Potencier * @author Yonel Ceruto * * @final */ class RouterListener implements EventSubscriberInterface { private RequestContext $context; /** * @param RequestContext|null $context The RequestContext (can be null when $matcher implements RequestContextAwareInterface) * * @throws \InvalidArgumentException */ public function __construct( private UrlMatcherInterface|RequestMatcherInterface $matcher, private RequestStack $requestStack, ?RequestContext $context = null, private ?LoggerInterface $logger = null, private ?string $projectDir = null, private bool $debug = true, ) { if (null === $context && !$matcher instanceof RequestContextAwareInterface) { throw new \InvalidArgumentException('You must either pass a RequestContext or the matcher must implement RequestContextAwareInterface.'); } $this->context = $context ?? $matcher->getContext(); } private function setCurrentRequest(?Request $request): void { if (null !== $request) { try { $this->context->fromRequest($request); } catch (\UnexpectedValueException $e) { throw new BadRequestHttpException($e->getMessage(), $e, $e->getCode()); } } } /** * After a sub-request is done, we need to reset the routing context to the parent request so that the URL generator * operates on the correct context again. */ public function onKernelFinishRequest(): void { $this->setCurrentRequest($this->requestStack->getParentRequest()); } public function onKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $this->setCurrentRequest($request); if ($request->attributes->has('_controller')) { // routing is already done return; } // add attributes based on the request (routing) try { // matching a request is more powerful than matching a URL path + context, so try that first if ($this->matcher instanceof RequestMatcherInterface) { $parameters = $this->matcher->matchRequest($request); } else { $parameters = $this->matcher->match($request->getPathInfo()); } $this->logger?->info('Matched route "{route}".', [ 'route' => $parameters['_route'] ?? 'n/a', 'route_parameters' => $parameters, 'request_uri' => $request->getUri(), 'method' => $request->getMethod(), ]); $attributes = $parameters; if ($mapping = $parameters['_route_mapping'] ?? false) { unset($parameters['_route_mapping']); $mappedAttributes = []; $attributes = []; foreach ($parameters as $parameter => $value) { if (!isset($mapping[$parameter])) { $attribute = $parameter; } elseif (\is_array($mapping[$parameter])) { [$attribute, $parameter] = $mapping[$parameter]; $mappedAttributes[$attribute] = ''; } else { $attribute = $mapping[$parameter]; } if (!isset($mappedAttributes[$attribute])) { $attributes[$attribute] = $value; $mappedAttributes[$attribute] = $parameter; } elseif ('' !== $mappedAttributes[$attribute]) { $attributes[$attribute] = [ $mappedAttributes[$attribute] => $attributes[$attribute], $parameter => $value, ]; $mappedAttributes[$attribute] = ''; } else { $attributes[$attribute][$parameter] = $value; } } $attributes['_route_mapping'] = $mapping; } $request->attributes->add($attributes); unset($parameters['_route'], $parameters['_controller']); $request->attributes->set('_route_params', $parameters); } catch (ResourceNotFoundException $e) { $message = \sprintf('No route found for "%s %s"', $request->getMethod(), $request->getUriForPath($request->getPathInfo())); if ($referer = $request->headers->get('referer')) { $message .= \sprintf(' (from "%s")', $referer); } throw new NotFoundHttpException($message, $e); } catch (MethodNotAllowedException $e) { $message = \sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', $request->getMethod(), $request->getUriForPath($request->getPathInfo()), implode(', ', $e->getAllowedMethods())); throw new MethodNotAllowedHttpException($e->getAllowedMethods(), $message, $e); } } public function onKernelException(ExceptionEvent $event): void { if (!$this->debug || !($e = $event->getThrowable()) instanceof NotFoundHttpException) { return; } if ($e->getPrevious() instanceof NoConfigurationException) { $event->setResponse($this->createWelcomeResponse()); } } public static function getSubscribedEvents(): array { return [ KernelEvents::REQUEST => [['onKernelRequest', 32]], KernelEvents::FINISH_REQUEST => [['onKernelFinishRequest', 0]], KernelEvents::EXCEPTION => ['onKernelException', -64], ]; } private function createWelcomeResponse(): Response { $version = Kernel::VERSION; $projectDir = realpath((string) $this->projectDir).\DIRECTORY_SEPARATOR; $docVersion = substr(Kernel::VERSION, 0, 3); ob_start(); include \dirname(__DIR__).'/Resources/welcome.html.php'; return new Response(ob_get_clean(), Response::HTTP_NOT_FOUND); } } ================================================ FILE: EventListener/SerializeControllerResultAttributeListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\Serialize; use Symfony\Component\HttpKernel\Event\ControllerAttributeEvent; use Symfony\Component\HttpKernel\Event\ViewEvent; use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Serializer\Exception\UnsupportedFormatException; use Symfony\Component\Serializer\SerializerInterface; /** * @author Konstantin Myakshin */ final class SerializeControllerResultAttributeListener implements EventSubscriberInterface { public function __construct(private readonly ?SerializerInterface $serializer) { } /** * @param ControllerAttributeEvent $event */ public function onView(ControllerAttributeEvent $event): void { $kernelEvent = $event->kernelEvent; if (!$kernelEvent instanceof ViewEvent) { return; } if (!$this->serializer) { throw new \LogicException(\sprintf('The "symfony/serializer" component is required to use the "#[%s]" attribute. Try running "composer require symfony/serializer".', Serialize::class)); } $request = $kernelEvent->getRequest(); $controllerResult = $kernelEvent->getControllerResult(); $format = $request->getRequestFormat('json'); try { $data = $this->serializer->serialize($controllerResult, $format, $event->attribute->context); } catch (UnsupportedFormatException $exception) { throw new UnsupportedMediaTypeHttpException(\sprintf('Unsupported format "%s".', $format), $exception->getPrevious()); } $headers = $this->mergeHeaders($event->attribute, $request, $format); $response = new Response($data, $event->attribute->code, $headers); $kernelEvent->setResponse($response); } public static function getSubscribedEvents(): array { return [ KernelEvents::VIEW.'.'.Serialize::class => 'onView', ]; } /** * @return array */ private function mergeHeaders(Serialize $attribute, Request $request, string $format): array { $headers = array_combine( array_map('strtolower', array_keys($attribute->headers)), array_values($attribute->headers), ); if (!isset($headers['content-type'])) { $headers['content-type'] = $request->getMimeType($format); } return $headers; } } ================================================ FILE: EventListener/SessionListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; /** * Sets the session in the request. * * @author Fabien Potencier * * @final */ class SessionListener extends AbstractSessionListener { public function __construct( private ?ContainerInterface $container = null, bool $debug = false, array $sessionOptions = [], ) { parent::__construct($container, $debug, $sessionOptions); } protected function getSession(): ?SessionInterface { if ($this->container->has('session_factory')) { return $this->container->get('session_factory')->createSession(); } return null; } } ================================================ FILE: EventListener/SurrogateListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\HttpCache\HttpCache; use Symfony\Component\HttpKernel\HttpCache\SurrogateInterface; use Symfony\Component\HttpKernel\KernelEvents; /** * SurrogateListener adds a Surrogate-Control HTTP header when the Response needs to be parsed for Surrogates. * * @author Fabien Potencier * * @final */ class SurrogateListener implements EventSubscriberInterface { public function __construct( private ?SurrogateInterface $surrogate = null, ) { } /** * Filters the Response. */ public function onKernelResponse(ResponseEvent $event): void { if (!$event->isMainRequest()) { return; } $kernel = $event->getKernel(); $surrogate = $this->surrogate; if ($kernel instanceof HttpCache) { $surrogate = $kernel->getSurrogate(); if (null !== $this->surrogate && $this->surrogate->getName() !== $surrogate->getName()) { $surrogate = $this->surrogate; } } if (null === $surrogate) { return; } $surrogate->addSurrogateControl($event->getResponse()); } public static function getSubscribedEvents(): array { return [ KernelEvents::RESPONSE => 'onKernelResponse', ]; } } ================================================ FILE: EventListener/ValidateRequestListener.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; /** * Validates Requests. * * @author Magnus Nordlander * * @final */ class ValidateRequestListener implements EventSubscriberInterface { /** * Performs the validation. */ public function onKernelRequest(RequestEvent $event): void { if (!$event->isMainRequest()) { return; } $request = $event->getRequest(); if ($request::getTrustedProxies()) { $request->getClientIps(); } $request->getHost(); } public static function getSubscribedEvents(): array { return [ KernelEvents::REQUEST => [ ['onKernelRequest', 256], ], ]; } } ================================================ FILE: Exception/AccessDeniedHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Fabien Potencier * @author Christophe Coevoet */ class AccessDeniedHttpException extends HttpException { public function __construct(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(403, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/BadRequestHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Ben Ramsey */ class BadRequestHttpException extends HttpException { public function __construct(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(400, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/ConflictHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Ben Ramsey */ class ConflictHttpException extends HttpException { public function __construct(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(409, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/ControllerDoesNotReturnResponseException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Grégoire Pineau */ class ControllerDoesNotReturnResponseException extends \LogicException { public function __construct(string $message, callable $controller, string $file, int $line) { parent::__construct($message); if (!$controllerDefinition = $this->parseControllerDefinition($controller)) { return; } $this->file = $controllerDefinition['file']; $this->line = $controllerDefinition['line']; $r = new \ReflectionProperty(\Exception::class, 'trace'); $r->setValue($this, array_merge([ [ 'line' => $line, 'file' => $file, ], ], $this->getTrace())); } private function parseControllerDefinition(callable $controller): ?array { if (\is_string($controller) && str_contains($controller, '::')) { $controller = explode('::', $controller); } if (\is_array($controller)) { try { $r = new \ReflectionMethod($controller[0], $controller[1]); return [ 'file' => $r->getFileName(), 'line' => $r->getEndLine(), ]; } catch (\ReflectionException) { return null; } } if ($controller instanceof \Closure) { $r = new \ReflectionFunction($controller); return [ 'file' => $r->getFileName(), 'line' => $r->getEndLine(), ]; } if (\is_object($controller)) { $r = new \ReflectionClass($controller); try { $line = $r->getMethod('__invoke')->getEndLine(); } catch (\ReflectionException) { $line = $r->getEndLine(); } return [ 'file' => $r->getFileName(), 'line' => $line, ]; } return null; } } ================================================ FILE: Exception/GoneHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Ben Ramsey */ class GoneHttpException extends HttpException { public function __construct(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(410, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/HttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * HttpException. * * @author Kris Wallsmith */ class HttpException extends \RuntimeException implements HttpExceptionInterface { public function __construct( private int $statusCode, string $message = '', ?\Throwable $previous = null, private array $headers = [], int $code = 0, ) { parent::__construct($message, $code, $previous); } public static function fromStatusCode(int $statusCode, string $message = '', ?\Throwable $previous = null, array $headers = [], int $code = 0): self { return match ($statusCode) { 400 => new BadRequestHttpException($message, $previous, $code, $headers), 403 => new AccessDeniedHttpException($message, $previous, $code, $headers), 404 => new NotFoundHttpException($message, $previous, $code, $headers), 406 => new NotAcceptableHttpException($message, $previous, $code, $headers), 409 => new ConflictHttpException($message, $previous, $code, $headers), 410 => new GoneHttpException($message, $previous, $code, $headers), 411 => new LengthRequiredHttpException($message, $previous, $code, $headers), 412 => new PreconditionFailedHttpException($message, $previous, $code, $headers), 423 => new LockedHttpException($message, $previous, $code, $headers), 415 => new UnsupportedMediaTypeHttpException($message, $previous, $code, $headers), 422 => new UnprocessableEntityHttpException($message, $previous, $code, $headers), 428 => new PreconditionRequiredHttpException($message, $previous, $code, $headers), 429 => new TooManyRequestsHttpException(null, $message, $previous, $code, $headers), 503 => new ServiceUnavailableHttpException(null, $message, $previous, $code, $headers), default => new static($statusCode, $message, $previous, $headers, $code), }; } public function getStatusCode(): int { return $this->statusCode; } public function getHeaders(): array { return $this->headers; } public function setHeaders(array $headers): void { $this->headers = $headers; } } ================================================ FILE: Exception/HttpExceptionInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * Interface for HTTP error exceptions. * * @author Kris Wallsmith */ interface HttpExceptionInterface extends \Throwable { /** * Returns the status code. */ public function getStatusCode(): int; /** * Returns response headers. */ public function getHeaders(): array; } ================================================ FILE: Exception/InvalidMetadataException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; class InvalidMetadataException extends \LogicException { } ================================================ FILE: Exception/LengthRequiredHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Ben Ramsey */ class LengthRequiredHttpException extends HttpException { public function __construct(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(411, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/LockedHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Peter Dietrich */ class LockedHttpException extends HttpException { public function __construct(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(423, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/MethodNotAllowedHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Kris Wallsmith */ class MethodNotAllowedHttpException extends HttpException { /** * @param string[] $allow An array of allowed methods */ public function __construct(array $allow, string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { $headers['Allow'] = strtoupper(implode(', ', $allow)); parent::__construct(405, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/NearMissValueResolverException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * Lets value resolvers tell when an argument could be under their watch but failed to be resolved. * * Throwing this exception inside `ValueResolverInterface::resolve` does not interrupt the value resolvers chain. */ class NearMissValueResolverException extends \RuntimeException { } ================================================ FILE: Exception/NotAcceptableHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Ben Ramsey */ class NotAcceptableHttpException extends HttpException { public function __construct(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(406, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/NotFoundHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Fabien Potencier */ class NotFoundHttpException extends HttpException { public function __construct(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(404, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/PreconditionFailedHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Ben Ramsey */ class PreconditionFailedHttpException extends HttpException { public function __construct(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(412, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/PreconditionRequiredHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Ben Ramsey * * @see http://tools.ietf.org/html/rfc6585 */ class PreconditionRequiredHttpException extends HttpException { public function __construct(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(428, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/ResolverNotFoundException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; class ResolverNotFoundException extends \RuntimeException { /** * @param string[] $alternatives */ public function __construct(string $name, array $alternatives = []) { $msg = \sprintf('You have requested a non-existent resolver "%s".', $name); if ($alternatives) { if (1 === \count($alternatives)) { $msg .= ' Did you mean this: "'; } else { $msg .= ' Did you mean one of these: "'; } $msg .= implode('", "', $alternatives).'"?'; } parent::__construct($msg); } } ================================================ FILE: Exception/ServiceUnavailableHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Ben Ramsey */ class ServiceUnavailableHttpException extends HttpException { /** * @param int|string|null $retryAfter The number of seconds or HTTP-date after which the request may be retried */ public function __construct(int|string|null $retryAfter = null, string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { if ($retryAfter) { $headers['Retry-After'] = $retryAfter; } parent::__construct(503, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/TooManyRequestsHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Ben Ramsey * * @see http://tools.ietf.org/html/rfc6585 */ class TooManyRequestsHttpException extends HttpException { /** * @param int|string|null $retryAfter The number of seconds or HTTP-date after which the request may be retried */ public function __construct(int|string|null $retryAfter = null, string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { if ($retryAfter) { $headers['Retry-After'] = $retryAfter; } parent::__construct(429, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/UnauthorizedHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Ben Ramsey */ class UnauthorizedHttpException extends HttpException { /** * @param string $challenge WWW-Authenticate challenge string */ public function __construct(string $challenge, string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { $headers['WWW-Authenticate'] = $challenge; parent::__construct(401, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/UnexpectedSessionUsageException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Mathias Arlaud */ class UnexpectedSessionUsageException extends \LogicException { } ================================================ FILE: Exception/UnprocessableEntityHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Steve Hutchins */ class UnprocessableEntityHttpException extends HttpException { public function __construct(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(422, $message, $previous, $headers, $code); } } ================================================ FILE: Exception/UnsupportedMediaTypeHttpException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * @author Ben Ramsey */ class UnsupportedMediaTypeHttpException extends HttpException { public function __construct(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []) { parent::__construct(415, $message, $previous, $headers, $code); } } ================================================ FILE: Fragment/AbstractSurrogateFragmentRenderer.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Fragment; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\HttpCache\SurrogateInterface; /** * Implements Surrogate rendering strategy. * * @author Fabien Potencier */ abstract class AbstractSurrogateFragmentRenderer extends RoutableFragmentRenderer { /** * The "fallback" strategy when surrogate is not available should always be an * instance of InlineFragmentRenderer. * * @param FragmentRendererInterface $inlineStrategy The inline strategy to use when the surrogate is not supported */ public function __construct( private ?SurrogateInterface $surrogate, private FragmentRendererInterface $inlineStrategy, private ?UriSigner $signer = null, ) { } /** * Note that if the current Request has no surrogate capability, this method * falls back to use the inline rendering strategy. * * Additional available options: * * * alt: an alternative URI to render in case of an error * * comment: a comment to add when returning the surrogate tag * * absolute_uri: whether to generate an absolute URI or not. Default is false * * Note, that not all surrogate strategies support all options. For now * 'alt' and 'comment' are only supported by ESI. * * @see Symfony\Component\HttpKernel\HttpCache\SurrogateInterface */ public function render(string|ControllerReference $uri, Request $request, array $options = []): Response { if (!$this->surrogate || !$this->surrogate->hasSurrogateCapability($request)) { $request->attributes->set('_check_controller_is_allowed', true); if ($uri instanceof ControllerReference && $this->containsNonScalars($uri->attributes)) { throw new \InvalidArgumentException('Passing non-scalar values as part of URI attributes to the ESI and SSI rendering strategies is not supported. Use a different rendering strategy or pass scalar values.'); } return $this->inlineStrategy->render($uri, $request, $options); } $absolute = $options['absolute_uri'] ?? false; if ($uri instanceof ControllerReference) { $uri = $this->generateSignedFragmentUri($uri, $request, $absolute); } $alt = $options['alt'] ?? null; if ($alt instanceof ControllerReference) { $alt = $this->generateSignedFragmentUri($alt, $request, $absolute); } $tag = $this->surrogate->renderIncludeTag($uri, $alt, $options['ignore_errors'] ?? false, $options['comment'] ?? ''); return new Response($tag); } private function generateSignedFragmentUri(ControllerReference $uri, Request $request, bool $absolute): string { return (new FragmentUriGenerator($this->fragmentPath, $this->signer))->generate($uri, $request, $absolute); } private function containsNonScalars(array $values): bool { foreach ($values as $value) { if (\is_scalar($value) || null === $value) { continue; } if (!\is_array($value) || $this->containsNonScalars($value)) { return true; } } return false; } } ================================================ FILE: Fragment/EsiFragmentRenderer.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Fragment; /** * Implements the ESI rendering strategy. * * @author Fabien Potencier */ class EsiFragmentRenderer extends AbstractSurrogateFragmentRenderer { public function getName(): string { return 'esi'; } } ================================================ FILE: Fragment/FragmentHandler.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Fragment; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Exception\HttpException; /** * Renders a URI that represents a resource fragment. * * This class handles the rendering of resource fragments that are included into * a main resource. The handling of the rendering is managed by specialized renderers. * * @author Fabien Potencier * * @see FragmentRendererInterface */ class FragmentHandler { /** @var array */ private array $renderers = []; /** * @param FragmentRendererInterface[] $renderers An array of FragmentRendererInterface instances * @param bool $debug Whether the debug mode is enabled or not */ public function __construct( private RequestStack $requestStack, array $renderers = [], private bool $debug = false, ) { foreach ($renderers as $renderer) { $this->addRenderer($renderer); } } /** * Adds a renderer. */ public function addRenderer(FragmentRendererInterface $renderer): void { $this->renderers[$renderer->getName()] = $renderer; } /** * Renders a URI and returns the Response content. * * Available options: * * * ignore_errors: true to return an empty string in case of an error * * @throws \InvalidArgumentException when the renderer does not exist * @throws \LogicException when no main request is being handled */ public function render(string|ControllerReference $uri, string $renderer = 'inline', array $options = []): ?string { if (!isset($options['ignore_errors'])) { $options['ignore_errors'] = !$this->debug; } if (!isset($this->renderers[$renderer])) { throw new \InvalidArgumentException(\sprintf('The "%s" renderer does not exist.', $renderer)); } if (!$request = $this->requestStack->getCurrentRequest()) { throw new \LogicException('Rendering a fragment can only be done when handling a Request.'); } return $this->deliver($this->renderers[$renderer]->render($uri, $request, $options)); } /** * Delivers the Response as a string. * * When the Response is a StreamedResponse, the content is streamed immediately * instead of being returned. * * @return string|null The Response content or null when the Response is streamed * * @throws \RuntimeException when the Response is not successful */ protected function deliver(Response $response): ?string { if (!$response->isSuccessful()) { $responseStatusCode = $response->getStatusCode(); throw new \RuntimeException(\sprintf('Error when rendering "%s" (Status code is %d).', $this->requestStack->getCurrentRequest()->getUri(), $responseStatusCode), 0, new HttpException($responseStatusCode)); } if (!$response instanceof StreamedResponse) { return $response->getContent(); } $response->sendContent(); return null; } } ================================================ FILE: Fragment/FragmentRendererInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Fragment; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ControllerReference; /** * Interface implemented by all rendering strategies. * * @author Fabien Potencier */ interface FragmentRendererInterface { /** * Renders a URI and returns the Response content. */ public function render(string|ControllerReference $uri, Request $request, array $options = []): Response; /** * Gets the name of the strategy. */ public function getName(): string; } ================================================ FILE: Fragment/FragmentUriGenerator.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Fragment; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\HttpKernel\Controller\ControllerReference; /** * Generates a fragment URI. * * @author Kévin Dunglas * @author Fabien Potencier */ final class FragmentUriGenerator implements FragmentUriGeneratorInterface { public function __construct( private string $fragmentPath, private ?UriSigner $signer = null, private ?RequestStack $requestStack = null, ) { } public function generate(ControllerReference $controller, ?Request $request = null, bool $absolute = false, bool $strict = true, bool $sign = true): string { if (null === $request && (null === $this->requestStack || null === $request = $this->requestStack->getCurrentRequest())) { throw new \LogicException('Generating a fragment URL can only be done when handling a Request.'); } if ($sign && null === $this->signer) { throw new \LogicException('You must use a URI when using the ESI rendering strategy or set a URL signer.'); } if ($strict) { $this->checkNonScalar($controller->attributes); } // We need to forward the current _format and _locale values as we don't have // a proper routing pattern to do the job for us. // This makes things inconsistent if you switch from rendering a controller // to rendering a route if the route pattern does not contain the special // _format and _locale placeholders. if (!isset($controller->attributes['_format'])) { $controller->attributes['_format'] = $request->getRequestFormat(); } if (!isset($controller->attributes['_locale'])) { $controller->attributes['_locale'] = $request->getLocale(); } $controller->attributes['_controller'] = $controller->controller; $controller->query['_path'] = http_build_query($controller->attributes, '', '&'); $path = $this->fragmentPath.'?'.http_build_query($controller->query, '', '&'); // we need to sign the absolute URI, but want to return the path only. $fragmentUri = $sign || $absolute ? $request->getUriForPath($path) : $request->getBaseUrl().$path; if (!$sign) { return $fragmentUri; } $fragmentUri = $this->signer->sign($fragmentUri); return $absolute ? $fragmentUri : substr($fragmentUri, \strlen($request->getSchemeAndHttpHost())); } private function checkNonScalar(array $values): void { foreach ($values as $key => $value) { if (\is_array($value)) { $this->checkNonScalar($value); } elseif (!\is_scalar($value) && null !== $value) { throw new \LogicException(\sprintf('Controller attributes cannot contain non-scalar/non-null values (value for key "%s" is not a scalar or null).', $key)); } } } } ================================================ FILE: Fragment/FragmentUriGeneratorInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Fragment; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerReference; /** * Interface implemented by rendering strategies able to generate a URL for a fragment. * * @author Kévin Dunglas */ interface FragmentUriGeneratorInterface { /** * Generates a fragment URI for a given controller. * * @param bool $absolute Whether to generate an absolute URL or not * @param bool $strict Whether to allow non-scalar attributes or not * @param bool $sign Whether to sign the URL or not */ public function generate(ControllerReference $controller, ?Request $request = null, bool $absolute = false, bool $strict = true, bool $sign = true): string; } ================================================ FILE: Fragment/HIncludeFragmentRenderer.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Fragment; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Twig\Environment; /** * Implements the Hinclude rendering strategy. * * @author Fabien Potencier */ class HIncludeFragmentRenderer extends RoutableFragmentRenderer { /** * @param string|null $globalDefaultTemplate The global default content (it can be a template name or the content) */ public function __construct( private ?Environment $twig = null, private ?UriSigner $signer = null, private ?string $globalDefaultTemplate = null, private string $charset = 'utf-8', ) { } /** * Checks if a templating engine has been set. */ public function hasTemplating(): bool { return null !== $this->twig; } /** * Additional available options: * * * default: The default content (it can be a template name or the content) * * id: An optional hx:include tag id attribute * * attributes: An optional array of hx:include tag attributes */ public function render(string|ControllerReference $uri, Request $request, array $options = []): Response { if ($uri instanceof ControllerReference) { $uri = (new FragmentUriGenerator($this->fragmentPath, $this->signer))->generate($uri, $request); } // We need to replace ampersands in the URI with the encoded form in order to return valid html/xml content. $uri = str_replace('&', '&', $uri); $template = $options['default'] ?? $this->globalDefaultTemplate; if (null !== $this->twig && $template && $this->twig->getLoader()->exists($template)) { $content = $this->twig->render($template); } else { $content = $template; } $attributes = isset($options['attributes']) && \is_array($options['attributes']) ? $options['attributes'] : []; if (isset($options['id']) && $options['id']) { $attributes['id'] = $options['id']; } $renderedAttributes = ''; if (\count($attributes) > 0) { $flags = \ENT_QUOTES | \ENT_SUBSTITUTE; foreach ($attributes as $attribute => $value) { $renderedAttributes .= \sprintf( ' %s="%s"', htmlspecialchars($attribute, $flags, $this->charset, false), htmlspecialchars($value, $flags, $this->charset, false) ); } } return new Response(\sprintf('%s', $uri, $renderedAttributes, $content)); } public function getName(): string { return 'hinclude'; } } ================================================ FILE: Fragment/InlineFragmentRenderer.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Fragment; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\HttpCache\SubRequestHandler; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * Implements the inline rendering strategy where the Request is rendered by the current HTTP kernel. * * @author Fabien Potencier */ class InlineFragmentRenderer extends RoutableFragmentRenderer { public function __construct( private HttpKernelInterface $kernel, private ?EventDispatcherInterface $dispatcher = null, ) { } /** * Additional available options: * * * alt: an alternative URI to render in case of an error */ public function render(string|ControllerReference $uri, Request $request, array $options = []): Response { $reference = null; if ($uri instanceof ControllerReference) { $reference = $uri; // Remove attributes from the generated URI because if not, the Symfony // routing system will use them to populate the Request attributes. We don't // want that as we want to preserve objects (so we manually set Request attributes // below instead) $attributes = $reference->attributes; $reference->attributes = []; // The request format and locale might have been overridden by the user foreach (['_format', '_locale'] as $key) { if (isset($attributes[$key])) { $reference->attributes[$key] = $attributes[$key]; } } $uri = $this->generateFragmentUri($uri, $request, false, false); $reference->attributes = array_merge($attributes, $reference->attributes); } $subRequest = $this->createSubRequest($uri, $request); // override Request attributes as they can be objects (which are not supported by the generated URI) if (null !== $reference) { $subRequest->attributes->add($reference->attributes); } $level = ob_get_level(); try { return SubRequestHandler::handle($this->kernel, $subRequest, HttpKernelInterface::SUB_REQUEST, false); } catch (\Exception $e) { // we dispatch the exception event to trigger the logging // the response that comes back is ignored if (isset($options['ignore_errors']) && $options['ignore_errors'] && $this->dispatcher) { $event = new ExceptionEvent($this->kernel, $request, HttpKernelInterface::SUB_REQUEST, $e); $this->dispatcher->dispatch($event, KernelEvents::EXCEPTION); } // let's clean up the output buffers that were created by the sub-request Response::closeOutputBuffers($level, false); if (isset($options['alt'])) { $alt = $options['alt']; unset($options['alt']); return $this->render($alt, $request, $options); } if (!isset($options['ignore_errors']) || !$options['ignore_errors']) { throw $e; } return new Response(); } } protected function createSubRequest(string $uri, Request $request): Request { $cookies = $request->cookies->all(); $server = $request->server->all(); unset($server['HTTP_IF_MODIFIED_SINCE']); unset($server['HTTP_IF_NONE_MATCH']); $subRequest = Request::create($uri, 'get', [], $cookies, [], $server); if ($request->headers->has('Surrogate-Capability')) { $subRequest->headers->set('Surrogate-Capability', $request->headers->get('Surrogate-Capability')); } static $setSession; $setSession ??= \Closure::bind(static function ($subRequest, $request) { $subRequest->session = $request->session; }, null, Request::class); $setSession($subRequest, $request); if ($request->attributes->has('_format')) { $subRequest->attributes->set('_format', $request->attributes->get('_format')); } if ($request->getDefaultLocale() !== $request->getLocale()) { $subRequest->setLocale($request->getLocale()); } if ($request->attributes->has('_stateless')) { $subRequest->attributes->set('_stateless', $request->attributes->get('_stateless')); } if ($request->attributes->has('_check_controller_is_allowed')) { $subRequest->attributes->set('_check_controller_is_allowed', $request->attributes->get('_check_controller_is_allowed')); } return $subRequest; } public function getName(): string { return 'inline'; } } ================================================ FILE: Fragment/RoutableFragmentRenderer.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Fragment; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\EventListener\FragmentListener; /** * Adds the possibility to generate a fragment URI for a given Controller. * * @author Fabien Potencier */ abstract class RoutableFragmentRenderer implements FragmentRendererInterface { /** * @internal */ protected string $fragmentPath = '/_fragment'; /** * Sets the fragment path that triggers the fragment listener. * * @see FragmentListener */ public function setFragmentPath(string $path): void { $this->fragmentPath = $path; } /** * Generates a fragment URI for a given controller. * * @param bool $absolute Whether to generate an absolute URL or not * @param bool $strict Whether to allow non-scalar attributes or not */ protected function generateFragmentUri(ControllerReference $reference, Request $request, bool $absolute = false, bool $strict = true): string { return (new FragmentUriGenerator($this->fragmentPath))->generate($reference, $request, $absolute, $strict, false); } } ================================================ FILE: Fragment/SsiFragmentRenderer.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Fragment; /** * Implements the SSI rendering strategy. * * @author Sebastian Krebs */ class SsiFragmentRenderer extends AbstractSurrogateFragmentRenderer { public function getName(): string { return 'ssi'; } } ================================================ FILE: HttpCache/AbstractSurrogate.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\HttpCache; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; /** * Abstract class implementing Surrogate capabilities to Request and Response instances. * * @author Fabien Potencier * @author Robin Chalas */ abstract class AbstractSurrogate implements SurrogateInterface { /** * @param array $contentTypes An array of content-type that should be parsed for Surrogate information * (default: text/html, text/xml, application/xhtml+xml, and application/xml) */ public function __construct( protected array $contentTypes = ['text/html', 'text/xml', 'application/xhtml+xml', 'application/xml'], ) { } /** * Returns a new cache strategy instance. */ public function createCacheStrategy(): ResponseCacheStrategyInterface { return new ResponseCacheStrategy(); } public function hasSurrogateCapability(Request $request): bool { if (null === $value = $request->headers->get('Surrogate-Capability')) { return false; } return str_contains($value, \sprintf('%s/1.0', strtoupper($this->getName()))); } public function addSurrogateCapability(Request $request): void { $current = $request->headers->get('Surrogate-Capability'); $new = \sprintf('symfony="%s/1.0"', strtoupper($this->getName())); $request->headers->set('Surrogate-Capability', $current ? $current.', '.$new : $new); } public function needsParsing(Response $response): bool { if (!$control = $response->headers->get('Surrogate-Control')) { return false; } $pattern = \sprintf('#content="[^"]*%s/1.0[^"]*"#', strtoupper($this->getName())); return (bool) preg_match($pattern, $control); } public function handle(HttpCache $cache, string $uri, string $alt, bool $ignoreErrors): string { $subRequest = Request::create($uri, 'GET', [], $cache->getRequest()->cookies->all(), [], $cache->getRequest()->server->all()); try { $response = $cache->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true); if (!$response->isSuccessful() && Response::HTTP_NOT_MODIFIED !== $response->getStatusCode()) { throw new \RuntimeException(\sprintf('Error when rendering "%s" (Status code is %d).', $subRequest->getUri(), $response->getStatusCode())); } return $response->getContent(); } catch (\Exception $e) { if ($alt) { return $this->handle($cache, $alt, '', $ignoreErrors); } if (!$ignoreErrors) { throw $e; } } return ''; } /** * Remove the Surrogate from the Surrogate-Control header. */ protected function removeFromControl(Response $response): void { if (!$response->headers->has('Surrogate-Control')) { return; } $value = $response->headers->get('Surrogate-Control'); $upperName = strtoupper($this->getName()); if (\sprintf('content="%s/1.0"', $upperName) == $value) { $response->headers->remove('Surrogate-Control'); } elseif (preg_match(\sprintf('#,\s*content="%s/1.0"#', $upperName), $value)) { $response->headers->set('Surrogate-Control', preg_replace(\sprintf('#,\s*content="%s/1.0"#', $upperName), '', $value)); } elseif (preg_match(\sprintf('#content="%s/1.0",\s*#', $upperName), $value)) { $response->headers->set('Surrogate-Control', preg_replace(\sprintf('#content="%s/1.0",\s*#', $upperName), '', $value)); } } protected static function generateBodyEvalBoundary(): string { static $cookie; $cookie = hash('xxh128', $cookie ?? $cookie = random_bytes(16), true); $boundary = base64_encode($cookie); \assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === \strlen($boundary)); return $boundary; } } ================================================ FILE: HttpCache/CacheWasLockedException.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\HttpCache; /** * @internal */ class CacheWasLockedException extends \Exception { } ================================================ FILE: HttpCache/Esi.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\HttpCache; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** * Esi implements the ESI capabilities to Request and Response instances. * * For more information, read the following W3C notes: * * * ESI Language Specification 1.0 (http://www.w3.org/TR/esi-lang) * * * Edge Architecture Specification (http://www.w3.org/TR/edge-arch) * * @author Fabien Potencier */ class Esi extends AbstractSurrogate { public function getName(): string { return 'esi'; } public function addSurrogateControl(Response $response): void { if (str_contains($response->getContent(), 'headers->set('Surrogate-Control', 'content="ESI/1.0"'); } } public function renderIncludeTag(string $uri, ?string $alt = null, bool $ignoreErrors = true, string $comment = ''): string { $html = \sprintf('', $uri, $ignoreErrors ? ' onerror="continue"' : '', $alt ? \sprintf(' alt="%s"', $alt) : '' ); if ($comment) { return \sprintf("\n%s", $comment, $html); } return $html; } public function process(Request $request, Response $response): Response { $type = $response->headers->get('Content-Type'); if (!$type) { $type = 'text/html'; } $parts = explode(';', $type); if (!\in_array($parts[0], $this->contentTypes, true)) { return $response; } // we don't use a proper XML parser here as we can have ESI tags in a plain text response $content = $response->getContent(); $content = preg_replace('#.*?#s', '', $content); $content = preg_replace('#]+>#s', '', $content); $boundary = self::generateBodyEvalBoundary(); $chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); $i = 1; while (isset($chunks[$i])) { $options = []; preg_match_all('/(src|onerror|alt)="([^"]*?)"/', $chunks[$i], $matches, \PREG_SET_ORDER); foreach ($matches as $set) { $options[$set[1]] = $set[2]; } if (!isset($options['src'])) { throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.'); } $chunks[$i] = $boundary.$options['src']."\n".($options['alt'] ?? '')."\n".('continue' === ($options['onerror'] ?? ''))."\n"; $i += 2; } $content = $boundary.implode('', $chunks).$boundary; $response->setContent($content); $response->headers->set('X-Body-Eval', 'ESI'); // remove ESI/1.0 from the Surrogate-Control header $this->removeFromControl($response); return $response; } } ================================================ FILE: HttpCache/HttpCache.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /* * This code is partially based on the Rack-Cache library by Ryan Tomayko, * which is released under the MIT license. * (based on commit 02d2b48d75bcb63cf1c0c7149c077ad256542801) */ namespace Symfony\Component\HttpKernel\HttpCache; use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\TerminableInterface; /** * Cache provides HTTP caching. * * @author Fabien Potencier */ class HttpCache implements HttpKernelInterface, TerminableInterface { public const BODY_EVAL_BOUNDARY_LENGTH = 24; private Request $request; private ?ResponseCacheStrategyInterface $surrogateCacheStrategy = null; private array $options = []; private array $traces = []; /** * Constructor. * * The available options are: * * * debug If true, exceptions are thrown when things go wrong. Otherwise, the cache * will try to carry on and deliver a meaningful response. * * * trace_level May be one of 'none', 'short' and 'full'. For 'short', a concise trace of the * main request will be added as an HTTP header. 'full' will add traces for all * requests (including ESI subrequests). (default: 'full' if in debug; 'none' otherwise) * * * trace_header Header name to use for traces. (default: X-Symfony-Cache) * * * default_ttl The number of seconds that a cache entry should be considered * fresh when no explicit freshness information is provided in * a response. Explicit Cache-Control or Expires headers * override this value. (default: 0) * * * private_headers Set of request headers that trigger "private" cache-control behavior * on responses that don't explicitly state whether the response is * public or private via a Cache-Control directive. (default: Authorization and Cookie) * * * skip_response_headers Set of response headers that are never cached even if a response is cacheable (public). * (default: Set-Cookie) * * * allow_reload Specifies whether the client can force a cache reload by including a * Cache-Control "no-cache" directive in the request. Set it to ``true`` * for compliance with RFC 2616. (default: false) * * * allow_revalidate Specifies whether the client can force a cache revalidate by including * a Cache-Control "max-age=0" directive in the request. Set it to ``true`` * for compliance with RFC 2616. (default: false) * * * stale_while_revalidate Specifies the default number of seconds (the granularity is the second as the * Response TTL precision is a second) during which the cache can immediately return * a stale response while it revalidates it in the background (default: 2). * This setting is overridden by the stale-while-revalidate HTTP Cache-Control * extension (see RFC 5861). * * * stale_if_error Specifies the default number of seconds (the granularity is the second) during which * the cache can serve a stale response when an error is encountered (default: 60). * This setting is overridden by the stale-if-error HTTP Cache-Control extension * (see RFC 5861). */ public function __construct( private HttpKernelInterface $kernel, private StoreInterface $store, private ?SurrogateInterface $surrogate = null, array $options = [], ) { // needed in case there is a fatal error because the backend is too slow to respond register_shutdown_function($this->store->cleanup(...)); $this->options = array_merge([ 'debug' => false, 'default_ttl' => 0, 'private_headers' => ['Authorization', 'Cookie'], 'skip_response_headers' => ['Set-Cookie'], 'allow_reload' => false, 'allow_revalidate' => false, 'stale_while_revalidate' => 2, 'stale_if_error' => 60, 'trace_level' => 'none', 'trace_header' => 'X-Symfony-Cache', ], $options); if (!isset($options['trace_level'])) { $this->options['trace_level'] = $this->options['debug'] ? 'full' : 'none'; } } /** * Gets the current store. */ public function getStore(): StoreInterface { return $this->store; } /** * Returns an array of events that took place during processing of the last request. */ public function getTraces(): array { return $this->traces; } private function addTraces(Response $response): void { $traceString = null; if ('full' === $this->options['trace_level']) { $traceString = $this->getLog(); } if ('short' === $this->options['trace_level'] && $masterId = array_key_first($this->traces)) { $traceString = implode('/', $this->traces[$masterId]); } if (null !== $traceString) { $response->headers->add([$this->options['trace_header'] => $traceString]); } } /** * Returns a log message for the events of the last request processing. */ public function getLog(): string { $log = []; foreach ($this->traces as $request => $traces) { $log[] = \sprintf('%s: %s', $request, implode(', ', $traces)); } return implode('; ', $log); } /** * Gets the Request instance associated with the main request. */ public function getRequest(): Request { return $this->request; } /** * Gets the Kernel instance. */ public function getKernel(): HttpKernelInterface { return $this->kernel; } /** * Gets the Surrogate instance. * * @throws \LogicException */ public function getSurrogate(): SurrogateInterface { return $this->surrogate; } public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response { // FIXME: catch exceptions and implement a 500 error page here? -> in Varnish, there is a built-in error page mechanism if (HttpKernelInterface::MAIN_REQUEST === $type) { $this->traces = []; // Keep a clone of the original request for surrogates so they can access it. // We must clone here to get a separate instance because the application will modify the request during // the application flow (we know it always does because we do ourselves by setting REMOTE_ADDR to 127.0.0.1 // and adding the X-Forwarded-For header, see HttpCache::forward()). $this->request = clone $request; if (null !== $this->surrogate) { $this->surrogateCacheStrategy = $this->surrogate->createCacheStrategy(); } } $this->traces[$this->getTraceKey($request)] = []; if (!$request->isMethodSafe()) { $response = $this->invalidate($request, $catch); } elseif ($request->headers->has('expect') || !$request->isMethodCacheable()) { $response = $this->pass($request, $catch); } elseif ($this->options['allow_reload'] && $request->isNoCache()) { /* If allow_reload is configured and the client requests "Cache-Control: no-cache", reload the cache by fetching a fresh response and caching it (if possible). */ $this->record($request, 'reload'); $response = $this->fetch($request, $catch); } else { $response = null; do { try { $response = $this->lookup($request, $catch); } catch (CacheWasLockedException) { } } while (null === $response); } $this->restoreResponseBody($request, $response); if (HttpKernelInterface::MAIN_REQUEST === $type) { $this->addTraces($response); } if (null !== $this->surrogate) { if (HttpKernelInterface::MAIN_REQUEST === $type) { $this->surrogateCacheStrategy->update($response); } else { $this->surrogateCacheStrategy->add($response); } } $response->prepare($request); if (HttpKernelInterface::MAIN_REQUEST === $type) { $response->isNotModified($request); } return $response; } public function terminate(Request $request, Response $response): void { // Do not call any listeners in case of a cache hit. // This ensures identical behavior as if you had a separate // reverse caching proxy such as Varnish and the like. if (\in_array('fresh', $this->traces[$this->getTraceKey($request)] ?? [], true)) { return; } if ($this->getKernel() instanceof TerminableInterface) { $this->getKernel()->terminate($request, $response); } } /** * Forwards the Request to the backend without storing the Response in the cache. * * @param bool $catch Whether to process exceptions */ protected function pass(Request $request, bool $catch = false): Response { $this->record($request, 'pass'); return $this->forward($request, $catch); } /** * Invalidates non-safe methods (like POST, PUT, and DELETE). * * @param bool $catch Whether to process exceptions * * @throws \Exception * * @see RFC2616 13.10 */ protected function invalidate(Request $request, bool $catch = false): Response { $response = $this->pass($request, $catch); // invalidate only when the response is successful if ($response->isSuccessful() || $response->isRedirect()) { try { $this->store->invalidate($request); // As per the RFC, invalidate Location and Content-Location URLs if present foreach (['Location', 'Content-Location'] as $header) { if ($uri = $response->headers->get($header)) { $subRequest = Request::create($uri, 'get', [], [], [], $request->server->all()); $this->store->invalidate($subRequest); } } $this->record($request, 'invalidate'); } catch (\Exception $e) { $this->record($request, 'invalidate-failed'); if ($this->options['debug']) { throw $e; } } } return $response; } /** * Lookups a Response from the cache for the given Request. * * When a matching cache entry is found and is fresh, it uses it as the * response without forwarding any request to the backend. When a matching * cache entry is found but is stale, it attempts to "validate" the entry with * the backend using conditional GET. When no matching cache entry is found, * it triggers "miss" processing. * * @param bool $catch Whether to process exceptions * * @throws \Exception */ protected function lookup(Request $request, bool $catch = false): Response { try { $entry = $this->store->lookup($request); } catch (\Exception $e) { $this->record($request, 'lookup-failed'); if ($this->options['debug']) { throw $e; } return $this->pass($request, $catch); } if (null === $entry) { $this->record($request, 'miss'); return $this->fetch($request, $catch); } if (!$this->isFreshEnough($request, $entry)) { $this->record($request, 'stale'); return $this->validate($request, $entry, $catch); } if ($entry->headers->hasCacheControlDirective('no-cache')) { return $this->validate($request, $entry, $catch); } $this->record($request, 'fresh'); $entry->headers->set('Age', $entry->getAge()); return $entry; } /** * Validates that a cache entry is fresh. * * The original request is used as a template for a conditional * GET request with the backend. * * @param bool $catch Whether to process exceptions */ protected function validate(Request $request, Response $entry, bool $catch = false): Response { $subRequest = clone $request; // send no head requests because we want content if ('HEAD' === $request->getMethod()) { $subRequest->setMethod('GET'); } // add our cached last-modified validator if ($entry->headers->has('Last-Modified')) { $subRequest->headers->set('If-Modified-Since', $entry->headers->get('Last-Modified')); } // Add our cached etag validator to the environment. // We keep the etags from the client to handle the case when the client // has a different private valid entry which is not cached here. $cachedEtags = $entry->getEtag() ? [$entry->getEtag()] : []; $requestEtags = $request->getETags(); if ($etags = array_unique(array_merge($cachedEtags, $requestEtags))) { $subRequest->headers->set('If-None-Match', implode(', ', $etags)); } $response = $this->forward($subRequest, $catch, $entry); if (304 == $response->getStatusCode()) { $this->record($request, 'valid'); // return the response and not the cache entry if the response is valid but not cached $etag = $response->getEtag(); if ($etag && \in_array($etag, $requestEtags, true) && !\in_array($etag, $cachedEtags, true)) { return $response; } $entry = clone $entry; $entry->headers->remove('Date'); foreach (['Date', 'Expires', 'Cache-Control', 'ETag', 'Last-Modified'] as $name) { if ($response->headers->has($name)) { $entry->headers->set($name, $response->headers->get($name)); } } $response = $entry; } else { $this->record($request, 'invalid'); } if ($response->isCacheable()) { $this->store($request, $response); } return $response; } /** * Unconditionally fetches a fresh response from the backend and * stores it in the cache if is cacheable. * * @param bool $catch Whether to process exceptions */ protected function fetch(Request $request, bool $catch = false): Response { $subRequest = clone $request; // send no head requests because we want content if ('HEAD' === $request->getMethod()) { $subRequest->setMethod('GET'); } // avoid that the backend sends no content $subRequest->headers->remove('If-Modified-Since'); $subRequest->headers->remove('If-None-Match'); $response = $this->forward($subRequest, $catch); if ($response->isCacheable()) { $this->store($request, $response); } return $response; } /** * Forwards the Request to the backend and returns the Response. * * All backend requests (cache passes, fetches, cache validations) * run through this method. * * @param bool $catch Whether to catch exceptions or not * @param Response|null $entry A Response instance (the stale entry if present, null otherwise) */ protected function forward(Request $request, bool $catch = false, ?Response $entry = null): Response { $this->surrogate?->addSurrogateCapability($request); // always a "master" request (as the real master request can be in cache) $response = SubRequestHandler::handle($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $catch); /* * Support stale-if-error given on Responses or as a config option. * RFC 7234 summarizes in Section 4.2.4 (but also mentions with the individual * Cache-Control directives) that * * A cache MUST NOT generate a stale response if it is prohibited by an * explicit in-protocol directive (e.g., by a "no-store" or "no-cache" * cache directive, a "must-revalidate" cache-response-directive, or an * applicable "s-maxage" or "proxy-revalidate" cache-response-directive; * see Section 5.2.2). * * https://tools.ietf.org/html/rfc7234#section-4.2.4 * * We deviate from this in one detail, namely that we *do* serve entries in the * stale-if-error case even if they have a `s-maxage` Cache-Control directive. */ if (null !== $entry && \in_array($response->getStatusCode(), [500, 502, 503, 504], true) && !$entry->headers->hasCacheControlDirective('no-cache') && !$entry->mustRevalidate() ) { if (null === $age = $entry->headers->getCacheControlDirective('stale-if-error')) { $age = $this->options['stale_if_error']; } /* * stale-if-error gives the (extra) time that the Response may be used *after* it has become stale. * So we compare the time the $entry has been sitting in the cache already with the * time it was fresh plus the allowed grace period. */ if ($entry->getAge() <= $entry->getMaxAge() + $age) { $this->record($request, 'stale-if-error'); return $entry; } } /* RFC 7231 Sect. 7.1.1.2 says that a server that does not have a reasonably accurate clock MUST NOT send a "Date" header, although it MUST send one in most other cases except for 1xx or 5xx responses where it MAY do so. Anyway, a client that received a message without a "Date" header MUST add it. */ if (!$response->headers->has('Date')) { $response->setDate(\DateTimeImmutable::createFromFormat('U', time())); } $this->processResponseBody($request, $response); if ($this->isPrivateRequest($request) && !$response->headers->hasCacheControlDirective('public')) { $response->setPrivate(); } elseif ($this->options['default_ttl'] > 0 && null === $response->getTtl() && !$response->headers->getCacheControlDirective('must-revalidate')) { $response->setTtl($this->options['default_ttl']); } return $response; } /** * Checks whether the cache entry is "fresh enough" to satisfy the Request. */ protected function isFreshEnough(Request $request, Response $entry): bool { if (!$entry->isFresh()) { return $this->lock($request, $entry); } if ($this->options['allow_revalidate'] && null !== $maxAge = $request->headers->getCacheControlDirective('max-age')) { return $maxAge > 0 && $maxAge >= $entry->getAge(); } return true; } /** * Locks a Request during the call to the backend. * * @return bool true if the cache entry can be returned even if it is staled, false otherwise */ protected function lock(Request $request, Response $entry): bool { // try to acquire a lock to call the backend $lock = $this->store->lock($request); if (true === $lock) { // we have the lock, call the backend return false; } // there is already another process calling the backend // May we serve a stale response? if ($this->mayServeStaleWhileRevalidate($entry)) { $this->record($request, 'stale-while-revalidate'); return true; } $this->record($request, 'waiting'); // wait for the lock to be released if ($this->waitForLock($request)) { throw new CacheWasLockedException(); // unwind back to handle(), try again } // backend is slow as hell, send a 503 response (to avoid the dog pile effect) $entry->setStatusCode(503); $entry->setContent('503 Service Unavailable'); $entry->headers->set('Retry-After', 10); return true; } /** * Writes the Response to the cache. * * @throws \Exception */ protected function store(Request $request, Response $response): void { try { $restoreHeaders = []; foreach ($this->options['skip_response_headers'] as $header) { if (!$response->headers->has($header)) { continue; } $restoreHeaders[$header] = $response->headers->all($header); $response->headers->remove($header); } $this->store->write($request, $response); $this->record($request, 'store'); $response->headers->set('Age', $response->getAge()); } catch (\Exception $e) { $this->record($request, 'store-failed'); if ($this->options['debug']) { throw $e; } } finally { foreach ($restoreHeaders as $header => $values) { $response->headers->set($header, $values); } } // now that the response is cached, release the lock $this->store->unlock($request); } /** * Restores the Response body. */ private function restoreResponseBody(Request $request, Response $response): void { if ($response->headers->has('X-Body-Eval')) { \assert(self::BODY_EVAL_BOUNDARY_LENGTH === 24); ob_start(); $content = $response->getContent(); $boundary = substr($content, 0, 24); $j = strpos($content, $boundary, 24); echo substr($content, 24, $j - 24); $i = $j + 24; while (false !== $j = strpos($content, $boundary, $i)) { [$uri, $alt, $ignoreErrors, $part] = explode("\n", substr($content, $i, $j - $i), 4); $i = $j + 24; echo $this->surrogate->handle($this, $uri, $alt, $ignoreErrors); echo $part; } $response->setContent(ob_get_clean()); $response->headers->remove('X-Body-Eval'); if (!$response->headers->has('Transfer-Encoding')) { $response->headers->set('Content-Length', \strlen($response->getContent())); } } elseif ($response->headers->has('X-Body-File')) { // Response does not include possibly dynamic content (ESI, SSI), so we need // not handle the content for HEAD requests if (!$request->isMethod('HEAD')) { $response->setContent(file_get_contents($response->headers->get('X-Body-File'))); } } else { return; } $response->headers->remove('X-Body-File'); } protected function processResponseBody(Request $request, Response $response): void { if ($this->surrogate?->needsParsing($response)) { $this->surrogate->process($request, $response); } } /** * Checks if the Request includes authorization or other sensitive information * that should cause the Response to be considered private by default. */ private function isPrivateRequest(Request $request): bool { foreach ($this->options['private_headers'] as $key) { $key = strtolower(str_replace('HTTP_', '', $key)); if ('cookie' === $key) { if (\count($request->cookies->all())) { return true; } } elseif ($request->headers->has($key)) { return true; } } return false; } /** * Records that an event took place. */ private function record(Request $request, string $event): void { $this->traces[$this->getTraceKey($request)][] = $event; } /** * Calculates the key we use in the "trace" array for a given request. */ private function getTraceKey(Request $request): string { $path = $request->getPathInfo(); if ($qs = $request->getQueryString()) { $path .= '?'.$qs; } try { return $request->getMethod().' '.$path; } catch (SuspiciousOperationException) { return '_BAD_METHOD_ '.$path; } } /** * Checks whether the given (cached) response may be served as "stale" when a revalidation * is currently in progress. */ private function mayServeStaleWhileRevalidate(Response $entry): bool { $timeout = $entry->headers->getCacheControlDirective('stale-while-revalidate'); $timeout ??= $this->options['stale_while_revalidate']; $age = $entry->getAge(); $maxAge = $entry->getMaxAge() ?? 0; $ttl = $maxAge - $age; return abs($ttl) < $timeout; } /** * Waits for the store to release a locked entry. */ private function waitForLock(Request $request): bool { $wait = 0; while ($this->store->isLocked($request) && $wait < 100) { usleep(50000); ++$wait; } return $wait < 100; } } ================================================ FILE: HttpCache/ResponseCacheStrategy.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\HttpCache; use Symfony\Component\HttpFoundation\Response; /** * ResponseCacheStrategy knows how to compute the Response cache HTTP header * based on the different response cache headers. * * This implementation changes the main response TTL to the smallest TTL received * or force validation if one of the surrogates has validation cache strategy. * * @author Fabien Potencier */ class ResponseCacheStrategy implements ResponseCacheStrategyInterface { /** * Cache-Control headers that are sent to the final response if they appear in ANY of the responses. */ private const OVERRIDE_DIRECTIVES = ['private', 'no-cache', 'no-store', 'no-transform', 'must-revalidate', 'proxy-revalidate']; /** * Cache-Control headers that are sent to the final response if they appear in ALL of the responses. */ private const INHERIT_DIRECTIVES = ['public', 'immutable']; private int $embeddedResponses = 0; private bool $isNotCacheableResponseEmbedded = false; private int $age = 0; private \DateTimeInterface|false|null $lastModified = null; private array $flagDirectives = [ 'no-cache' => null, 'no-store' => null, 'no-transform' => null, 'must-revalidate' => null, 'proxy-revalidate' => null, 'public' => null, 'private' => null, 'immutable' => null, ]; private array $ageDirectives = [ 'max-age' => null, 's-maxage' => null, 'expires' => false, ]; public function add(Response $response): void { ++$this->embeddedResponses; foreach (self::OVERRIDE_DIRECTIVES as $directive) { if ($response->headers->hasCacheControlDirective($directive)) { $this->flagDirectives[$directive] = true; } } foreach (self::INHERIT_DIRECTIVES as $directive) { if (false !== $this->flagDirectives[$directive]) { $this->flagDirectives[$directive] = $response->headers->hasCacheControlDirective($directive); } } $age = $response->getAge(); $this->age = max($this->age, $age); if ($this->willMakeFinalResponseUncacheable($response)) { $this->isNotCacheableResponseEmbedded = true; return; } $maxAge = $response->headers->hasCacheControlDirective('max-age') ? (int) $response->headers->getCacheControlDirective('max-age') : null; $sharedMaxAge = $response->headers->hasCacheControlDirective('s-maxage') ? (int) $response->headers->getCacheControlDirective('s-maxage') : $maxAge; $expires = $response->getExpires(); $expires = null !== $expires ? (int) $expires->format('U') - (int) $response->getDate()->format('U') : null; // See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2 // If a response is "public" but does not have maximum lifetime, heuristics might be applied. // Do not store NULL values so the final response can have more limiting value from other responses. $isHeuristicallyCacheable = $response->headers->hasCacheControlDirective('public') && null === $maxAge && null === $sharedMaxAge && null === $expires; if (!$isHeuristicallyCacheable || null !== $maxAge || null !== $expires) { $this->storeRelativeAgeDirective('max-age', $maxAge, $expires, $age); } if (!$isHeuristicallyCacheable || null !== $sharedMaxAge || null !== $expires) { $this->storeRelativeAgeDirective('s-maxage', $sharedMaxAge, $expires, $age); } if (null !== $expires) { $this->ageDirectives['expires'] = true; } if (false !== $this->lastModified) { $lastModified = $response->getLastModified(); $this->lastModified = $lastModified ? max($this->lastModified, $lastModified) : false; } } public function update(Response $response): void { // if we have no embedded Response, do nothing if (0 === $this->embeddedResponses) { return; } // Remove Etag since it cannot be merged from embedded responses. $response->setEtag(null); $this->add($response); $response->headers->set('Age', $this->age); if ($this->isNotCacheableResponseEmbedded) { $response->setLastModified(null); if ($this->flagDirectives['no-store']) { $response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate'); } else { $response->headers->set('Cache-Control', 'no-cache, must-revalidate'); } return; } $response->setLastModified($this->lastModified ?: null); $flags = array_filter($this->flagDirectives); if (isset($flags['must-revalidate'])) { $flags['no-cache'] = true; } $response->headers->set('Cache-Control', implode(', ', array_keys($flags))); $maxAge = null; if (is_numeric($this->ageDirectives['max-age'])) { $maxAge = $this->ageDirectives['max-age'] + $this->age; $response->headers->addCacheControlDirective('max-age', $maxAge); } if (is_numeric($this->ageDirectives['s-maxage'])) { $sMaxage = $this->ageDirectives['s-maxage'] + $this->age; if ($maxAge !== $sMaxage) { $response->headers->addCacheControlDirective('s-maxage', $sMaxage); } } if ($this->ageDirectives['expires'] && null !== $maxAge) { $date = clone $response->getDate(); $date = $date->modify('+'.$maxAge.' seconds'); $response->setExpires($date); } } /** * RFC2616, Section 13.4. * * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4 */ private function willMakeFinalResponseUncacheable(Response $response): bool { // RFC2616: A response received with a status code of 200, 203, 300, 301 or 410 // MAY be stored by a cache […] unless a cache-control directive prohibits caching. if ($response->headers->hasCacheControlDirective('no-cache') || $response->headers->hasCacheControlDirective('no-store') ) { return true; } // Etag headers cannot be merged, they render the response uncacheable // by default (except if the response also has max-age etc.). if (null === $response->getEtag() && \in_array($response->getStatusCode(), [200, 203, 300, 301, 410], true)) { return false; } // RFC2616: A response received with any other status code (e.g. status codes 302 and 307) // MUST NOT be returned in a reply to a subsequent request unless there are // cache-control directives or another header(s) that explicitly allow it. $cacheControl = ['max-age', 's-maxage', 'must-revalidate', 'proxy-revalidate', 'public', 'private']; foreach ($cacheControl as $key) { if ($response->headers->hasCacheControlDirective($key)) { return false; } } if ($response->headers->has('Expires')) { return false; } return true; } /** * Store lowest max-age/s-maxage/expires for the final response. * * The response might have been stored in cache a while ago. To keep things comparable, * we have to subtract the age so that the value is normalized for an age of 0. * * If the value is lower than the currently stored value, we update the value, to keep a rolling * minimal value of each instruction. If the value is NULL, the directive will not be set on the final response. */ private function storeRelativeAgeDirective(string $directive, ?int $value, ?int $expires, int $age): void { if (null === $value && null === $expires) { $this->ageDirectives[$directive] = false; } if (false !== $this->ageDirectives[$directive]) { $value = min($value ?? \PHP_INT_MAX, $expires ?? \PHP_INT_MAX); $value -= $age; $this->ageDirectives[$directive] = null !== $this->ageDirectives[$directive] ? min($this->ageDirectives[$directive], $value) : $value; } } } ================================================ FILE: HttpCache/ResponseCacheStrategyInterface.php ================================================ * * This code is partially based on the Rack-Cache library by Ryan Tomayko, * which is released under the MIT license. * (based on commit 02d2b48d75bcb63cf1c0c7149c077ad256542801) * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\HttpCache; use Symfony\Component\HttpFoundation\Response; /** * ResponseCacheStrategyInterface implementations know how to compute the * Response cache HTTP header based on the different response cache headers. * * @author Fabien Potencier */ interface ResponseCacheStrategyInterface { /** * Adds a Response. */ public function add(Response $response): void; /** * Updates the Response HTTP headers based on the embedded Responses. */ public function update(Response $response): void; } ================================================ FILE: HttpCache/Ssi.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\HttpCache; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** * Ssi implements the SSI capabilities to Request and Response instances. * * @author Sebastian Krebs */ class Ssi extends AbstractSurrogate { public function getName(): string { return 'ssi'; } public function addSurrogateControl(Response $response): void { if (str_contains($response->getContent(), '', $uri); } public function process(Request $request, Response $response): Response { $type = $response->headers->get('Content-Type'); if (!$type) { $type = 'text/html'; } $parts = explode(';', $type); if (!\in_array($parts[0], $this->contentTypes, true)) { return $response; } // we don't use a proper XML parser here as we can have SSI tags in a plain text response $content = $response->getContent(); $boundary = self::generateBodyEvalBoundary(); $chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); $i = 1; while (isset($chunks[$i])) { $options = []; preg_match_all('/(virtual)="([^"]*?)"/', $chunks[$i], $matches, \PREG_SET_ORDER); foreach ($matches as $set) { $options[$set[1]] = $set[2]; } if (!isset($options['virtual'])) { throw new \RuntimeException('Unable to process an SSI tag without a "virtual" attribute.'); } $chunks[$i] = $boundary.$options['virtual']."\n\n\n"; $i += 2; } $content = $boundary.implode('', $chunks).$boundary; $response->setContent($content); $response->headers->set('X-Body-Eval', 'SSI'); // remove SSI/1.0 from the Surrogate-Control header $this->removeFromControl($response); return $response; } } ================================================ FILE: HttpCache/Store.php ================================================ * * This code is partially based on the Rack-Cache library by Ryan Tomayko, * which is released under the MIT license. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\HttpCache; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** * Store implements all the logic for storing cache metadata (Request and Response headers). * * @author Fabien Potencier */ class Store implements StoreInterface { /** @var \SplObjectStorage */ private \SplObjectStorage $keyCache; /** @var array */ private array $locks = []; /** * Constructor. * * The available options are: * * * private_headers Set of response headers that should not be stored * when a response is cached. (default: Set-Cookie) * * @throws \RuntimeException */ public function __construct( protected string $root, private array $options = [], ) { if (!is_dir($this->root) && !@mkdir($this->root, 0o777, true) && !is_dir($this->root)) { throw new \RuntimeException(\sprintf('Unable to create the store directory (%s).', $this->root)); } $this->keyCache = new \SplObjectStorage(); $this->options['private_headers'] ??= ['Set-Cookie']; } /** * Cleanups storage. */ public function cleanup(): void { // unlock everything foreach ($this->locks as $lock) { flock($lock, \LOCK_UN); fclose($lock); } $this->locks = []; } /** * Tries to lock the cache for a given Request, without blocking. * * @return bool|string true if the lock is acquired, the path to the current lock otherwise */ public function lock(Request $request): bool|string { $key = $this->getCacheKey($request); if (!isset($this->locks[$key])) { $path = $this->getPath($key); if (!is_dir(\dirname($path)) && false === @mkdir(\dirname($path), 0o777, true) && !is_dir(\dirname($path))) { return $path; } $h = fopen($path, 'c'); if (!flock($h, \LOCK_EX | \LOCK_NB)) { fclose($h); return $path; } $this->locks[$key] = $h; } return true; } /** * Releases the lock for the given Request. * * @return bool False if the lock file does not exist or cannot be unlocked, true otherwise */ public function unlock(Request $request): bool { $key = $this->getCacheKey($request); if (isset($this->locks[$key])) { flock($this->locks[$key], \LOCK_UN); fclose($this->locks[$key]); unset($this->locks[$key]); return true; } return false; } public function isLocked(Request $request): bool { $key = $this->getCacheKey($request); if (isset($this->locks[$key])) { return true; // shortcut if lock held by this process } if (!is_file($path = $this->getPath($key))) { return false; } $h = fopen($path, 'r'); flock($h, \LOCK_EX | \LOCK_NB, $wouldBlock); flock($h, \LOCK_UN); // release the lock we just acquired fclose($h); return (bool) $wouldBlock; } /** * Locates a cached Response for the Request provided. */ public function lookup(Request $request): ?Response { $key = $this->getCacheKey($request); if (!$entries = $this->getMetadata($key)) { return null; } // find a cached entry that matches the request. $match = null; foreach ($entries as $entry) { if ($this->requestsMatch(isset($entry[1]['vary'][0]) ? implode(', ', $entry[1]['vary']) : '', $request->headers->all(), $entry[0])) { $match = $entry; break; } } if (null === $match) { return null; } $headers = $match[1]; if (file_exists($path = $this->getPath($headers['x-content-digest'][0]))) { return $this->restoreResponse($headers, $path); } // TODO the metaStore referenced an entity that doesn't exist in // the entityStore. We definitely want to return nil but we should // also purge the entry from the meta-store when this is detected. return null; } /** * Writes a cache entry to the store for the given Request and Response. * * Existing entries are read and any that match the response are removed. This * method calls write with the new list of cache entries. * * @throws \RuntimeException */ public function write(Request $request, Response $response): string { $key = $this->getCacheKey($request); $storedEnv = $this->persistRequest($request); if ($response->headers->has('X-Body-File')) { // Assume the response came from disk, but at least perform some safeguard checks if (!$response->headers->has('X-Content-Digest')) { throw new \RuntimeException('A restored response must have the X-Content-Digest header.'); } $digest = $response->headers->get('X-Content-Digest'); if ($this->getPath($digest) !== $response->headers->get('X-Body-File')) { throw new \RuntimeException('X-Body-File and X-Content-Digest do not match.'); } // Everything seems ok, omit writing content to disk } else { $digest = $this->generateContentDigest($response); $response->headers->set('X-Content-Digest', $digest); if (!$this->save($digest, $response->getContent(), false)) { throw new \RuntimeException('Unable to store the entity.'); } if (!$response->headers->has('Transfer-Encoding')) { $response->headers->set('Content-Length', \strlen($response->getContent())); } } // read existing cache entries, remove non-varying, and add this one to the list $entries = []; $vary = implode(', ', $response->headers->all('vary')); foreach ($this->getMetadata($key) as $entry) { if (!$this->requestsMatch($vary ?? '', $entry[0], $storedEnv)) { $entries[] = $entry; } } $headers = $this->persistResponse($response); unset($headers['age']); foreach ($this->options['private_headers'] as $h) { unset($headers[strtolower($h)]); } array_unshift($entries, [$storedEnv, $headers]); if (!$this->save($key, serialize($entries))) { throw new \RuntimeException('Unable to store the metadata.'); } return $key; } /** * Returns content digest for $response. */ protected function generateContentDigest(Response $response): string { return 'en'.hash('xxh128', $response->getContent()); } /** * Invalidates all cache entries that match the request. * * @throws \RuntimeException */ public function invalidate(Request $request): void { $modified = false; $key = $this->getCacheKey($request); $entries = []; foreach ($this->getMetadata($key) as $entry) { $response = $this->restoreResponse($entry[1]); if ($response->isFresh()) { $response->expire(); $modified = true; $entries[] = [$entry[0], $this->persistResponse($response)]; } else { $entries[] = $entry; } } if ($modified && !$this->save($key, serialize($entries))) { throw new \RuntimeException('Unable to store the metadata.'); } } /** * Determines whether two Request HTTP header sets are non-varying based on * the vary response header value provided. * * @param string|null $vary A Response vary header * @param array $env1 A Request HTTP header array * @param array $env2 A Request HTTP header array */ private function requestsMatch(?string $vary, array $env1, array $env2): bool { if ('' === ($vary ?? '')) { return true; } foreach (preg_split('/[\s,]+/', $vary) as $header) { $key = str_replace('_', '-', strtolower($header)); $v1 = $env1[$key] ?? null; $v2 = $env2[$key] ?? null; if ($v1 !== $v2) { return false; } } return true; } /** * Gets all data associated with the given key. * * Use this method only if you know what you are doing. */ private function getMetadata(string $key): array { if (!$entries = $this->load($key)) { return []; } return unserialize($entries) ?: []; } /** * Purges data for the given URL. * * This method purges both the HTTP and the HTTPS version of the cache entry. * * @return bool true if the URL exists with either HTTP or HTTPS scheme and has been purged, false otherwise */ public function purge(string $url): bool { $http = preg_replace('#^https:#', 'http:', $url); $https = preg_replace('#^http:#', 'https:', $url); $purgedHttp = $this->doPurge($http); $purgedHttps = $this->doPurge($https); return $purgedHttp || $purgedHttps; } /** * Purges data for the given URL. */ private function doPurge(string $url): bool { $key = $this->getCacheKey(Request::create($url)); if (isset($this->locks[$key])) { flock($this->locks[$key], \LOCK_UN); fclose($this->locks[$key]); unset($this->locks[$key]); } if (is_file($path = $this->getPath($key))) { unlink($path); return true; } return false; } /** * Loads data for the given key. */ private function load(string $key): ?string { $path = $this->getPath($key); return is_file($path) && false !== ($contents = @file_get_contents($path)) ? $contents : null; } /** * Save data for the given key. */ private function save(string $key, string $data, bool $overwrite = true): bool { $path = $this->getPath($key); if (!$overwrite && file_exists($path)) { return true; } if (isset($this->locks[$key])) { $fp = $this->locks[$key]; @ftruncate($fp, 0); @fseek($fp, 0); $len = @fwrite($fp, $data); if (\strlen($data) !== $len) { @ftruncate($fp, 0); return false; } } else { if (!is_dir(\dirname($path)) && false === @mkdir(\dirname($path), 0o777, true) && !is_dir(\dirname($path))) { return false; } $tmpFile = tempnam(\dirname($path), basename($path)); if (false === $fp = @fopen($tmpFile, 'w')) { @unlink($tmpFile); return false; } @fwrite($fp, $data); @fclose($fp); if ($data != file_get_contents($tmpFile)) { @unlink($tmpFile); return false; } if (false === @rename($tmpFile, $path)) { @unlink($tmpFile); return false; } } @chmod($path, 0o666 & ~umask()); return true; } public function getPath(string $key): string { return $this->root.\DIRECTORY_SEPARATOR.substr($key, 0, 2).\DIRECTORY_SEPARATOR.substr($key, 2, 2).\DIRECTORY_SEPARATOR.substr($key, 4, 2).\DIRECTORY_SEPARATOR.substr($key, 6); } /** * Generates a cache key for the given Request. * * This method should return a key that must only depend on a * normalized version of the request URI. * * If the same URI can have more than one representation, based on some * headers, use a Vary header to indicate them, and each representation will * be stored independently under the same cache key. */ protected function generateCacheKey(Request $request): string { $key = $request->getUri(); if ('QUERY' === $request->getMethod()) { // add null byte to separate the URI from the body and avoid boundary collisions // which could lead to cache poisoning $key .= "\0".$request->getContent(); } return 'md'.hash('sha256', $key); } /** * Returns a cache key for the given Request. */ private function getCacheKey(Request $request): string { if (isset($this->keyCache[$request])) { return $this->keyCache[$request]; } return $this->keyCache[$request] = $this->generateCacheKey($request); } /** * Persists the Request HTTP headers. */ private function persistRequest(Request $request): array { return $request->headers->all(); } /** * Persists the Response HTTP headers. */ private function persistResponse(Response $response): array { $headers = $response->headers->all(); $headers['X-Status'] = [$response->getStatusCode()]; return $headers; } /** * Restores a Response from the HTTP headers and body. */ private function restoreResponse(array $headers, ?string $path = null): ?Response { $status = $headers['X-Status'][0]; unset($headers['X-Status']); $content = null; if (null !== $path) { $headers['X-Body-File'] = [$path]; unset($headers['x-body-file']); if ($headers['X-Body-Eval'] ?? $headers['x-body-eval'] ?? false) { $content = file_get_contents($path); \assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === 24); if (48 > \strlen($content) || substr($content, -24) !== substr($content, 0, 24)) { return null; } } } return new Response($content, $status, $headers); } } ================================================ FILE: HttpCache/StoreInterface.php ================================================ * * This code is partially based on the Rack-Cache library by Ryan Tomayko, * which is released under the MIT license. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\HttpCache; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** * Interface implemented by HTTP cache stores. * * @author Fabien Potencier */ interface StoreInterface { /** * Locates a cached Response for the Request provided. */ public function lookup(Request $request): ?Response; /** * Writes a cache entry to the store for the given Request and Response. * * Existing entries are read and any that match the response are removed. This * method calls write with the new list of cache entries. * * @return string The key under which the response is stored */ public function write(Request $request, Response $response): string; /** * Invalidates all cache entries that match the request. */ public function invalidate(Request $request): void; /** * Locks the cache for a given Request. * * @return bool|string true if the lock is acquired, the path to the current lock otherwise */ public function lock(Request $request): bool|string; /** * Releases the lock for the given Request. * * @return bool False if the lock file does not exist or cannot be unlocked, true otherwise */ public function unlock(Request $request): bool; /** * Returns whether or not a lock exists. * * @return bool true if lock exists, false otherwise */ public function isLocked(Request $request): bool; /** * Purges data for the given URL. * * @return bool true if the URL exists and has been purged, false otherwise */ public function purge(string $url): bool; /** * Cleanups storage. */ public function cleanup(): void; } ================================================ FILE: HttpCache/SubRequestHandler.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\HttpCache; use Symfony\Component\HttpFoundation\IpUtils; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; /** * @author Nicolas Grekas * * @internal */ class SubRequestHandler { public static function handle(HttpKernelInterface $kernel, Request $request, int $type, bool $catch): Response { // save global state related to trusted headers and proxies $trustedProxies = Request::getTrustedProxies(); $trustedHeaderSet = Request::getTrustedHeaderSet(); // remove untrusted values $remoteAddr = $request->server->get('REMOTE_ADDR'); if (!$remoteAddr || !IpUtils::checkIp($remoteAddr, $trustedProxies)) { $trustedHeaders = [ 'FORWARDED' => $trustedHeaderSet & Request::HEADER_FORWARDED, 'X_FORWARDED_FOR' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_FOR, 'X_FORWARDED_HOST' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_HOST, 'X_FORWARDED_PROTO' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_PROTO, 'X_FORWARDED_PORT' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_PORT, 'X_FORWARDED_PREFIX' => $trustedHeaderSet & Request::HEADER_X_FORWARDED_PREFIX, ]; foreach (array_filter($trustedHeaders) as $name => $key) { $request->headers->remove($name); $request->server->remove('HTTP_'.$name); } } // compute trusted values, taking any trusted proxies into account $trustedIps = []; $trustedValues = []; foreach (array_reverse($request->getClientIps()) as $ip) { $trustedIps[] = $ip; $trustedValues[] = \sprintf('for="%s"', $ip); } if ($ip !== $remoteAddr) { $trustedIps[] = $remoteAddr; $trustedValues[] = \sprintf('for="%s"', $remoteAddr); } // set trusted values, reusing as much as possible the global trusted settings if (Request::HEADER_FORWARDED & $trustedHeaderSet) { $trustedValues[0] .= \sprintf(';host="%s";proto=%s', $request->getHttpHost(), $request->getScheme()); $request->headers->set('Forwarded', $v = implode(', ', $trustedValues)); $request->server->set('HTTP_FORWARDED', $v); } if (Request::HEADER_X_FORWARDED_FOR & $trustedHeaderSet) { $request->headers->set('X-Forwarded-For', $v = implode(', ', $trustedIps)); $request->server->set('HTTP_X_FORWARDED_FOR', $v); } elseif (!(Request::HEADER_FORWARDED & $trustedHeaderSet)) { Request::setTrustedProxies($trustedProxies, $trustedHeaderSet | Request::HEADER_X_FORWARDED_FOR); $request->headers->set('X-Forwarded-For', $v = implode(', ', $trustedIps)); $request->server->set('HTTP_X_FORWARDED_FOR', $v); } // fix the client IP address by setting it to 127.0.0.1, // which is the core responsibility of this method $request->server->set('REMOTE_ADDR', '127.0.0.1'); // ensure 127.0.0.1 is set as trusted proxy if (!IpUtils::checkIp('127.0.0.1', $trustedProxies)) { Request::setTrustedProxies(array_merge($trustedProxies, ['127.0.0.1']), Request::getTrustedHeaderSet()); } try { return $kernel->handle($request, $type, $catch); } finally { // restore global state Request::setTrustedProxies($trustedProxies, $trustedHeaderSet); } } } ================================================ FILE: HttpCache/SurrogateInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\HttpCache; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; interface SurrogateInterface { /** * Returns surrogate name. */ public function getName(): string; /** * Returns a new cache strategy instance. */ public function createCacheStrategy(): ResponseCacheStrategyInterface; /** * Checks that at least one surrogate has Surrogate capability. */ public function hasSurrogateCapability(Request $request): bool; /** * Adds Surrogate-capability to the given Request. */ public function addSurrogateCapability(Request $request): void; /** * Adds HTTP headers to specify that the Response needs to be parsed for Surrogate. * * This method only adds an Surrogate HTTP header if the Response has some Surrogate tags. */ public function addSurrogateControl(Response $response): void; /** * Checks that the Response needs to be parsed for Surrogate tags. */ public function needsParsing(Response $response): bool; /** * Renders a Surrogate tag. * * @param string|null $alt An alternate URI * @param string $comment A comment to add as an esi:include tag */ public function renderIncludeTag(string $uri, ?string $alt = null, bool $ignoreErrors = true, string $comment = ''): string; /** * Replaces a Response Surrogate tags with the included resource content. */ public function process(Request $request, Response $response): Response; /** * Handles a Surrogate from the cache. * * @param string $alt An alternative URI * * @throws \RuntimeException * @throws \Exception */ public function handle(HttpCache $cache, string $uri, string $alt, bool $ignoreErrors): string; } ================================================ FILE: HttpClientKernel.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\Mime\Part\AbstractPart; use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\Mime\Part\Multipart\FormDataPart; use Symfony\Component\Mime\Part\TextPart; use Symfony\Contracts\HttpClient\HttpClientInterface; // Help opcache.preload discover always-needed symbols class_exists(ResponseHeaderBag::class); /** * An implementation of a Symfony HTTP kernel using a "real" HTTP client. * * @author Fabien Potencier */ final class HttpClientKernel implements HttpKernelInterface { private HttpClientInterface $client; public function __construct(?HttpClientInterface $client = null) { if (null === $client && !class_exists(HttpClient::class)) { throw new \LogicException(\sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__)); } $this->client = $client ?? HttpClient::create(); } public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response { $headers = $this->getHeaders($request); $body = ''; if (null !== $part = $this->getBody($request)) { $headers = array_merge($headers, $part->getPreparedHeaders()->toArray()); $body = $part->bodyToIterable(); } $response = $this->client->request($request->getMethod(), $request->getUri(), [ 'headers' => $headers, 'body' => $body, ] + $request->attributes->get('http_client_options', [])); $headers = new class($response->getHeaders(!$catch)) extends ResponseHeaderBag { protected function computeCacheControlValue(): string { return $this->getCacheControlHeader(); // preserve the original value } }; $headers->remove('X-Body-File'); $headers->remove('X-Body-Eval'); $headers->remove('X-Content-Digest'); try { return new Response($response->getContent(!$catch), $response->getStatusCode(), $headers); } catch (\TypeError) { // BC with Symfony < 8.1 $response = new Response($response->getContent(!$catch), $response->getStatusCode()); $response->headers = $headers; return $response; } } private function getBody(Request $request): ?AbstractPart { if (\in_array($request->getMethod(), ['GET', 'HEAD'], true)) { return null; } if (!class_exists(AbstractPart::class)) { throw new \LogicException('You cannot pass non-empty bodies as the Mime component is not installed. Try running "composer require symfony/mime".'); } if ($content = $request->getContent()) { return new TextPart($content, 'utf-8', 'plain', '8bit'); } $fields = $request->request->all(); foreach ($request->files->all() as $name => $file) { $fields[$name] = DataPart::fromPath($file->getPathname(), $file->getClientOriginalName(), $file->getClientMimeType()); } return new FormDataPart($fields); } private function getHeaders(Request $request): array { $headers = []; foreach ($request->headers as $key => $value) { $headers[$key] = $value; } $cookies = []; foreach ($request->cookies->all() as $name => $value) { $cookies[] = $name.'='.$value; } if ($cookies) { $headers['cookie'] = implode('; ', $cookies); } return $headers; } } ================================================ FILE: HttpKernel.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel; use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerArgumentsMetadata; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ControllerMetadata; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\HttpKernel\Event\ViewEvent; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\ControllerDoesNotReturnResponseException; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; // Help opcache.preload discover always-needed symbols class_exists(ControllerArgumentsEvent::class); class_exists(ControllerArgumentsMetadata::class); class_exists(ControllerEvent::class); class_exists(ExceptionEvent::class); class_exists(FinishRequestEvent::class); class_exists(RequestEvent::class); class_exists(ResponseEvent::class); class_exists(TerminateEvent::class); class_exists(ViewEvent::class); class_exists(KernelEvents::class); /** * HttpKernel notifies events to convert a Request object to a Response one. * * @author Fabien Potencier */ class HttpKernel implements HttpKernelInterface, TerminableInterface { protected RequestStack $requestStack; private ArgumentResolverInterface $argumentResolver; private bool $terminating = false; public function __construct( protected EventDispatcherInterface $dispatcher, protected ControllerResolverInterface $resolver, ?RequestStack $requestStack = null, ?ArgumentResolverInterface $argumentResolver = null, private bool $handleAllThrowables = false, ) { $this->requestStack = $requestStack ?? new RequestStack(); $this->argumentResolver = $argumentResolver ?? new ArgumentResolver(); } public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response { $request->headers->set('X-Php-Ob-Level', (string) ob_get_level()); $this->requestStack->push($request); $response = null; try { return $response = $this->handleRaw($request, $type, $controllerMetadata); } catch (\Throwable $e) { if ($e instanceof \Error && !$this->handleAllThrowables) { throw $e; } if ($e instanceof RequestExceptionInterface) { $e = new BadRequestHttpException($e->getMessage(), $e); } if (false === $catch) { $this->finishRequest($request, $type, $controllerMetadata); throw $e; } return $response = $this->handleThrowable($e, $request, $type, $controllerMetadata); } finally { $this->requestStack->pop(); if ($response instanceof StreamedResponse && $callback = $response->getCallback()) { $requestStack = $this->requestStack; $response->setCallback(static function () use ($request, $callback, $requestStack) { $requestStack->push($request); try { $callback(); } finally { $requestStack->pop(); } }); } } } public function terminate(Request $request, Response $response): void { try { $this->terminating = true; $this->dispatcher->dispatch(new TerminateEvent($this, $request, $response), KernelEvents::TERMINATE); } finally { $this->terminating = false; } } /** * @internal */ public function terminateWithException(\Throwable $exception, ?Request $request = null): void { if (!$request ??= $this->requestStack->getMainRequest()) { throw $exception; } if ($pop = $request !== $this->requestStack->getMainRequest()) { $this->requestStack->push($request); } try { $response = $this->handleThrowable($exception, $request, self::MAIN_REQUEST); } finally { if ($pop) { $this->requestStack->pop(); } } $response->sendHeaders(); $response->sendContent(); $this->terminate($request, $response); } /** * Handles a request to convert it to a response. * * Exceptions are not caught. * * @throws \LogicException If one of the listener does not behave as expected * @throws NotFoundHttpException When controller cannot be found */ private function handleRaw(Request $request, int $type = self::MAIN_REQUEST, ?ControllerMetadata &$controllerMetadata = null): Response { // request $event = new RequestEvent($this, $request, $type); $this->dispatcher->dispatch($event, KernelEvents::REQUEST); if ($event->hasResponse()) { return $this->filterResponse($event->getResponse(), $request, $type); } // load controller if (false === $controller = $this->resolver->getController($request)) { throw new NotFoundHttpException(\sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getPathInfo())); } $controllerEvent = $event = new ControllerEvent($this, $controller, $request, $type); $controllerMetadata = new ControllerMetadata($event); $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER); $controller = $event->getController(); // controller arguments $arguments = $this->argumentResolver->getArguments($request, $controller, $event->getControllerReflector()); $event = new ControllerArgumentsEvent($this, $event, $arguments, $request, $type); $controllerMetadata = new ControllerArgumentsMetadata($controllerEvent, $event); $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS); $controller = $event->getController(); $arguments = $event->getArguments(); // call controller $response = $controller(...$arguments); // view if (!$response instanceof Response) { $event = new ViewEvent($this, $request, $type, $response, $controllerMetadata); $this->dispatcher->dispatch($event, KernelEvents::VIEW); if ($event->hasResponse()) { $response = $event->getResponse(); } else { $msg = \sprintf('The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned %s.', $this->varToString($response)); // the user may have forgotten to return something if (null === $response) { $msg .= ' Did you forget to add a return statement somewhere in your controller?'; } throw new ControllerDoesNotReturnResponseException($msg, $controller, __FILE__, __LINE__ - 17); } } return $this->filterResponse($response, $request, $type, $controllerMetadata); } /** * Filters a response object. * * @throws \RuntimeException if the passed object is not a Response instance */ private function filterResponse(Response $response, Request $request, int $type, ?ControllerMetadata $controllerMetadata = null): Response { $event = new ResponseEvent($this, $request, $type, $response, $controllerMetadata); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->finishRequest($request, $type, $controllerMetadata); return $event->getResponse(); } /** * Publishes the finish request event, then pop the request from the stack. * * Note that the order of the operations is important here, otherwise * operations such as {@link RequestStack::getParentRequest()} can lead to * weird results. */ private function finishRequest(Request $request, int $type, ?ControllerMetadata $controllerMetadata = null): void { $this->dispatcher->dispatch(new FinishRequestEvent($this, $request, $type, $controllerMetadata), KernelEvents::FINISH_REQUEST); } /** * Handles a throwable by trying to convert it to a Response. */ private function handleThrowable(\Throwable $e, Request $request, int $type, ?ControllerMetadata $controllerMetadata = null): Response { $event = new ExceptionEvent($this, $request, $type, $e, isKernelTerminating: $this->terminating, controllerMetadata: $controllerMetadata); $this->dispatcher->dispatch($event, KernelEvents::EXCEPTION); // a listener might have replaced the exception $e = $event->getThrowable(); if (!$event->hasResponse()) { $this->finishRequest($request, $type, $controllerMetadata); throw $e; } $response = $event->getResponse(); // the developer asked for a specific status code if (!$event->isAllowingCustomResponseCode() && !$response->isClientError() && !$response->isServerError() && !$response->isRedirect()) { // ensure that we actually have an error response if ($e instanceof HttpExceptionInterface) { // keep the HTTP status code and headers $response->setStatusCode($e->getStatusCode()); $response->headers->add($e->getHeaders()); } else { $response->setStatusCode(500); } } try { return $this->filterResponse($response, $request, $type, $controllerMetadata); } catch (\Throwable $e) { if ($e instanceof \Error && !$this->handleAllThrowables) { throw $e; } return $response; } } /** * Returns a human-readable string for the specified variable. */ private function varToString(mixed $var): string { if (\is_object($var)) { return \sprintf('an object of type %s', $var::class); } if (\is_array($var)) { $a = []; foreach ($var as $k => $v) { $a[] = \sprintf('%s => ...', $k); } return \sprintf('an array ([%s])', mb_substr(implode(', ', $a), 0, 255)); } if (\is_resource($var)) { return \sprintf('a resource (%s)', get_resource_type($var)); } if (null === $var) { return 'null'; } if (false === $var) { return 'a boolean value (false)'; } if (true === $var) { return 'a boolean value (true)'; } if (\is_string($var)) { return \sprintf('a string ("%s%s")', mb_substr($var, 0, 255), mb_strlen($var) > 255 ? '...' : ''); } if (is_numeric($var)) { return \sprintf('a number (%s)', (string) $var); } return (string) $var; } } ================================================ FILE: HttpKernelBrowser.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel; use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\BrowserKit\CookieJar; use Symfony\Component\BrowserKit\History; use Symfony\Component\BrowserKit\Request as DomRequest; use Symfony\Component\BrowserKit\Response as DomResponse; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** * Simulates a browser and makes requests to an HttpKernel instance. * * @author Fabien Potencier * * @template-extends AbstractBrowser */ class HttpKernelBrowser extends AbstractBrowser { private bool $catchExceptions = true; /** * @param array $server The server parameters (equivalent of $_SERVER) */ public function __construct( protected HttpKernelInterface $kernel, array $server = [], ?History $history = null, ?CookieJar $cookieJar = null, ) { // These class properties must be set before calling the parent constructor, as it may depend on it. $this->followRedirects = false; parent::__construct($server, $history, $cookieJar); } /** * Sets whether to catch exceptions when the kernel is handling a request. */ public function catchExceptions(bool $catchExceptions): void { $this->catchExceptions = $catchExceptions; } /** * @param Request $request */ protected function doRequest(object $request): Response { $response = $this->kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, $this->catchExceptions); if ($this->kernel instanceof TerminableInterface) { $this->kernel->terminate($request, $response); } return $response; } /** * @param Request $request */ protected function getScript(object $request): string { $kernel = var_export(serialize($this->kernel), true); $request = var_export(serialize($request), true); $errorReporting = error_reporting(); $requires = ''; foreach (get_declared_classes() as $class) { if (str_starts_with($class, 'ComposerAutoloaderInit')) { $r = new \ReflectionClass($class); $file = \dirname($r->getFileName(), 2).'/autoload.php'; if (file_exists($file)) { $requires .= 'require_once '.var_export($file, true).";\n"; } } } if (!$requires) { throw new \RuntimeException('Composer autoloader not found.'); } $code = <<getHandleScript(); } protected function getHandleScript(): string { return <<<'EOF' $response = $kernel->handle($request); if ($kernel instanceof Symfony\Component\HttpKernel\TerminableInterface) { $kernel->terminate($request, $response); } echo serialize($response); EOF; } protected function filterRequest(DomRequest $request): Request { $httpRequest = Request::create($request->getUri(), $request->getMethod(), $request->getParameters(), $request->getCookies(), $request->getFiles(), $server = $request->getServer(), $request->getContent()); if (!isset($server['HTTP_ACCEPT'])) { $httpRequest->headers->remove('Accept'); } foreach ($this->filterFiles($httpRequest->files->all()) as $key => $value) { $httpRequest->files->set($key, $value); } return $httpRequest; } /** * Filters an array of files. * * This method created test instances of UploadedFile so that the move() * method can be called on those instances. * * If the size of a file is greater than the allowed size (from php.ini) then * an invalid UploadedFile is returned with an error set to UPLOAD_ERR_INI_SIZE. * * @see UploadedFile */ protected function filterFiles(array $files): array { $filtered = []; foreach ($files as $key => $value) { if (\is_array($value)) { $filtered[$key] = $this->filterFiles($value); } elseif ($value instanceof UploadedFile) { if ($value->isValid() && $value->getSize() > UploadedFile::getMaxFilesize()) { $filtered[$key] = new UploadedFile( '', $value->getClientOriginalName(), $value->getClientMimeType(), \UPLOAD_ERR_INI_SIZE, true ); } else { $filtered[$key] = new UploadedFile( $value->getPathname(), $value->getClientOriginalName(), $value->getClientMimeType(), $value->getError(), true ); } } } return $filtered; } /** * @param Response $response */ protected function filterResponse(object $response): DomResponse { $content = ''; ob_start(static function ($chunk) use (&$content) { $content .= $chunk; return ''; }); try { $response->sendContent(); } finally { ob_end_clean(); } return new DomResponse($content, $response->getStatusCode(), $response->headers->all()); } } ================================================ FILE: HttpKernelInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** * HttpKernelInterface handles a Request to convert it to a Response. * * @author Fabien Potencier */ interface HttpKernelInterface { public const MAIN_REQUEST = 1; public const SUB_REQUEST = 2; /** * Handles a Request to convert it to a Response. * * When $catch is true, the implementation must catch all exceptions * and do its best to convert them to a Response instance. * * @param int $type The type of the request * (one of HttpKernelInterface::MAIN_REQUEST or HttpKernelInterface::SUB_REQUEST) * @param bool $catch Whether to catch exceptions or not * * @throws \Exception When an Exception occurs during processing */ public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response; } ================================================ FILE: Kernel.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel; use Symfony\Component\Config\ConfigCache; use Symfony\Component\Config\Loader\DelegatingLoader; use Symfony\Component\Config\Loader\LoaderResolver; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Compiler\RemoveBuildParametersPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; use Symfony\Component\DependencyInjection\Dumper\Preloader; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\DependencyInjection\Loader\DirectoryLoader; use Symfony\Component\DependencyInjection\Loader\GlobFileLoader; use Symfony\Component\DependencyInjection\Loader\IniFileLoader; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\ErrorHandler\DebugClassLoader; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\HttpKernel\Config\FileLocator; use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass; // Help opcache.preload discover always-needed symbols class_exists(ConfigCache::class); /** * The Kernel is the heart of the Symfony system. * * It manages an environment made of bundles. * * Environment names must always start with a letter and * they must only contain letters and numbers. * * @author Fabien Potencier */ abstract class Kernel implements KernelInterface, RebootableInterface, TerminableInterface { /** * @var array */ protected array $bundles = []; protected ?ContainerInterface $container = null; protected bool $booted = false; protected ?float $startTime = null; private string $projectDir; private ?string $warmupDir = null; private int $requestStackSize = 0; private bool $resetServices = false; private bool $handlingHttpCache = false; /** * @var array */ private static array $freshCache = []; public const VERSION = '8.1.0-DEV'; public const VERSION_ID = 80100; public const MAJOR_VERSION = 8; public const MINOR_VERSION = 1; public const RELEASE_VERSION = 0; public const EXTRA_VERSION = 'DEV'; public const END_OF_MAINTENANCE = '01/2027'; public const END_OF_LIFE = '01/2027'; public function __construct( protected string $environment, protected bool $debug, ) { if (!$environment) { throw new \InvalidArgumentException(\sprintf('Invalid environment provided to "%s": the environment cannot be empty.', get_debug_type($this))); } } public function __clone() { $this->booted = false; $this->container = null; $this->requestStackSize = 0; $this->resetServices = false; $this->handlingHttpCache = false; } public function boot(): void { if ($this->booted) { if (!$this->requestStackSize && $this->resetServices) { if ($this->container->has('services_resetter')) { $this->container->get('services_resetter')->reset(); } $this->resetServices = false; if ($this->debug) { $this->startTime = microtime(true); } } return; } if (!$this->container) { $this->preBoot(); } foreach ($this->getBundles() as $bundle) { $bundle->setContainer($this->container); $bundle->boot(); } $this->booted = true; } public function reboot(?string $warmupDir): void { $this->shutdown(); $this->warmupDir = $warmupDir; $this->boot(); } public function terminate(Request $request, Response $response): void { if (!$this->booted) { return; } if ($this->getHttpKernel() instanceof TerminableInterface) { $this->getHttpKernel()->terminate($request, $response); } } public function shutdown(): void { if (!$this->booted) { return; } $this->booted = false; foreach ($this->getBundles() as $bundle) { $bundle->shutdown(); $bundle->setContainer(null); } $this->container = null; $this->requestStackSize = 0; $this->resetServices = false; } public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response { if (!$this->container) { $this->preBoot(); } if (HttpKernelInterface::MAIN_REQUEST === $type && !$this->handlingHttpCache && $this->container->has('http_cache')) { $this->handlingHttpCache = true; try { return $this->container->get('http_cache')->handle($request, $type, $catch); } finally { $this->handlingHttpCache = false; $this->resetServices = true; } } $this->boot(); ++$this->requestStackSize; if (!$this->handlingHttpCache) { $this->resetServices = true; } try { return $this->getHttpKernel()->handle($request, $type, $catch); } finally { --$this->requestStackSize; } } /** * Gets an HTTP kernel from the container. */ protected function getHttpKernel(): HttpKernelInterface { return $this->container->get('http_kernel'); } public function getBundles(): array { return $this->bundles; } public function getBundle(string $name): BundleInterface { if (!isset($this->bundles[$name])) { throw new \InvalidArgumentException(\sprintf('Bundle "%s" does not exist or it is not enabled. Maybe you forgot to add it in the "registerBundles()" method of your "%s.php" file?', $name, get_debug_type($this))); } return $this->bundles[$name]; } public function locateResource(string $name): string { if ('@' !== $name[0]) { throw new \InvalidArgumentException(\sprintf('A resource name must start with @ ("%s" given).', $name)); } if (str_contains($name, '..')) { throw new \RuntimeException(\sprintf('File name "%s" contains invalid characters (..).', $name)); } $bundleName = substr($name, 1); $path = ''; if (str_contains($bundleName, '/')) { [$bundleName, $path] = explode('/', $bundleName, 2); } $bundle = $this->getBundle($bundleName); if (file_exists($file = $bundle->getPath().'/'.$path)) { return $file; } throw new \InvalidArgumentException(\sprintf('Unable to find file "%s".', $name)); } public function getEnvironment(): string { return $this->environment; } public function isDebug(): bool { return $this->debug; } /** * Gets the application root dir (path of the project's composer file). */ public function getProjectDir(): string { if (!isset($this->projectDir)) { $r = new \ReflectionObject($this); if (!is_file($dir = $r->getFileName())) { throw new \LogicException(\sprintf('Cannot auto-detect project dir for kernel of class "%s".', $r->name)); } $dir = $rootDir = \dirname($dir); while (!is_file($dir.'/composer.json')) { if ($dir === \dirname($dir)) { return $this->projectDir = $rootDir; } $dir = \dirname($dir); } $this->projectDir = $dir; } return $this->projectDir; } public function getContainer(): ContainerInterface { if (!$this->container) { throw new \LogicException('Cannot retrieve the container from a non-booted kernel.'); } return $this->container; } public function getStartTime(): float { return $this->debug && null !== $this->startTime ? $this->startTime : -\INF; } public function getCacheDir(): string { return $this->getProjectDir().'/var/cache/'.$this->environment; } public function getBuildDir(): string { // Returns $this->getCacheDir() for backward compatibility return $this->getCacheDir(); } public function getShareDir(): ?string { // Returns $this->getCacheDir() for backward compatibility return $this->getCacheDir(); } public function getLogDir(): string { return $this->getProjectDir().'/var/log'; } public function getCharset(): string { return 'UTF-8'; } /** * Initializes bundles. * * @throws \LogicException if two bundles share a common name */ protected function initializeBundles(): void { // init bundles $this->bundles = []; foreach ($this->registerBundles() as $bundle) { $name = $bundle->getName(); if (isset($this->bundles[$name])) { throw new \LogicException(\sprintf('Trying to register two bundles with the same name "%s".', $name)); } $this->bundles[$name] = $bundle; } } /** * The extension point similar to the Bundle::build() method. * * Use this method to register compiler passes and manipulate the container during the building process. */ protected function build(ContainerBuilder $container): void { } /** * Gets the container class. * * @throws \InvalidArgumentException If the generated classname is invalid */ protected function getContainerClass(): string { $class = static::class; $class = str_contains($class, "@anonymous\0") ? get_parent_class($class).str_replace('.', '_', ContainerBuilder::hash($class)) : $class; $class = str_replace('\\', '_', $class).ucfirst($this->environment).($this->debug ? 'Debug' : '').'Container'; if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $class)) { throw new \InvalidArgumentException(\sprintf('The environment "%s" contains invalid characters, it can only contain characters allowed in PHP class names.', $this->environment)); } return $class; } /** * Gets the container's base class. * * All names except Container must be fully qualified. */ protected function getContainerBaseClass(): string { return 'Container'; } /** * Initializes the service container. * * The built version of the service container is used when fresh, otherwise the * container is built. */ protected function initializeContainer(): void { $class = $this->getContainerClass(); $buildDir = $this->warmupDir ?: $this->getBuildDir(); $skip = $_SERVER['SYMFONY_DISABLE_RESOURCE_TRACKING'] ?? ''; $skip = filter_var($skip, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE) ?? explode(',', $skip); $cache = new ConfigCache($buildDir.'/'.$class.'.php', $this->debug, null, \is_array($skip) && ['*'] !== $skip ? $skip : ($skip ? [] : null)); $cachePath = $cache->getPath(); // Silence E_WARNING to ignore "include" failures - don't use "@" to prevent silencing fatal errors $errorLevel = error_reporting(); error_reporting($errorLevel & ~\E_WARNING); try { if (is_file($cachePath) && \is_object($this->container = include $cachePath) && (!$this->debug || (self::$freshCache[$cachePath] ?? $cache->isFresh())) ) { self::$freshCache[$cachePath] = true; $this->container->set('kernel', $this); error_reporting($errorLevel); return; } } catch (\Throwable $e) { } $oldContainer = \is_object($this->container) ? new \ReflectionClass($this->container) : $this->container = null; try { is_dir($buildDir) ?: mkdir($buildDir, 0o777, true); if ($lock = fopen($cachePath.'.lock', 'w+')) { if (!flock($lock, \LOCK_EX | \LOCK_NB, $wouldBlock) && !flock($lock, $wouldBlock ? \LOCK_SH : \LOCK_EX)) { fclose($lock); $lock = null; } elseif (!is_file($cachePath) || !\is_object($this->container = include $cachePath)) { $this->container = null; } elseif (!$oldContainer || $this->container::class !== $oldContainer->name) { flock($lock, \LOCK_UN); fclose($lock); $this->container->set('kernel', $this); return; } } } catch (\Throwable $e) { } finally { error_reporting($errorLevel); } if ($collectDeprecations = $this->debug && !\defined('PHPUNIT_COMPOSER_INSTALL')) { $collectedLogs = []; $previousHandler = set_error_handler(static function ($type, $message, $file, $line) use (&$collectedLogs, &$previousHandler) { if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type) { return $previousHandler ? $previousHandler($type, $message, $file, $line) : false; } if (isset($collectedLogs[$message])) { ++$collectedLogs[$message]['count']; return null; } $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 5); // Clean the trace by removing first frames added by the error handler itself. for ($i = 0; isset($backtrace[$i]); ++$i) { if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) { $backtrace = \array_slice($backtrace, 1 + $i); break; } } for ($i = 0; isset($backtrace[$i]); ++$i) { if (!isset($backtrace[$i]['file'], $backtrace[$i]['line'], $backtrace[$i]['function'])) { continue; } if (!isset($backtrace[$i]['class']) && 'trigger_deprecation' === $backtrace[$i]['function']) { $file = $backtrace[$i]['file']; $line = $backtrace[$i]['line']; $backtrace = \array_slice($backtrace, 1 + $i); break; } } // Remove frames added by DebugClassLoader. for ($i = \count($backtrace) - 2; 0 < $i; --$i) { if (DebugClassLoader::class === ($backtrace[$i]['class'] ?? null)) { $backtrace = [$backtrace[$i + 1]]; break; } } $collectedLogs[$message] = [ 'type' => $type, 'message' => $message, 'file' => $file, 'line' => $line, 'trace' => [$backtrace[0]], 'count' => 1, ]; return null; }); } try { $container = null; $container = $this->buildContainer(); $container->compile(); } finally { if ($collectDeprecations) { restore_error_handler(); @file_put_contents($buildDir.'/'.$class.'Deprecations.log', serialize(array_values($collectedLogs))); @file_put_contents($buildDir.'/'.$class.'Compiler.log', null !== $container ? implode("\n", $container->getCompiler()->getLog()) : ''); } } $this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass()); if ($lock) { flock($lock, \LOCK_UN); fclose($lock); } $this->container = require $cachePath; $this->container->set('kernel', $this); if ($oldContainer && $this->container::class !== $oldContainer->name) { // Because concurrent requests might still be using them, // old container files are not removed immediately, // but on a next dump of the container. static $legacyContainers = []; $oldContainerDir = \dirname($oldContainer->getFileName()); $legacyContainers[$oldContainerDir.'.legacy'] = true; foreach (glob(\dirname($oldContainerDir).\DIRECTORY_SEPARATOR.'*.legacy', \GLOB_NOSORT) as $legacyContainer) { if (!isset($legacyContainers[$legacyContainer]) && @unlink($legacyContainer)) { (new Filesystem())->remove(substr($legacyContainer, 0, -7)); } } touch($oldContainerDir.'.legacy'); } $buildDir = $this->container->getParameter('kernel.build_dir'); $cacheDir = $this->container->getParameter('kernel.cache_dir'); $preload = $this instanceof WarmableInterface ? $this->warmUp($cacheDir, $buildDir) : []; if ($this->container->has('cache_warmer')) { $cacheWarmer = $this->container->get('cache_warmer'); if ($cacheDir !== $buildDir) { $cacheWarmer->enableOptionalWarmers(); } $preload = array_merge($preload, $cacheWarmer->warmUp($cacheDir, $buildDir)); } if ($preload && file_exists($preloadFile = $buildDir.'/'.$class.'.preload.php')) { Preloader::append($preloadFile, $preload); } } /** * Returns the kernel parameters. * * @return array */ protected function getKernelParameters(): array { $bundles = []; $bundlesMetadata = []; foreach ($this->bundles as $name => $bundle) { $bundles[$name] = $bundle::class; $bundlesMetadata[$name] = [ 'path' => $bundle->getPath(), 'namespace' => $bundle->getNamespace(), ]; } return [ 'kernel.project_dir' => realpath($this->getProjectDir()) ?: $this->getProjectDir(), 'kernel.environment' => $this->environment, 'kernel.runtime_environment' => '%env(default:kernel.environment:APP_RUNTIME_ENV)%', 'kernel.runtime_mode' => '%env(query_string:default:container.runtime_mode:APP_RUNTIME_MODE)%', 'kernel.runtime_mode.web' => '%env(bool:default::key:web:default:kernel.runtime_mode:)%', 'kernel.runtime_mode.cli' => '%env(not:default:kernel.runtime_mode.web:)%', 'kernel.runtime_mode.worker' => '%env(bool:default::key:worker:default:kernel.runtime_mode:)%', 'kernel.debug' => $this->debug, 'kernel.build_dir' => realpath($dir = $this->warmupDir ?: $this->getBuildDir()) ?: $dir, 'kernel.cache_dir' => realpath($dir = ($this->getCacheDir() === $this->getBuildDir() ? ($this->warmupDir ?: $this->getCacheDir()) : $this->getCacheDir())) ?: $dir, 'kernel.logs_dir' => realpath($dir = $this->getLogDir()) ?: $dir, 'kernel.bundles' => $bundles, 'kernel.bundles_metadata' => $bundlesMetadata, 'kernel.charset' => $this->getCharset(), 'kernel.container_class' => $this->getContainerClass(), ] + (null !== ($dir = $this->getShareDir()) ? ['kernel.share_dir' => realpath($dir) ?: $dir] : []); } /** * Builds the service container. * * @throws \RuntimeException */ protected function buildContainer(): ContainerBuilder { foreach (['cache' => $this->getCacheDir(), 'build' => $this->warmupDir ?: $this->getBuildDir()] as $name => $dir) { if (!is_dir($dir)) { if (!@mkdir($dir, 0o777, true) && !is_dir($dir)) { throw new \RuntimeException(\sprintf('Unable to create the "%s" directory (%s).', $name, $dir)); } } elseif (!is_writable($dir)) { throw new \RuntimeException(\sprintf('Unable to write in the "%s" directory (%s).', $name, $dir)); } } $container = $this->getContainerBuilder(); $container->addObjectResource($this); $this->prepareContainer($container); $this->registerContainerConfiguration($this->getContainerLoader($container)); return $container; } /** * Prepares the ContainerBuilder before it is compiled. */ protected function prepareContainer(ContainerBuilder $container): void { $extensions = []; foreach ($this->bundles as $bundle) { if ($extension = $bundle->getContainerExtension()) { $container->registerExtension($extension); } if ($this->debug) { $container->addObjectResource($bundle); } if ($bundle instanceof CompilerPassInterface) { $container->addCompilerPass($bundle, PassConfig::TYPE_BEFORE_OPTIMIZATION, -10000); } } foreach ($this->bundles as $bundle) { $bundle->build($container); } $this->build($container); foreach ($container->getExtensions() as $extension) { $extensions[] = $extension->getAlias(); } // ensure these extensions are implicitly loaded $container->getCompilerPassConfig()->setMergePass(new MergeExtensionConfigurationPass($extensions)); } /** * Gets a new ContainerBuilder instance used to build the service container. */ protected function getContainerBuilder(): ContainerBuilder { $container = new ContainerBuilder(); $container->getParameterBag()->add($this->getKernelParameters()); if ($this instanceof ExtensionInterface) { $container->registerExtension($this); } if ($this instanceof CompilerPassInterface) { $container->addCompilerPass($this, PassConfig::TYPE_BEFORE_OPTIMIZATION, -10000); } return $container; } /** * Dumps the service container to PHP code in the cache. * * @param string $class The name of the class to generate * @param string $baseClass The name of the container's base class */ protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container, string $class, string $baseClass): void { // cache the container $dumper = new PhpDumper($container); $buildParameters = []; foreach ($container->getCompilerPassConfig()->getPasses() as $pass) { if ($pass instanceof RemoveBuildParametersPass) { $buildParameters = array_merge($buildParameters, $pass->getRemovedParameters()); } } if (null === $buildTime = filter_var($_SERVER['SOURCE_DATE_EPOCH'] ?? null, \FILTER_VALIDATE_INT, \FILTER_NULL_ON_FAILURE)) { $buildTime = time(); } $content = $dumper->dump([ 'class' => $class, 'base_class' => $baseClass, 'file' => $cache->getPath(), 'as_files' => true, 'debug' => $this->debug, 'inline_factories' => $buildParameters['.container.dumper.inline_factories'] ?? false, 'inline_class_loader' => $buildParameters['.container.dumper.inline_class_loader'] ?? $this->debug, 'build_time' => $container->hasParameter('kernel.container_build_time') ? $container->getParameter('kernel.container_build_time') : $buildTime, 'preload_classes' => array_map('get_class', $this->bundles), ]); $rootCode = array_pop($content); $dir = \dirname($cache->getPath()).'/'; $fs = new Filesystem(); foreach ($content as $file => $code) { $fs->dumpFile($dir.$file, $code); @chmod($dir.$file, 0o666 & ~umask()); } $legacyFile = \dirname($dir.key($content)).'.legacy'; if (is_file($legacyFile)) { @unlink($legacyFile); } $cache->write($rootCode, $container->getResources()); } /** * Returns a loader for the container. */ protected function getContainerLoader(ContainerInterface $container): DelegatingLoader { $env = $this->getEnvironment(); $locator = new FileLocator($this); $resolver = new LoaderResolver([ new YamlFileLoader($container, $locator, $env), new IniFileLoader($container, $locator, $env), new PhpFileLoader($container, $locator, $env), new GlobFileLoader($container, $locator, $env), new DirectoryLoader($container, $locator, $env), new ClosureLoader($container, $env), ]); return new DelegatingLoader($resolver); } private function preBoot(): ContainerInterface { if ($this->debug) { $this->startTime = microtime(true); } if ($this->debug && !isset($_ENV['SHELL_VERBOSITY']) && !isset($_SERVER['SHELL_VERBOSITY'])) { if (\function_exists('putenv')) { putenv('SHELL_VERBOSITY=3'); } $_ENV['SHELL_VERBOSITY'] = 3; $_SERVER['SHELL_VERBOSITY'] = 3; } $this->initializeBundles(); $this->initializeContainer(); $container = $this->container; if ($container->hasParameter('kernel.trusted_hosts') && $trustedHosts = $container->getParameter('kernel.trusted_hosts')) { Request::setTrustedHosts(\is_array($trustedHosts) ? $trustedHosts : preg_split('/\s*+,\s*+(?![^{]*})/', $trustedHosts)); } if ($container->hasParameter('kernel.trusted_proxies') && $container->hasParameter('kernel.trusted_headers') && $trustedProxies = $container->getParameter('kernel.trusted_proxies')) { $trustedHeaders = $container->getParameter('kernel.trusted_headers'); if (\is_string($trustedHeaders)) { $trustedHeaders = array_map('trim', explode(',', $trustedHeaders)); } if (\is_array($trustedHeaders)) { $trustedHeaderSet = 0; foreach ($trustedHeaders as $header) { if (!\defined($const = Request::class.'::HEADER_'.strtr(strtoupper($header), '-', '_'))) { throw new \InvalidArgumentException(\sprintf('The trusted header "%s" is not supported.', $header)); } $trustedHeaderSet |= \constant($const); } } else { $trustedHeaderSet = $trustedHeaders ?? (Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO); } Request::setTrustedProxies(\is_array($trustedProxies) ? $trustedProxies : array_map('trim', explode(',', $trustedProxies)), $trustedHeaderSet); } return $container; } public function __serialize(): array { return [ 'environment' => $this->environment, 'debug' => $this->debug, ]; } public function __unserialize(array $data): void { $environment = $data['environment'] ?? $data["\0*\0environment"]; $debug = $data['debug'] ?? $data["\0*\0debug"]; if (\is_object($environment) || \is_object($debug)) { throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); } $this->environment = $environment; $this->debug = $debug; $this->__construct($environment, $debug); } } ================================================ FILE: KernelEvents.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\HttpKernel\Event\ViewEvent; /** * Contains all events thrown in the HttpKernel component. * * @author Bernhard Schussek */ final class KernelEvents { /** * The REQUEST event occurs at the very beginning of request * dispatching. * * This event allows you to create a response for a request before any * other code in the framework is executed. * * @Event("Symfony\Component\HttpKernel\Event\RequestEvent") */ public const REQUEST = 'kernel.request'; /** * The EXCEPTION event occurs when an uncaught exception appears. * * This event allows you to create a response for a thrown exception or * to modify the thrown exception. * * @Event("Symfony\Component\HttpKernel\Event\ExceptionEvent") */ public const EXCEPTION = 'kernel.exception'; /** * The CONTROLLER event occurs once a controller was found for * handling a request. * * This event allows you to change the controller that will handle the * request. * * @Event("Symfony\Component\HttpKernel\Event\ControllerEvent") */ public const CONTROLLER = 'kernel.controller'; /** * The CONTROLLER_ARGUMENTS event occurs once controller arguments have been resolved. * * This event allows you to change the arguments that will be passed to * the controller. * * @Event("Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent") */ public const CONTROLLER_ARGUMENTS = 'kernel.controller_arguments'; /** * The VIEW event occurs when the return value of a controller * is not a Response instance. * * This event allows you to create a response for the return value of the * controller. * * @Event("Symfony\Component\HttpKernel\Event\ViewEvent") */ public const VIEW = 'kernel.view'; /** * The RESPONSE event occurs once a response was created for * replying to a request. * * This event allows you to modify or replace the response that will be * replied. * * @Event("Symfony\Component\HttpKernel\Event\ResponseEvent") */ public const RESPONSE = 'kernel.response'; /** * The FINISH_REQUEST event occurs when a response was generated for a request. * * This event allows you to reset the global and environmental state of * the application, when it was changed during the request. * * @Event("Symfony\Component\HttpKernel\Event\FinishRequestEvent") */ public const FINISH_REQUEST = 'kernel.finish_request'; /** * The TERMINATE event occurs once a response was sent. * * This event allows you to run expensive post-response jobs. * * @Event("Symfony\Component\HttpKernel\Event\TerminateEvent") */ public const TERMINATE = 'kernel.terminate'; /** * Event aliases. * * These aliases can be consumed by RegisterListenersPass. */ public const ALIASES = [ ControllerArgumentsEvent::class => self::CONTROLLER_ARGUMENTS, ControllerEvent::class => self::CONTROLLER, ResponseEvent::class => self::RESPONSE, FinishRequestEvent::class => self::FINISH_REQUEST, RequestEvent::class => self::REQUEST, ViewEvent::class => self::VIEW, ExceptionEvent::class => self::EXCEPTION, TerminateEvent::class => self::TERMINATE, ]; } ================================================ FILE: KernelInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\Bundle\BundleInterface; /** * The Kernel is the heart of the Symfony system. * * It manages an environment made of application kernel and bundles. * * @author Fabien Potencier */ interface KernelInterface extends HttpKernelInterface { /** * Returns an array of bundles to register. * * @return iterable */ public function registerBundles(): iterable; /** * Loads the container configuration. */ public function registerContainerConfiguration(LoaderInterface $loader): void; /** * Boots the current kernel. */ public function boot(): void; /** * Shutdowns the kernel. * * This method is mainly useful when doing functional testing. */ public function shutdown(): void; /** * Gets the registered bundle instances. * * @return array */ public function getBundles(): array; /** * Returns a bundle. * * @throws \InvalidArgumentException when the bundle is not enabled */ public function getBundle(string $name): BundleInterface; /** * Returns the file path for a given bundle resource. * * A Resource can be a file or a directory. * * The resource name must follow the following pattern: * * "@BundleName/path/to/a/file.something" * * where BundleName is the name of the bundle * and the remaining part is the relative path in the bundle. * * @throws \InvalidArgumentException if the file cannot be found or the name is not valid * @throws \RuntimeException if the name contains invalid/unsafe characters */ public function locateResource(string $name): string; /** * Gets the environment. */ public function getEnvironment(): string; /** * Checks if debug mode is enabled. */ public function isDebug(): bool; /** * Gets the project dir (path of the project's composer file). */ public function getProjectDir(): string; /** * Gets the current container. */ public function getContainer(): ContainerInterface; /** * Gets the request start time (not available if debug is disabled). */ public function getStartTime(): float; /** * Gets the cache directory. * * This directory should be used for caches that are written at runtime. * For caches and artifacts that can be warmed at compile-time and deployed as read-only, * use the "build directory" returned by the {@see getBuildDir()} method. */ public function getCacheDir(): string; /** * Returns the build directory. * * This directory should be used to store build artifacts, and can be read-only at runtime. * System caches written at runtime should be stored in the "cache directory" ({@see KernelInterface::getCacheDir()}). * Application caches that are shared between all front-end servers should be stored * in the "share directory" ({@see KernelInterface::getShareDir()}). */ public function getBuildDir(): string; /** * Returns the share directory. * * This directory should be used to store data that is shared between all front-end servers. * This typically fits application caches. */ public function getShareDir(): ?string; /** * Gets the log directory. */ public function getLogDir(): string; /** * Gets the charset of the application. */ public function getCharset(): string; } ================================================ FILE: LICENSE ================================================ Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Log/DebugLoggerConfigurator.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Log; use Monolog\Logger; /** * @author Nicolas Grekas */ class DebugLoggerConfigurator { private ?object $processor = null; public function __construct(callable $processor, ?bool $enable = null) { if ($enable ?? !\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { $this->processor = \is_object($processor) ? $processor : $processor(...); } } public function pushDebugLogger(Logger $logger): void { if ($this->processor) { $logger->pushProcessor($this->processor); } } public static function getDebugLogger(mixed $logger): ?DebugLoggerInterface { if ($logger instanceof DebugLoggerInterface) { return $logger; } if (!$logger instanceof Logger) { return null; } foreach ($logger->getProcessors() as $processor) { if ($processor instanceof DebugLoggerInterface) { return $processor; } } return null; } } ================================================ FILE: Log/DebugLoggerInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Log; use Symfony\Component\HttpFoundation\Request; /** * DebugLoggerInterface. * * @author Fabien Potencier */ interface DebugLoggerInterface { /** * Returns an array of logs. * * @return array, * message: string, * priority: int, * priorityName: string, * timestamp: int, * timestamp_rfc3339: string, * }> */ public function getLogs(?Request $request = null): array; /** * Returns the number of errors. */ public function countErrors(?Request $request = null): int; /** * Removes all log records. */ public function clear(): void; } ================================================ FILE: Log/Logger.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Log; use Psr\Log\AbstractLogger; use Psr\Log\InvalidArgumentException; use Psr\Log\LogLevel; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; /** * Minimalist PSR-3 logger designed to write in stderr or any other stream. * * @author Kévin Dunglas */ class Logger extends AbstractLogger implements DebugLoggerInterface { private const LEVELS = [ LogLevel::DEBUG => 0, LogLevel::INFO => 1, LogLevel::NOTICE => 2, LogLevel::WARNING => 3, LogLevel::ERROR => 4, LogLevel::CRITICAL => 5, LogLevel::ALERT => 6, LogLevel::EMERGENCY => 7, ]; private const PRIORITIES = [ LogLevel::DEBUG => 100, LogLevel::INFO => 200, LogLevel::NOTICE => 250, LogLevel::WARNING => 300, LogLevel::ERROR => 400, LogLevel::CRITICAL => 500, LogLevel::ALERT => 550, LogLevel::EMERGENCY => 600, ]; private int $minLevelIndex; private \Closure $formatter; private bool $debug = false; private array $logs = []; private array $errorCount = []; /** @var resource|null */ private $handle; /** * @param string|resource|null $output */ public function __construct(?string $minLevel = null, $output = null, ?callable $formatter = null, private readonly ?RequestStack $requestStack = null, bool $debug = false) { $minLevel ??= match ((int) ($_ENV['SHELL_VERBOSITY'] ?? $_SERVER['SHELL_VERBOSITY'] ?? 0)) { -1 => LogLevel::ERROR, 1 => LogLevel::NOTICE, 2 => LogLevel::INFO, 3 => LogLevel::DEBUG, default => null === $output || 'php://stdout' === $output || 'php://stderr' === $output ? LogLevel::ERROR : LogLevel::WARNING, }; if (!isset(self::LEVELS[$minLevel])) { throw new InvalidArgumentException(\sprintf('The log level "%s" does not exist.', $minLevel)); } $this->minLevelIndex = self::LEVELS[$minLevel]; $this->formatter = null !== $formatter ? $formatter(...) : $this->format(...); if ($output && false === $this->handle = \is_string($output) ? @fopen($output, 'a') : $output) { throw new InvalidArgumentException(\sprintf('Unable to open "%s".', $output)); } $this->debug = $debug; } public function enableDebug(): void { $this->debug = true; } public function log($level, $message, array $context = []): void { if (!isset(self::LEVELS[$level])) { throw new InvalidArgumentException(\sprintf('The log level "%s" does not exist.', $level)); } if (self::LEVELS[$level] < $this->minLevelIndex) { return; } $formatter = $this->formatter; if ($this->handle) { @fwrite($this->handle, $formatter($level, $message, $context).\PHP_EOL); } else { error_log($formatter($level, $message, $context, false)); } if ($this->debug && $this->requestStack) { $this->record($level, $message, $context); } } public function getLogs(?Request $request = null): array { if ($request) { return $this->logs[spl_object_id($request)] ?? []; } return array_merge(...array_values($this->logs)); } public function countErrors(?Request $request = null): int { if ($request) { return $this->errorCount[spl_object_id($request)] ?? 0; } return array_sum($this->errorCount); } public function clear(): void { $this->logs = []; $this->errorCount = []; } private function format(string $level, string $message, array $context, bool $prefixDate = true): string { if (str_contains($message, '{')) { $replacements = []; foreach ($context as $key => $val) { if (null === $val || \is_scalar($val) || $val instanceof \Stringable) { $replacements["{{$key}}"] = $val; } elseif ($val instanceof \DateTimeInterface) { $replacements["{{$key}}"] = $val->format(\DateTimeInterface::RFC3339); } elseif (\is_object($val)) { $replacements["{{$key}}"] = '[object '.$val::class.']'; } else { $replacements["{{$key}}"] = '['.\gettype($val).']'; } } $message = strtr($message, $replacements); } $log = \sprintf('[%s] %s', $level, $message); if ($prefixDate) { $log = date(\DateTimeInterface::RFC3339).' '.$log; } return $log; } private function record($level, $message, array $context): void { $request = $this->requestStack->getCurrentRequest(); $key = $request ? spl_object_id($request) : ''; $this->logs[$key][] = [ 'channel' => null, 'context' => $context, 'message' => $message, 'priority' => self::PRIORITIES[$level], 'priorityName' => $level, 'timestamp' => time(), 'timestamp_rfc3339' => date(\DATE_RFC3339_EXTENDED), ]; $this->errorCount[$key] ??= 0; switch ($level) { case LogLevel::ERROR: case LogLevel::CRITICAL: case LogLevel::ALERT: case LogLevel::EMERGENCY: ++$this->errorCount[$key]; } } } ================================================ FILE: Profiler/FileProfilerStorage.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Profiler; /** * Storage for profiler using files. * * @author Alexandre Salomé */ class FileProfilerStorage implements ProfilerStorageInterface { /** * Folder where profiler data are stored. */ private string $folder; /** * Constructs the file storage using a "dsn-like" path. * * Example : "file:/path/to/the/storage/folder" * * @throws \RuntimeException */ public function __construct(string $dsn) { if (!str_starts_with($dsn, 'file:')) { throw new \RuntimeException(\sprintf('Please check your configuration. You are trying to use FileStorage with an invalid dsn "%s". The expected format is "file:/path/to/the/storage/folder".', $dsn)); } $this->folder = substr($dsn, 5); if (!is_dir($this->folder) && false === @mkdir($this->folder, 0o777, true) && !is_dir($this->folder)) { throw new \RuntimeException(\sprintf('Unable to create the storage directory (%s).', $this->folder)); } } public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?int $start = null, ?int $end = null, ?string $statusCode = null, ?\Closure $filter = null): array { $file = $this->getIndexFilename(); if (!file_exists($file)) { return []; } $file = fopen($file, 'r'); fseek($file, 0, \SEEK_END); $result = []; while (\count($result) < $limit && $line = $this->readLineFromFile($file)) { $values = str_getcsv($line, ',', '"', '\\'); if (7 > \count($values)) { // skip invalid lines continue; } [$csvToken, $csvIp, $csvMethod, $csvUrl, $csvTime, $csvParent, $csvStatusCode, $csvVirtualType, $csvHasErrors] = $values + [7 => null, 8 => null]; $csvTime = (int) $csvTime; $urlFilter = false; if ($url) { $urlFilter = str_starts_with($url, '!') ? str_contains($csvUrl, substr($url, 1)) : !str_contains($csvUrl, $url); } if ($ip && !str_contains($csvIp, $ip) || $urlFilter || $method && !str_contains($csvMethod, $method) || $statusCode && !str_contains($csvStatusCode, $statusCode)) { continue; } if ($start && $csvTime < $start) { continue; } if ($end && $csvTime > $end) { continue; } $profile = [ 'token' => $csvToken, 'ip' => $csvIp, 'method' => $csvMethod, 'url' => $csvUrl, 'time' => $csvTime, 'parent' => $csvParent, 'status_code' => $csvStatusCode, 'virtual_type' => $csvVirtualType ?: 'request', 'has_errors' => (bool) $csvHasErrors, ]; if ($filter && !$filter($profile)) { continue; } $result[$csvToken] = $profile; } fclose($file); return array_values($result); } public function purge(): void { $flags = \FilesystemIterator::SKIP_DOTS; $iterator = new \RecursiveDirectoryIterator($this->folder, $flags); $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST); foreach ($iterator as $file) { if (is_file($file)) { unlink($file); } else { rmdir($file); } } } public function read(string $token): ?Profile { return $this->doRead($token); } /** * @throws \RuntimeException */ public function write(Profile $profile): bool { $file = $this->getFilename($profile->getToken()); $profileIndexed = is_file($file); if (!$profileIndexed) { // Create directory $dir = \dirname($file); if (!is_dir($dir) && false === @mkdir($dir, 0o777, true) && !is_dir($dir)) { throw new \RuntimeException(\sprintf('Unable to create the storage directory (%s).', $dir)); } } $profileToken = $profile->getToken(); // when there are errors in sub-requests, the parent and/or children tokens // may equal the profile token, resulting in infinite loops $parentToken = $profile->getParentToken() !== $profileToken ? $profile->getParentToken() : null; $childrenToken = array_filter(array_map(static fn (Profile $p) => $profileToken !== $p->getToken() ? $p->getToken() : null, $profile->getChildren())); // Store profile $data = [ 'token' => $profileToken, 'parent' => $parentToken, 'children' => $childrenToken, 'data' => $profile->getCollectors(), 'ip' => $profile->getIp(), 'method' => $profile->getMethod(), 'url' => $profile->getUrl(), 'time' => $profile->getTime(), 'status_code' => $profile->getStatusCode(), 'virtual_type' => $profile->getVirtualType() ?? 'request', 'has_errors' => $profile->hasErrors(), ]; $data = serialize($data); if (\function_exists('gzencode')) { $data = gzencode($data, 3); } if (false === file_put_contents($file, $data, \LOCK_EX)) { return false; } if (!$profileIndexed) { // Add to index if (false === $file = fopen($this->getIndexFilename(), 'a')) { return false; } fputcsv($file, [ $profile->getToken(), $profile->getIp(), $profile->getMethod(), $profile->getUrl(), $profile->getTime() ?: time(), $profile->getParentToken(), $profile->getStatusCode(), $profile->getVirtualType() ?? 'request', $profile->hasErrors() ? '1' : '0', ], ',', '"', '\\'); fclose($file); if (1 === random_int(1, 10)) { $this->removeExpiredProfiles(); } } return true; } /** * Gets filename to store data, associated to the token. */ protected function getFilename(string $token): string { // Uses 4 last characters, because first are mostly the same. $folderA = substr($token, -2, 2); $folderB = substr($token, -4, 2); return $this->folder.'/'.$folderA.'/'.$folderB.'/'.$token; } /** * Gets the index filename. */ protected function getIndexFilename(): string { return $this->folder.'/index.csv'; } /** * Reads a line in the file, backward. * * This function automatically skips the empty lines and do not include the line return in result value. * * @param resource $file The file resource, with the pointer placed at the end of the line to read */ protected function readLineFromFile($file): mixed { $line = ''; $position = ftell($file); if (0 === $position) { return null; } while (true) { $chunkSize = min($position, 1024); $position -= $chunkSize; fseek($file, $position); if (0 === $chunkSize) { // bof reached break; } $buffer = fread($file, $chunkSize); if (false === ($upTo = strrpos($buffer, "\n"))) { $line = $buffer.$line; continue; } $position += $upTo; $line = substr($buffer, $upTo + 1).$line; fseek($file, max(0, $position), \SEEK_SET); if ('' !== $line) { break; } } return '' === $line ? null : $line; } protected function createProfileFromData(string $token, array $data, ?Profile $parent = null): Profile { $profile = new Profile($token); $profile->setIp($data['ip']); $profile->setMethod($data['method']); $profile->setUrl($data['url']); $profile->setTime($data['time']); $profile->setStatusCode($data['status_code']); $profile->setVirtualType($data['virtual_type'] ?: 'request'); $profile->setHasErrors($data['has_errors'] ?? false); $profile->setCollectors($data['data']); if (!$parent && $data['parent']) { $parent = $this->read($data['parent']); } if ($parent) { $profile->setParent($parent); } foreach ($data['children'] as $token) { if (null !== $childProfile = $this->doRead($token, $profile)) { $profile->addChild($childProfile); } } return $profile; } private function doRead($token, ?Profile $profile = null): ?Profile { if (!$token || !file_exists($file = $this->getFilename($token))) { return null; } $h = fopen($file, 'r'); flock($h, \LOCK_SH); $data = stream_get_contents($h); flock($h, \LOCK_UN); fclose($h); if (\function_exists('gzdecode')) { $data = @gzdecode($data) ?: $data; } if (!$data = unserialize($data)) { return null; } return $this->createProfileFromData($token, $data, $profile); } private function removeExpiredProfiles(): void { $minimalProfileTimestamp = time() - 2 * 86400; $file = $this->getIndexFilename(); $handle = fopen($file, 'r'); if ($offset = is_file($file.'.offset') ? (int) file_get_contents($file.'.offset') : 0) { fseek($handle, $offset); } while ($line = fgets($handle)) { $values = str_getcsv($line, ',', '"', '\\'); if (7 > \count($values)) { // skip invalid lines $offset += \strlen($line); continue; } [$csvToken, , , , $csvTime] = $values; if ($csvTime >= $minimalProfileTimestamp) { break; } @unlink($this->getFilename($csvToken)); $offset += \strlen($line); } fclose($handle); file_put_contents($file.'.offset', $offset); } } ================================================ FILE: Profiler/Profile.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Profiler; use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; /** * @author Fabien Potencier * * @final */ class Profile { /** * @var DataCollectorInterface[] */ private array $collectors = []; private ?string $ip = null; private ?string $method = null; private ?string $url = null; private ?int $time = null; private ?int $statusCode = null; private ?self $parent = null; private ?string $virtualType = null; private bool $hasErrors = false; /** * @var Profile[] */ private array $children = []; public function __construct( private string $token, ) { } public function setToken(string $token): void { $this->token = $token; } /** * Gets the token. */ public function getToken(): string { return $this->token; } /** * Sets the parent token. */ public function setParent(self $parent): void { $this->parent = $parent; } /** * Returns the parent profile. */ public function getParent(): ?self { return $this->parent; } /** * Returns the parent token. */ public function getParentToken(): ?string { return $this->parent?->getToken(); } /** * Returns the IP. */ public function getIp(): ?string { return $this->ip; } public function setIp(?string $ip): void { $this->ip = $ip; } /** * Returns the request method. */ public function getMethod(): ?string { return $this->method; } public function setMethod(string $method): void { $this->method = $method; } /** * Returns the URL. */ public function getUrl(): ?string { return $this->url; } public function setUrl(?string $url): void { $this->url = $url; } public function getTime(): int { return $this->time ?? 0; } public function setTime(int $time): void { $this->time = $time; } public function setStatusCode(int $statusCode): void { $this->statusCode = $statusCode; } public function getStatusCode(): ?int { return $this->statusCode; } /** * @internal */ public function setVirtualType(?string $virtualType): void { $this->virtualType = $virtualType; } /** * @internal */ public function getVirtualType(): ?string { return $this->virtualType; } public function hasErrors(): bool { return $this->hasErrors; } public function setHasErrors(bool $hasErrors): void { $this->hasErrors = $hasErrors; } /** * Finds children profilers. * * @return self[] */ public function getChildren(): array { return $this->children; } /** * Sets children profiler. * * @param Profile[] $children */ public function setChildren(array $children): void { $this->children = []; foreach ($children as $child) { $this->addChild($child); } } /** * Adds the child token. */ public function addChild(self $child): void { $this->children[] = $child; $child->setParent($this); } public function getChildByToken(string $token): ?self { foreach ($this->children as $child) { if ($token === $child->getToken()) { return $child; } } return null; } /** * Gets a Collector by name. * * @throws \InvalidArgumentException if the collector does not exist */ public function getCollector(string $name): DataCollectorInterface { if (!isset($this->collectors[$name])) { throw new \InvalidArgumentException(\sprintf('Collector "%s" does not exist.', $name)); } return $this->collectors[$name]; } /** * Gets the Collectors associated with this profile. * * @return DataCollectorInterface[] */ public function getCollectors(): array { return $this->collectors; } /** * Sets the Collectors associated with this profile. * * @param DataCollectorInterface[] $collectors */ public function setCollectors(array $collectors): void { $this->collectors = []; foreach ($collectors as $collector) { $this->addCollector($collector); } } /** * Adds a Collector. */ public function addCollector(DataCollectorInterface $collector): void { $this->collectors[$collector->getName()] = $collector; } public function hasCollector(string $name): bool { return isset($this->collectors[$name]); } public function __serialize(): array { return [ 'token' => $this->token, 'parent' => $this->parent, 'children' => $this->children, 'collectors' => $this->collectors, 'ip' => $this->ip, 'method' => $this->method, 'url' => $this->url, 'time' => $this->time, 'statusCode' => $this->statusCode, 'virtualType' => $this->virtualType, 'hasErrors' => $this->hasErrors, ]; } } ================================================ FILE: Profiler/Profiler.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Profiler; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector; use Symfony\Contracts\Service\ResetInterface; /** * Profiler. * * @author Fabien Potencier */ class Profiler implements ResetInterface { /** * @var DataCollectorInterface[] */ private array $collectors = []; private bool $initiallyEnabled = true; public function __construct( private ProfilerStorageInterface $storage, private ?LoggerInterface $logger = null, private bool $enabled = true, ) { $this->initiallyEnabled = $enabled; } /** * Disables the profiler. */ public function disable(): void { $this->enabled = false; } /** * Enables the profiler. */ public function enable(): void { $this->enabled = true; } public function isEnabled(): bool { return $this->enabled; } /** * Loads the Profile for the given Response. */ public function loadProfileFromResponse(Response $response): ?Profile { if (!$token = $response->headers->get('X-Debug-Token')) { return null; } return $this->loadProfile($token); } /** * Loads the Profile for the given token. */ public function loadProfile(string $token): ?Profile { return $this->storage->read($token); } /** * Saves a Profile. */ public function saveProfile(Profile $profile): bool { // late collect foreach ($profile->getCollectors() as $collector) { if ($collector instanceof LateDataCollectorInterface) { $collector->lateCollect(); } } // Update hasErrors flag to include error-level logs (available after lateCollect) if (!$profile->hasErrors() && $profile->hasCollector('logger') && ($logger = $profile->getCollector('logger')) instanceof LoggerDataCollector && $logger->countErrors() > 0 ) { $profile->setHasErrors(true); } if (!($ret = $this->storage->write($profile)) && null !== $this->logger) { $this->logger->warning('Unable to store the profiler information.', ['configured_storage' => $this->storage::class]); } return $ret; } /** * Purges all data from the storage. */ public function purge(): void { $this->storage->purge(); } /** * Finds profiler tokens for the given criteria. * * @param int|null $limit The maximum number of tokens to return * @param string|null $start The start date to search from * @param string|null $end The end date to search to * @param \Closure|null $filter A filter to apply on the list of tokens * * @see https://php.net/datetime.formats for the supported date/time formats */ public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?string $start, ?string $end, ?string $statusCode = null, ?\Closure $filter = null): array { return $this->storage->find($ip, $url, $limit, $method, $this->getTimestamp($start), $this->getTimestamp($end), $statusCode, $filter); } /** * Collects data for the given Response. */ public function collect(Request $request, Response $response, ?\Throwable $exception = null): ?Profile { if (false === $this->enabled) { return null; } $profile = new Profile(bin2hex(random_bytes(3))); $profile->setTime(time()); $profile->setUrl($request->getUri()); $profile->setMethod($request->getMethod()); $profile->setStatusCode($response->getStatusCode()); try { $profile->setIp($request->getClientIp()); } catch (ConflictingHeadersException) { $profile->setIp('Unknown'); } if ($request->attributes->has('_virtual_type')) { $profile->setVirtualType($request->attributes->get('_virtual_type')); } $profile->setHasErrors(null !== $exception); if ($prevToken = $response->headers->get('X-Debug-Token')) { $response->headers->set('X-Previous-Debug-Token', $prevToken); } $response->headers->set('X-Debug-Token', $profile->getToken()); foreach ($this->collectors as $collector) { $collector->collect($request, $response, $exception); // we need to clone for sub-requests $profile->addCollector(clone $collector); } return $profile; } public function reset(): void { foreach ($this->collectors as $collector) { $collector->reset(); } $this->enabled = $this->initiallyEnabled; } /** * Gets the Collectors associated with this profiler. */ public function all(): array { return $this->collectors; } /** * Sets the Collectors associated with this profiler. * * @param DataCollectorInterface[] $collectors An array of collectors */ public function set(array $collectors = []): void { $this->collectors = []; foreach ($collectors as $collector) { $this->add($collector); } } /** * Adds a Collector. */ public function add(DataCollectorInterface $collector): void { $this->collectors[$collector->getName()] = $collector; } /** * Returns true if a Collector for the given name exists. * * @param string $name A collector name */ public function has(string $name): bool { return isset($this->collectors[$name]); } /** * Gets a Collector by name. * * @param string $name A collector name * * @throws \InvalidArgumentException if the collector does not exist */ public function get(string $name): DataCollectorInterface { if (!isset($this->collectors[$name])) { throw new \InvalidArgumentException(\sprintf('Collector "%s" does not exist.', $name)); } return $this->collectors[$name]; } private function getTimestamp(?string $value): ?int { if (null === $value || '' === $value) { return null; } try { $value = new \DateTimeImmutable(is_numeric($value) ? '@'.$value : $value); } catch (\Exception) { return null; } return $value->getTimestamp(); } } ================================================ FILE: Profiler/ProfilerStateChecker.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Profiler; use Psr\Container\ContainerInterface; class ProfilerStateChecker { public function __construct( private ContainerInterface $container, private bool $defaultEnabled, ) { } public function isProfilerEnabled(): bool { return $this->container->get('profiler')?->isEnabled() ?? $this->defaultEnabled; } public function isProfilerDisabled(): bool { return !$this->isProfilerEnabled(); } } ================================================ FILE: Profiler/ProfilerStorageInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Profiler; /** * ProfilerStorageInterface. * * This interface exists for historical reasons. The only supported * implementation is FileProfilerStorage. * * As the profiler must only be used on non-production servers, the file storage * is more than enough and no other implementations will ever be supported. * * @internal * * @author Fabien Potencier */ interface ProfilerStorageInterface { /** * Finds profiler tokens for the given criteria. * * @param int|null $limit The maximum number of tokens to return * @param int|null $start The start date to search from * @param int|null $end The end date to search to * @param string|null $statusCode The response status code * @param \Closure|null $filter A filter to apply on the list of tokens */ public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?int $start = null, ?int $end = null, ?string $statusCode = null, ?\Closure $filter = null): array; /** * Reads data associated with the given token. * * The method returns false if the token does not exist in the storage. */ public function read(string $token): ?Profile; /** * Saves a Profile. */ public function write(Profile $profile): bool; /** * Purges all data from the database. */ public function purge(): void; } ================================================ FILE: README.md ================================================ HttpKernel Component ==================== The HttpKernel component provides a structured process for converting a Request into a Response by making use of the EventDispatcher component. It's flexible enough to create full-stack frameworks, micro-frameworks or advanced CMS systems like Drupal. Resources --------- * [Documentation](https://symfony.com/doc/current/components/http_kernel.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) in the [main Symfony repository](https://github.com/symfony/symfony) ================================================ FILE: RebootableInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel; /** * Allows the Kernel to be rebooted using a temporary cache directory. * * @author Nicolas Grekas */ interface RebootableInterface { /** * Reboots a kernel. * * The getBuildDir() method of a rebootable kernel should not be called * while building the container. Use the %kernel.build_dir% parameter instead. * * @param string|null $warmupDir pass null to reboot in the regular build directory */ public function reboot(?string $warmupDir): void; } ================================================ FILE: Resources/welcome.html.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ $renderSymfonyLogoSvg = << SVG; // SVG icons from the Tabler Icons project // MIT License - Copyright (c) 2020-2023 Paweł Kuna // https://github.com/tabler/tabler-icons/blob/master/LICENSE $renderBoxIconSvg = << SVG; $renderFolderIconSvg = << SVG; $renderInfoIconSvg = << SVG; $renderNextStepIconSvg = << SVG; $renderLearnIconSvg = << SVG; $renderCommunityIconSvg = << SVG; $renderUpdatesIconSvg = << SVG; $renderWavesSvg = << SVG; ?> Welcome to Symfony!
  • You are using Symfony version
  • Your application is ready at:
  • You are seeing this page because the homepage URL is not configured and debug mode is enabled.

Next Step Create your first page to replace this placeholder page.

================================================ FILE: TerminableInterface.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; /** * Terminable extends the Kernel request/response cycle with dispatching a post * response event after sending the response and before shutting down the kernel. * * @author Jordi Boggiano * @author Pierre Minnieur */ interface TerminableInterface { /** * Terminates a request/response cycle. * * Should be called after sending the response and before shutting down the kernel. */ public function terminate(Request $request, Response $response): void; } ================================================ FILE: Tests/Attribute/WithLogLevelTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Attribute; use PHPUnit\Framework\TestCase; use Psr\Log\LogLevel; use Symfony\Component\HttpKernel\Attribute\WithLogLevel; /** * @author Dejan Angelov */ class WithLogLevelTest extends TestCase { public function testWithValidLogLevel() { $logLevel = LogLevel::NOTICE; $attribute = new WithLogLevel($logLevel); $this->assertSame($logLevel, $attribute->level); } public function testWithInvalidLogLevel() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid log level "invalid".'); new WithLogLevel('invalid'); } } ================================================ FILE: Tests/Bundle/BundleTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Bundle; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\Tests\Fixtures\BundleCompilerPass\BundleAsCompilerPassBundle; use Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle\ExtensionPresentBundle; class BundleTest extends TestCase { public function testGetContainerExtension() { $bundle = new ExtensionPresentBundle(); $this->assertInstanceOf( 'Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle\DependencyInjection\ExtensionPresentExtension', $bundle->getContainerExtension() ); } public function testBundleNameIsGuessedFromClass() { $bundle = new GuessedNameBundle(); $this->assertSame('Symfony\Component\HttpKernel\Tests\Bundle', $bundle->getNamespace()); $this->assertSame('GuessedNameBundle', $bundle->getName()); } public function testBundleNameCanBeExplicitlyProvided() { $bundle = new NamedBundle(); $this->assertSame('ExplicitlyNamedBundle', $bundle->getName()); $this->assertSame('Symfony\Component\HttpKernel\Tests\Bundle', $bundle->getNamespace()); $this->assertSame('ExplicitlyNamedBundle', $bundle->getName()); } public function testBundleAsCompilerPass() { $kernel = new class('test', true) extends Kernel { public function registerBundles(): iterable { yield new BundleAsCompilerPassBundle(); } public function registerContainerConfiguration(LoaderInterface $loader): void { } public function getProjectDir(): string { return sys_get_temp_dir().'/bundle_as_compiler_pass'; } }; $kernel->boot(); $this->assertTrue($kernel->getContainer()->has('foo')); } } class NamedBundle extends Bundle { public function __construct() { $this->name = 'ExplicitlyNamedBundle'; } } class GuessedNameBundle extends Bundle { } ================================================ FILE: Tests/CacheClearer/ChainCacheClearerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\CacheClearer; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface; use Symfony\Component\HttpKernel\CacheClearer\ChainCacheClearer; class ChainCacheClearerTest extends TestCase { protected static string $cacheDir; public static function setUpBeforeClass(): void { self::$cacheDir = tempnam(sys_get_temp_dir(), 'sf_cache_clearer_dir'); } public static function tearDownAfterClass(): void { @unlink(self::$cacheDir); } public function testInjectClearersInConstructor() { $clearer = $this->createMock(CacheClearerInterface::class); $clearer ->expects($this->once()) ->method('clear'); $chainClearer = new ChainCacheClearer([$clearer]); $chainClearer->clear(self::$cacheDir); } } ================================================ FILE: Tests/CacheClearer/Psr6CacheClearerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\CacheClearer; use PHPUnit\Framework\TestCase; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer; class Psr6CacheClearerTest extends TestCase { public function testClearPoolsInjectedInConstructor() { $pool = $this->createMock(CacheItemPoolInterface::class); $pool ->expects($this->once()) ->method('clear'); (new Psr6CacheClearer(['pool' => $pool]))->clear(''); } public function testClearPool() { $pool = $this->createMock(CacheItemPoolInterface::class); $pool ->expects($this->once()) ->method('clear') ->willReturn(true) ; $this->assertTrue((new Psr6CacheClearer(['pool' => $pool]))->clearPool('pool')); } public function testClearPoolThrowsExceptionOnUnreferencedPool() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Cache pool not found: "unknown"'); (new Psr6CacheClearer())->clearPool('unknown'); } } ================================================ FILE: Tests/CacheWarmer/CacheWarmerAggregateTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\CacheWarmer; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerAggregate; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; class CacheWarmerAggregateTest extends TestCase { public function testInjectWarmersUsingConstructor() { $warmer = $this->createMock(CacheWarmerInterface::class); $warmer ->expects($this->once()) ->method('warmUp'); $aggregate = new CacheWarmerAggregate([$warmer]); $aggregate->warmUp(__DIR__); } public function testWarmupDoesCallWarmupOnOptionalWarmersWhenEnableOptionalWarmersIsEnabled() { $warmer = $this->createMock(CacheWarmerInterface::class); $warmer ->expects($this->never()) ->method('isOptional'); $warmer ->expects($this->once()) ->method('warmUp'); $aggregate = new CacheWarmerAggregate([$warmer]); $aggregate->enableOptionalWarmers(); $aggregate->warmUp(__DIR__); } public function testWarmupDoesNotCallWarmupOnOptionalWarmersWhenEnableOptionalWarmersIsNotEnabled() { $warmer = $this->createMock(CacheWarmerInterface::class); $warmer ->expects($this->once()) ->method('isOptional') ->willReturn(true); $warmer ->expects($this->never()) ->method('warmUp'); $aggregate = new CacheWarmerAggregate([$warmer]); $aggregate->warmUp(__DIR__); } public function testWarmupReturnsFilesOrClasses() { $warmer = $this->createMock(CacheWarmerInterface::class); $warmer ->expects($this->never()) ->method('isOptional'); $warmer ->expects($this->once()) ->method('warmUp') ->willReturn([__CLASS__, __FILE__]); $aggregate = new CacheWarmerAggregate([$warmer]); $aggregate->enableOptionalWarmers(); $this->assertSame([__CLASS__, __FILE__], $aggregate->warmUp(__DIR__)); } public function testWarmupChecksInvalidFiles() { $warmer = $this->createMock(CacheWarmerInterface::class); $warmer ->expects($this->never()) ->method('isOptional'); $warmer ->expects($this->once()) ->method('warmUp') ->willReturn([self::class, __DIR__]); $aggregate = new CacheWarmerAggregate([$warmer]); $aggregate->enableOptionalWarmers(); $this->expectException(\LogicException::class); $aggregate->warmUp(__DIR__); } public function testWarmupPassBuildDir() { $warmer = $this->createMock(CacheWarmerInterface::class); $warmer ->expects($this->once()) ->method('warmUp') ->with('cache_dir', 'build_dir'); $aggregate = new CacheWarmerAggregate([$warmer]); $aggregate->enableOptionalWarmers(); $aggregate->warmUp('cache_dir', 'build_dir'); } public function testWarmupOnOptionalWarmerPassBuildDir() { $warmer = $this->createMock(CacheWarmerInterface::class); $warmer ->expects($this->once()) ->method('isOptional') ->willReturn(true); $warmer ->expects($this->once()) ->method('warmUp') ->with('cache_dir', 'build_dir'); $aggregate = new CacheWarmerAggregate([$warmer]); $aggregate->enableOnlyOptionalWarmers(); $aggregate->warmUp('cache_dir', 'build_dir'); } public function testWarmupWhenDebugDisplaysWarmupDuration() { $warmer = $this->createMock(CacheWarmerInterface::class); $io = $this->createMock(SymfonyStyle::class); $io ->expects($this->once()) ->method('isDebug') ->willReturn(true) ; $io ->expects($this->once()) ->method('info') ->with($this->matchesRegularExpression('/"(.+)" completed in (.+)ms\./')) ; $warmer ->expects($this->once()) ->method('warmUp'); $aggregate = new CacheWarmerAggregate([$warmer]); $aggregate->warmUp(__DIR__, null, $io); } public function testWarmupWhenNotDebugDoesntDisplayWarmupDuration() { $warmer = $this->createMock(CacheWarmerInterface::class); $io = $this->createMock(SymfonyStyle::class); $io ->expects($this->once()) ->method('isDebug') ->willReturn(false) ; $io ->expects($this->never()) ->method('info') ->with($this->matchesRegularExpression('/"(.+)" completed in (.+)ms\./')) ; $warmer ->expects($this->once()) ->method('warmUp'); $aggregate = new CacheWarmerAggregate([$warmer]); $aggregate->warmUp(__DIR__, null, $io); } } ================================================ FILE: Tests/CacheWarmer/CacheWarmerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\CacheWarmer; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmer; class CacheWarmerTest extends TestCase { protected static string $cacheFile; public static function setUpBeforeClass(): void { self::$cacheFile = tempnam(sys_get_temp_dir(), 'sf_cache_warmer_dir'); } public static function tearDownAfterClass(): void { @unlink(self::$cacheFile); } public function testWriteCacheFileCreatesTheFile() { $warmer = new TestCacheWarmer(self::$cacheFile); $warmer->warmUp(\dirname(self::$cacheFile)); $this->assertFileExists(self::$cacheFile); } public function testWriteNonWritableCacheFileThrowsARuntimeException() { $this->expectException(\RuntimeException::class); $nonWritableFile = '/this/file/is/very/probably/not/writable'; $warmer = new TestCacheWarmer($nonWritableFile); $warmer->warmUp(\dirname($nonWritableFile)); } } class TestCacheWarmer extends CacheWarmer { protected string $file; public function __construct(string $file) { $this->file = $file; } public function warmUp(string $cacheDir, ?string $buildDir = null): array { $this->writeCacheFile($this->file, 'content'); return []; } public function isOptional(): bool { return false; } } ================================================ FILE: Tests/Config/FileLocatorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Config; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Config\FileLocator; use Symfony\Component\HttpKernel\KernelInterface; class FileLocatorTest extends TestCase { public function testLocate() { $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->atLeastOnce()) ->method('locateResource') ->with('@BundleName/some/path') ->willReturn('/bundle-name/some/path'); $locator = new FileLocator($kernel); $this->assertEquals('/bundle-name/some/path', $locator->locate('@BundleName/some/path')); $kernel ->expects($this->never()) ->method('locateResource'); $this->expectException(\LogicException::class); $locator->locate('/some/path'); } } ================================================ FILE: Tests/Controller/ArgumentResolver/BackedEnumValueResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\BackedEnumValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Tests\Fixtures\IntEnum; use Symfony\Component\HttpKernel\Tests\Fixtures\Suit; class BackedEnumValueResolverTest extends TestCase { #[DataProvider('provideTestSupportsData')] public function testSupports(Request $request, ArgumentMetadata $metadata, bool $expectedSupport) { $resolver = new BackedEnumValueResolver(); $this->assertCount((int) $expectedSupport, $resolver->resolve($request, $metadata)); } public static function provideTestSupportsData(): iterable { yield 'unsupported type' => [ self::createRequest(['suit' => 'H']), self::createArgumentMetadata('suit', \stdClass::class), false, ]; yield 'supports from attributes' => [ self::createRequest(['suit' => 'H']), self::createArgumentMetadata('suit', Suit::class), true, ]; yield 'with null attribute value' => [ self::createRequest(['suit' => null]), self::createArgumentMetadata('suit', Suit::class), true, ]; yield 'without matching attribute' => [ self::createRequest(), self::createArgumentMetadata('suit', Suit::class), false, ]; yield 'unsupported variadic' => [ self::createRequest(['suit' => ['H', 'S']]), self::createArgumentMetadata( 'suit', Suit::class, variadic: true, ), false, ]; } #[DataProvider('provideTestResolveData')] public function testResolve(Request $request, ArgumentMetadata $metadata, $expected) { $resolver = new BackedEnumValueResolver(); /** @var \Generator $results */ $results = $resolver->resolve($request, $metadata); self::assertSame($expected, $results); } public static function provideTestResolveData(): iterable { yield 'resolves from attributes' => [ self::createRequest(['suit' => 'H']), self::createArgumentMetadata('suit', Suit::class), [Suit::Hearts], ]; yield 'with null attribute value' => [ self::createRequest(['suit' => null]), self::createArgumentMetadata( 'suit', Suit::class, ), [null], ]; yield 'already resolved attribute value' => [ self::createRequest(['suit' => Suit::Hearts]), self::createArgumentMetadata('suit', Suit::class), [Suit::Hearts], ]; } public function testResolveThrowsNotFoundOnInvalidValue() { $resolver = new BackedEnumValueResolver(); $request = self::createRequest(['suit' => 'foo']); $metadata = self::createArgumentMetadata('suit', Suit::class); $this->expectException(NotFoundHttpException::class); $this->expectExceptionMessage('Could not resolve the "Symfony\Component\HttpKernel\Tests\Fixtures\Suit $suit" controller argument: "foo" is not a valid backing value for enum'); $resolver->resolve($request, $metadata); } public function testResolveThrowsOnUnexpectedType() { $resolver = new BackedEnumValueResolver(); $request = self::createRequest(['suit' => false]); $metadata = self::createArgumentMetadata('suit', Suit::class); $this->expectException(NotFoundHttpException::class); $this->expectExceptionMessage('Could not resolve the "Symfony\Component\HttpKernel\Tests\Fixtures\Suit $suit" controller argument: expecting an int or string, got "bool".'); $resolver->resolve($request, $metadata); } public function testResolveThrowsOnTypeError() { $resolver = new BackedEnumValueResolver(); $request = self::createRequest(['suit' => 'value']); $metadata = self::createArgumentMetadata('suit', IntEnum::class); $this->expectException(NotFoundHttpException::class); $this->expectExceptionMessage('Could not resolve the "Symfony\Component\HttpKernel\Tests\Fixtures\IntEnum $suit" controller argument: Symfony\Component\HttpKernel\Tests\Fixtures\IntEnum::from(): Argument #1 ($value) must be of type int, string given'); $resolver->resolve($request, $metadata); } private static function createRequest(array $attributes = []): Request { return new Request([], [], $attributes); } private static function createArgumentMetadata(string $name, string $type, bool $variadic = false): ArgumentMetadata { return new ArgumentMetadata($name, $type, $variadic, false, null); } } ================================================ FILE: Tests/Controller/ArgumentResolver/DateTimeValueResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Clock\MockClock; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapDateTime; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DateTimeValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class DateTimeValueResolverTest extends TestCase { private readonly string $defaultTimezone; protected function setUp(): void { $this->defaultTimezone = date_default_timezone_get(); } protected function tearDown(): void { date_default_timezone_set($this->defaultTimezone); } public static function getTimeZones() { yield ['UTC', false]; yield ['Pacific/Honolulu', false]; yield ['America/Toronto', false]; yield ['UTC', true]; yield ['Pacific/Honolulu', true]; yield ['America/Toronto', true]; } public static function getClasses() { yield [\DateTimeInterface::class]; yield [\DateTime::class]; yield [FooDateTime::class]; } public function testUnsupportedArgument() { $resolver = new DateTimeValueResolver(); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = self::requestWithAttributes(['dummy' => 'now']); $this->assertSame([], $resolver->resolve($request, $argument)); } #[DataProvider('getTimeZones')] public function testFullDate(string $timezone, bool $withClock) { date_default_timezone_set($withClock ? 'UTC' : $timezone); $resolver = new DateTimeValueResolver($withClock ? new MockClock('now', $timezone) : null); $argument = new ArgumentMetadata('dummy', \DateTimeImmutable::class, false, false, null); $request = self::requestWithAttributes(['dummy' => '2012-07-21 00:00:00']); $results = $resolver->resolve($request, $argument); $this->assertCount(1, $results); $this->assertInstanceOf(\DateTimeImmutable::class, $results[0]); $this->assertSame($timezone, $results[0]->getTimezone()->getName(), 'Default timezone'); $this->assertEquals('2012-07-21 00:00:00', $results[0]->format('Y-m-d H:i:s')); } #[DataProvider('getTimeZones')] public function testUnixTimestamp(string $timezone, bool $withClock) { date_default_timezone_set($withClock ? 'UTC' : $timezone); $resolver = new DateTimeValueResolver($withClock ? new MockClock('now', $timezone) : null); $argument = new ArgumentMetadata('dummy', \DateTimeImmutable::class, false, false, null); $request = self::requestWithAttributes(['dummy' => '989541720']); $results = $resolver->resolve($request, $argument); $this->assertCount(1, $results); $this->assertInstanceOf(\DateTimeImmutable::class, $results[0]); $this->assertSame('+00:00', $results[0]->getTimezone()->getName(), 'Timestamps are UTC'); $this->assertEquals('2001-05-11 00:42:00', $results[0]->format('Y-m-d H:i:s')); } public function testNullableWithEmptyAttribute() { $resolver = new DateTimeValueResolver(); $argument = new ArgumentMetadata('dummy', \DateTimeImmutable::class, false, false, null, true); $request = self::requestWithAttributes(['dummy' => '']); $results = $resolver->resolve($request, $argument); $this->assertCount(1, $results); $this->assertNull($results[0]); } /** * @param class-string<\DateTimeInterface> $class */ #[DataProvider('getClasses')] public function testNow(string $class) { date_default_timezone_set($timezone = 'Pacific/Honolulu'); $resolver = new DateTimeValueResolver(); $argument = new ArgumentMetadata('dummy', $class, false, false, null, false); $request = self::requestWithAttributes(['dummy' => null]); $results = $resolver->resolve($request, $argument); $this->assertCount(1, $results); $this->assertInstanceOf($class, $results[0]); $this->assertSame($timezone, $results[0]->getTimezone()->getName(), 'Default timezone'); $this->assertEquals('0', $results[0]->diff(new \DateTimeImmutable())->format('%s')); } /** * @param class-string<\DateTimeInterface> $class */ #[DataProvider('getClasses')] public function testNowWithClock(string $class) { date_default_timezone_set('Pacific/Honolulu'); $clock = new MockClock('2022-02-20 22:20:02'); $resolver = new DateTimeValueResolver($clock); $argument = new ArgumentMetadata('dummy', $class, false, false, null, false); $request = self::requestWithAttributes(['dummy' => null]); $results = $resolver->resolve($request, $argument); $this->assertCount(1, $results); $this->assertInstanceOf($class, $results[0]); $this->assertSame('UTC', $results[0]->getTimezone()->getName(), 'Default timezone'); $this->assertEquals($clock->now(), $results[0]); } /** * @param class-string<\DateTimeInterface> $class */ #[DataProvider('getClasses')] public function testPreviouslyConvertedAttribute(string $class) { $resolver = new DateTimeValueResolver(); $argument = new ArgumentMetadata('dummy', $class, false, false, null, true); $request = self::requestWithAttributes(['dummy' => $datetime = new \DateTimeImmutable()]); $results = $resolver->resolve($request, $argument); $this->assertCount(1, $results); $this->assertEquals($datetime, $results[0], 'The value is the same, but the class can be modified.'); $this->assertInstanceOf($class, $results[0]); } public function testCustomClass() { date_default_timezone_set('UTC'); $resolver = new DateTimeValueResolver(); $argument = new ArgumentMetadata('dummy', FooDateTime::class, false, false, null); $request = self::requestWithAttributes(['dummy' => '2016-09-08 00:00:00']); $results = $resolver->resolve($request, $argument); $this->assertCount(1, $results); $this->assertInstanceOf(FooDateTime::class, $results[0]); $this->assertEquals('2016-09-08 00:00:00+00:00', $results[0]->format('Y-m-d H:i:sP')); } #[DataProvider('getTimeZones')] public function testDateTimeImmutable(string $timezone, bool $withClock) { date_default_timezone_set($withClock ? 'UTC' : $timezone); $resolver = new DateTimeValueResolver($withClock ? new MockClock('now', $timezone) : null); $argument = new ArgumentMetadata('dummy', \DateTimeImmutable::class, false, false, null); $request = self::requestWithAttributes(['dummy' => '2016-09-08 00:00:00 +05:00']); $results = $resolver->resolve($request, $argument); $this->assertCount(1, $results); $this->assertInstanceOf(\DateTimeImmutable::class, $results[0]); $this->assertSame('+05:00', $results[0]->getTimezone()->getName(), 'Input timezone'); $this->assertEquals('2016-09-08 00:00:00', $results[0]->format('Y-m-d H:i:s')); } #[DataProvider('getTimeZones')] public function testWithFormat(string $timezone, bool $withClock) { date_default_timezone_set($withClock ? 'UTC' : $timezone); $resolver = new DateTimeValueResolver($withClock ? new MockClock('now', $timezone) : null); $argument = new ArgumentMetadata('dummy', \DateTimeInterface::class, false, false, null, false, [ MapDateTime::class => new MapDateTime('m-d-y H:i:s'), ]); $request = self::requestWithAttributes(['dummy' => '09-08-16 12:34:56']); $results = $resolver->resolve($request, $argument); $this->assertCount(1, $results); $this->assertInstanceOf(\DateTimeImmutable::class, $results[0]); $this->assertSame($timezone, $results[0]->getTimezone()->getName(), 'Default timezone'); $this->assertEquals('2016-09-08 12:34:56', $results[0]->format('Y-m-d H:i:s')); } public static function provideInvalidDates() { return [ 'invalid date' => [ new ArgumentMetadata('dummy', \DateTimeImmutable::class, false, false, null), self::requestWithAttributes(['dummy' => 'Invalid DateTime Format']), ], 'invalid format' => [ new ArgumentMetadata('dummy', \DateTimeImmutable::class, false, false, null, false, [new MapDateTime(format: 'd.m.Y')]), self::requestWithAttributes(['dummy' => '2012-07-21']), ], 'invalid ymd format' => [ new ArgumentMetadata('dummy', \DateTimeImmutable::class, false, false, null, false, [new MapDateTime(format: 'Y-m-d')]), self::requestWithAttributes(['dummy' => '2012-21-07']), ], ]; } #[DataProvider('provideInvalidDates')] public function test404Exception(ArgumentMetadata $argument, Request $request) { $resolver = new DateTimeValueResolver(); $this->expectException(NotFoundHttpException::class); $this->expectExceptionMessage('Invalid date given for parameter "dummy".'); $resolver->resolve($request, $argument); } private static function requestWithAttributes(array $attributes): Request { $request = Request::create('/'); foreach ($attributes as $name => $value) { $request->attributes->set($name, $value); } return $request; } } class FooDateTime extends \DateTimeImmutable { } ================================================ FILE: Tests/Controller/ArgumentResolver/NotTaggedControllerValueResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\NotTaggedControllerValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; class NotTaggedControllerValueResolverTest extends TestCase { public function testDoNotSupportWhenControllerExists() { $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([ 'App\\Controller\\Mine::method' => static fn () => new ServiceLocator([ 'dummy' => static fn () => new \stdClass(), ]), ])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => 'App\\Controller\\Mine::method']); $this->assertSame([], $resolver->resolve($request, $argument)); } public function testDoNotSupportEmptyController() { $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => '']); $this->assertSame([], $resolver->resolve($request, $argument)); } public function testController() { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => 'App\\Controller\\Mine::method']); $resolver->resolve($request, $argument); } public function testControllerWithATrailingBackSlash() { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => '\\App\\Controller\\Mine::method']); $resolver->resolve($request, $argument); } public function testControllerWithMethodNameStartUppercase() { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => 'App\\Controller\\Mine::Method']); $resolver->resolve($request, $argument); } public function testControllerNameIsAnArray() { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::method()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => ['App\\Controller\\Mine', 'method']]); $resolver->resolve($request, $argument); } public function testInvokableController() { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Could not resolve argument $dummy of "App\Controller\Mine::__invoke()", maybe you forgot to register the controller as a service or missed tagging it with the "controller.service_arguments"?'); $resolver = new NotTaggedControllerValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', \stdClass::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => 'App\Controller\Mine']); $resolver->resolve($request, $argument); } private function requestWithAttributes(array $attributes) { $request = Request::create('/'); foreach ($attributes as $name => $value) { $request->attributes->set($name, $value); } return $request; } } ================================================ FILE: Tests/Controller/ArgumentResolver/QueryParameterValueResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\MapQueryParameter; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\QueryParameterValueResolver; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Tests\Fixtures\Suit; use Symfony\Component\Uid\Ulid; class QueryParameterValueResolverTest extends TestCase { private ValueResolverInterface $resolver; protected function setUp(): void { $this->resolver = new QueryParameterValueResolver(); } public function testSkipWhenNoAttribute() { $metadata = new ArgumentMetadata('firstName', 'string', false, true, false); $this->assertSame([], $this->resolver->resolve(Request::create('/'), $metadata)); } #[DataProvider('validDataProvider')] public function testResolvingSuccessfully(Request $request, ArgumentMetadata $metadata, array $expected) { $this->assertEquals($expected, $this->resolver->resolve($request, $metadata)); } #[DataProvider('invalidArgumentTypeProvider')] public function testResolvingWithInvalidArgumentType(Request $request, ArgumentMetadata $metadata, string $exceptionMessage) { $this->expectException(\LogicException::class); $this->expectExceptionMessage($exceptionMessage); $this->resolver->resolve($request, $metadata); } #[DataProvider('invalidOrMissingArgumentProvider')] public function testResolvingWithInvalidOrMissingArgument(Request $request, ArgumentMetadata $metadata, HttpException $expectedException) { try { $this->resolver->resolve($request, $metadata); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $exception) { $this->assertSame($expectedException->getMessage(), $exception->getMessage()); $this->assertSame($expectedException->getStatusCode(), $exception->getStatusCode()); } } /** * @return iterable, * }> */ public static function validDataProvider(): iterable { yield 'parameter found and array' => [ Request::create('/', 'GET', ['ids' => ['1', '2']]), new ArgumentMetadata('ids', 'array', false, false, false, attributes: [new MapQueryParameter()]), [['1', '2']], ]; yield 'parameter found and array variadic' => [ Request::create('/', 'GET', ['ids' => [['1', '2'], ['2']]]), new ArgumentMetadata('ids', 'array', true, false, false, attributes: [new MapQueryParameter()]), [['1', '2'], ['2']], ]; yield 'parameter found and string' => [ Request::create('/', 'GET', ['firstName' => 'John']), new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter()]), ['John'], ]; yield 'parameter found and string variadic' => [ Request::create('/', 'GET', ['ids' => ['1', '2']]), new ArgumentMetadata('ids', 'string', true, false, false, attributes: [new MapQueryParameter()]), ['1', '2'], ]; yield 'parameter found and string with regexp filter that matches' => [ Request::create('/', 'GET', ['firstName' => 'John']), new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter(options: ['regexp' => '/John/'])]), ['John'], ]; yield 'parameter found and string with regexp filter that falls back to null on failure' => [ Request::create('/', 'GET', ['firstName' => 'Fabien']), new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE, options: ['regexp' => '/John/'])]), [null], ]; yield 'parameter found and string variadic with regexp filter that matches' => [ Request::create('/', 'GET', ['firstName' => ['John', 'John']]), new ArgumentMetadata('firstName', 'string', true, false, false, attributes: [new MapQueryParameter(options: ['regexp' => '/John/'])]), ['John', 'John'], ]; yield 'parameter found and string variadic with regexp filter that falls back to null on failure' => [ Request::create('/', 'GET', ['firstName' => ['John', 'Fabien']]), new ArgumentMetadata('firstName', 'string', true, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE, options: ['regexp' => '/John/'])]), ['John'], ]; yield 'parameter found and integer' => [ Request::create('/', 'GET', ['age' => '123']), new ArgumentMetadata('age', 'int', false, false, false, attributes: [new MapQueryParameter()]), [123], ]; yield 'parameter found and integer variadic' => [ Request::create('/', 'GET', ['age' => ['123', '222']]), new ArgumentMetadata('age', 'int', true, false, false, attributes: [new MapQueryParameter()]), [123, 222], ]; yield 'parameter found and float' => [ Request::create('/', 'GET', ['price' => '10.99']), new ArgumentMetadata('price', 'float', false, false, false, attributes: [new MapQueryParameter()]), [10.99], ]; yield 'parameter found and float variadic' => [ Request::create('/', 'GET', ['price' => ['10.99e2', '5.99']]), new ArgumentMetadata('price', 'float', true, false, false, attributes: [new MapQueryParameter()]), [1099.0, 5.99], ]; yield 'parameter found and boolean yes' => [ Request::create('/', 'GET', ['isVerified' => 'yes']), new ArgumentMetadata('isVerified', 'bool', false, false, false, attributes: [new MapQueryParameter()]), [true], ]; yield 'parameter found and boolean yes variadic' => [ Request::create('/', 'GET', ['isVerified' => ['yes', 'yes']]), new ArgumentMetadata('isVerified', 'bool', true, false, false, attributes: [new MapQueryParameter()]), [true, true], ]; yield 'parameter found and boolean true' => [ Request::create('/', 'GET', ['isVerified' => 'true']), new ArgumentMetadata('isVerified', 'bool', false, false, false, attributes: [new MapQueryParameter()]), [true], ]; yield 'parameter found and boolean 1' => [ Request::create('/', 'GET', ['isVerified' => '1']), new ArgumentMetadata('isVerified', 'bool', false, false, false, attributes: [new MapQueryParameter()]), [true], ]; yield 'parameter found and boolean no' => [ Request::create('/', 'GET', ['isVerified' => 'no']), new ArgumentMetadata('isVerified', 'bool', false, false, false, attributes: [new MapQueryParameter()]), [false], ]; yield 'parameter found and backing value' => [ Request::create('/', 'GET', ['suit' => 'H']), new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter()]), [Suit::Hearts], ]; yield 'parameter found and backing value variadic' => [ Request::create('/', 'GET', ['suits' => ['H', 'D']]), new ArgumentMetadata('suits', Suit::class, true, false, false, attributes: [new MapQueryParameter()]), [Suit::Hearts, Suit::Diamonds], ]; yield 'parameter found and backing value not int nor string that fallbacks to null on failure' => [ Request::create('/', 'GET', ['suit' => 1]), new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL, flags: \FILTER_NULL_ON_FAILURE)]), [null], ]; yield 'parameter found and value not valid backing value that falls back to null on failure' => [ Request::create('/', 'GET', ['suit' => 'B']), new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE)]), [null], ]; yield 'parameter found and backing type variadic and at least one backing value not int nor string that fallbacks to null on failure' => [ Request::create('/', 'GET', ['suits' => ['1', 'D']]), new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE)]), [null], ]; yield 'parameter found and backing type variadic and at least one value not valid backing value that falls back to null on failure' => [ Request::create('/', 'GET', ['suits' => ['B', 'D']]), new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(flags: \FILTER_NULL_ON_FAILURE)]), [null], ]; yield 'parameter not found but nullable' => [ Request::create('/', 'GET'), new ArgumentMetadata('firstName', 'string', false, false, false, true, [new MapQueryParameter()]), [], ]; yield 'parameter not found but optional' => [ Request::create('/', 'GET'), new ArgumentMetadata('firstName', 'string', false, true, false, attributes: [new MapQueryParameter()]), [], ]; yield 'parameter found and ULID' => [ Request::create('/', 'GET', ['groupId' => '01E439TP9XJZ9RPFH3T1PYBCR8']), new ArgumentMetadata('groupId', Ulid::class, false, true, false, attributes: [new MapQueryParameter()]), [Ulid::fromString('01E439TP9XJZ9RPFH3T1PYBCR8')], ]; } /** * @return iterable */ public static function invalidArgumentTypeProvider(): iterable { yield 'unsupported type' => [ Request::create('/', 'GET', ['standardClass' => 'test']), new ArgumentMetadata('standardClass', \stdClass::class, false, false, false, attributes: [new MapQueryParameter()]), '#[MapQueryParameter] cannot be used on controller argument "$standardClass" of type "stdClass"; one of array, string, int, float, bool, uid or \BackedEnum should be used.', ]; yield 'unsupported type variadic' => [ Request::create('/', 'GET', ['standardClass' => 'test']), new ArgumentMetadata('standardClass', \stdClass::class, true, false, false, attributes: [new MapQueryParameter()]), '#[MapQueryParameter] cannot be used on controller argument "...$standardClass" of type "stdClass"; one of array, string, int, float, bool, uid or \BackedEnum should be used.', ]; } /** * @return iterable */ public static function invalidOrMissingArgumentProvider(): iterable { yield 'parameter found and array variadic with parameter not array failure' => [ Request::create('/', 'GET', ['ids' => [['1', '2'], '1']]), new ArgumentMetadata('ids', 'array', true, false, false, attributes: [new MapQueryParameter()]), new NotFoundHttpException('Invalid query parameter "ids".'), ]; yield 'parameter found and string with regexp filter that does not match' => [ Request::create('/', 'GET', ['firstName' => 'Fabien']), new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/John/'])]), new NotFoundHttpException('Invalid query parameter "firstName".'), ]; yield 'parameter found and string variadic with regexp filter that does not match' => [ Request::create('/', 'GET', ['firstName' => ['Fabien']]), new ArgumentMetadata('firstName', 'string', true, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_REGEXP, options: ['regexp' => '/John/'])]), new NotFoundHttpException('Invalid query parameter "firstName".'), ]; yield 'parameter found and boolean invalid' => [ Request::create('/', 'GET', ['isVerified' => 'whatever']), new ArgumentMetadata('isVerified', 'bool', false, false, false, attributes: [new MapQueryParameter()]), new NotFoundHttpException('Invalid query parameter "isVerified".'), ]; yield 'parameter found and backing value not int nor string' => [ Request::create('/', 'GET', ['suit' => 1]), new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL)]), new NotFoundHttpException('Invalid query parameter "suit".'), ]; yield 'parameter found and value not valid backing value' => [ Request::create('/', 'GET', ['suit' => 'B']), new ArgumentMetadata('suit', Suit::class, false, false, false, attributes: [new MapQueryParameter()]), new NotFoundHttpException('Invalid query parameter "suit".'), ]; yield 'parameter found and backing type variadic and at least one backing value not int nor string' => [ Request::create('/', 'GET', ['suits' => [1, 'D']]), new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter(filter: \FILTER_VALIDATE_BOOL)]), new NotFoundHttpException('Invalid query parameter "suits".'), ]; yield 'parameter found and backing type variadic and at least one value not valid backing value' => [ Request::create('/', 'GET', ['suits' => ['B', 'D']]), new ArgumentMetadata('suits', Suit::class, false, false, false, attributes: [new MapQueryParameter()]), new NotFoundHttpException('Invalid query parameter "suits".'), ]; yield 'parameter not found' => [ Request::create('/', 'GET'), new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter()]), new NotFoundHttpException('Missing query parameter "firstName".'), ]; yield 'parameter not found with custom validation failed status code' => [ Request::create('/', 'GET'), new ArgumentMetadata('firstName', 'string', false, false, false, attributes: [new MapQueryParameter(validationFailedStatusCode: Response::HTTP_BAD_REQUEST)]), new BadRequestHttpException('Missing query parameter "firstName".'), ]; } } ================================================ FILE: Tests/Controller/ArgumentResolver/RequestAttributeValueResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class RequestAttributeValueResolverTest extends TestCase { public function testValidIntWithinRangeWorks() { $resolver = new RequestAttributeValueResolver(); $request = new Request(); $request->attributes->set('id', '123'); $metadata = new ArgumentMetadata('id', 'int', false, false, null); $result = iterator_to_array($resolver->resolve($request, $metadata)); $this->assertSame([123], $result); } public function testInvalidStringBecomes404() { $resolver = new RequestAttributeValueResolver(); $request = new Request(); $request->attributes->set('id', 'abc'); $metadata = new ArgumentMetadata('id', 'int', false, false, null); $this->expectException(NotFoundHttpException::class); iterator_to_array($resolver->resolve($request, $metadata)); } public function testOutOfRangeIntBecomes404() { $resolver = new RequestAttributeValueResolver(); $request = new Request(); // one more than PHP_INT_MAX on 64-bit (string input) $request->attributes->set('id', '9223372036854775808'); $metadata = new ArgumentMetadata('id', 'int', false, false, null); $this->expectException(NotFoundHttpException::class); iterator_to_array($resolver->resolve($request, $metadata)); } public function testNullableIntAllowsNull() { $resolver = new RequestAttributeValueResolver(); $request = new Request(); $request->attributes->set('id', null); $metadata = new ArgumentMetadata('id', 'int', false, true, null); $result = iterator_to_array($resolver->resolve($request, $metadata)); $this->assertSame([null], $result); } } ================================================ FILE: Tests/Controller/ArgumentResolver/RequestHeaderValueResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\AcceptHeader; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapRequestHeader; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestHeaderValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\HttpException; class RequestHeaderValueResolverTest extends TestCase { public static function provideHeaderValueWithStringType(): iterable { yield 'with accept' => ['accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8']; yield 'with accept-language' => ['accept-language', 'en-us,en;q=0.5']; yield 'with host' => ['host', 'localhost']; yield 'with user-agent' => ['user-agent', 'Symfony']; } public static function provideHeaderValueWithArrayType(): iterable { yield 'with accept' => [ 'accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', [ [ 'text/html', 'application/xhtml+xml', 'application/xml', '*/*', ], ], ]; yield 'with accept-language' => [ 'accept-language', 'en-us,en;q=0.5', [ [ 'en_US', 'en', ], ], ]; yield 'with host' => [ 'host', 'localhost', [ ['localhost'], ], ]; yield 'with user-agent' => [ 'user-agent', 'Symfony', [ ['Symfony'], ], ]; } public static function provideHeaderValueWithAcceptHeaderType(): iterable { yield 'with accept' => [ 'accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', [AcceptHeader::fromString('text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8')], ]; yield 'with accept-language' => [ 'accept-language', 'en-us,en;q=0.5', [AcceptHeader::fromString('en-us,en;q=0.5')], ]; yield 'with host' => [ 'host', 'localhost', [AcceptHeader::fromString('localhost')], ]; yield 'with user-agent' => [ 'user-agent', 'Symfony', [AcceptHeader::fromString('Symfony')], ]; } public static function provideHeaderValueWithDefaultAndNull(): iterable { yield 'with hasDefaultValue' => [true, 'foo', false, 'foo']; yield 'with no isNullable' => [false, null, true, null]; } public function testWrongType() { $this->expectException(\LogicException::class); $metadata = new ArgumentMetadata('accept', 'int', false, false, null, false, [ new MapRequestHeader(), ]); $request = Request::create('/'); $resolver = new RequestHeaderValueResolver(); $resolver->resolve($request, $metadata); } #[DataProvider('provideHeaderValueWithStringType')] public function testWithStringType(string $parameter, string $value) { $resolver = new RequestHeaderValueResolver(); $metadata = new ArgumentMetadata('variableName', 'string', false, false, null, false, [ new MapRequestHeader($parameter), ]); $request = Request::create('/'); $request->headers->set($parameter, $value); $arguments = $resolver->resolve($request, $metadata); self::assertEquals([$value], $arguments); } #[DataProvider('provideHeaderValueWithArrayType')] public function testWithArrayType(string $parameter, string $value, array $expected) { $resolver = new RequestHeaderValueResolver(); $metadata = new ArgumentMetadata('variableName', 'array', false, false, null, false, [ new MapRequestHeader($parameter), ]); $request = Request::create('/'); $request->headers->set($parameter, $value); $arguments = $resolver->resolve($request, $metadata); self::assertEquals($expected, $arguments); } #[DataProvider('provideHeaderValueWithAcceptHeaderType')] public function testWithAcceptHeaderType(string $parameter, string $value, array $expected) { $resolver = new RequestHeaderValueResolver(); $metadata = new ArgumentMetadata('variableName', AcceptHeader::class, false, false, null, false, [ new MapRequestHeader($parameter), ]); $request = Request::create('/'); $request->headers->set($parameter, $value); $arguments = $resolver->resolve($request, $metadata); self::assertEquals($expected, $arguments); } #[DataProvider('provideHeaderValueWithDefaultAndNull')] public function testWithDefaultValueAndNull(bool $hasDefaultValue, ?string $defaultValue, bool $isNullable, ?string $expected) { $metadata = new ArgumentMetadata('wrong-header', 'string', false, $hasDefaultValue, $defaultValue, $isNullable, [ new MapRequestHeader(), ]); $request = Request::create('/'); $resolver = new RequestHeaderValueResolver(); $arguments = $resolver->resolve($request, $metadata); self::assertEquals([$expected], $arguments); } public function testCamelCaseArgumentNameMapsToKebabCaseHeader() { $resolver = new RequestHeaderValueResolver(); $metadata = new ArgumentMetadata('acceptEncoding', 'string', false, false, null, false, [ new MapRequestHeader(), ]); $request = Request::create('/'); $request->headers->set('accept-encoding', 'gzip, deflate'); $arguments = $resolver->resolve($request, $metadata); self::assertSame(['gzip, deflate'], $arguments); } public function testWithNoDefaultAndNotNullable() { $this->expectException(HttpException::class); $this->expectExceptionMessage('Missing header "variable-name".'); $metadata = new ArgumentMetadata('variableName', 'string', false, false, null, false, [ new MapRequestHeader(), ]); $resolver = new RequestHeaderValueResolver(); $resolver->resolve(Request::create('/'), $metadata); } public function testWithNoDefaultAndNotNullableArray() { $metadata = new ArgumentMetadata('variableName', 'array', false, false, null, false, [ new MapRequestHeader(), ]); $resolver = new RequestHeaderValueResolver(); $arguments = $resolver->resolve(Request::create('/'), $metadata); self::assertEquals([[]], $arguments); } } ================================================ FILE: Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapQueryString; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\Attribute\ValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\PartialDenormalizationException; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Exception\ValidationFailedException; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\ValidatorBuilder; class RequestPayloadValueResolverTest extends TestCase { private const string FIXTURES_BASE_PATH = __DIR__.'/../../Fixtures/Controller/ArgumentResolver/UploadedFile'; public function testNotTypedArgument() { $resolver = new RequestPayloadValueResolver( new Serializer(), (new ValidatorBuilder())->getValidator(), ); $argument = new ArgumentMetadata('notTyped', null, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', server: ['HTTP_CONTENT_TYPE' => 'application/json']); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $this->expectException(\LogicException::class); $this->expectExceptionMessage('Could not resolve the "$notTyped" controller argument: argument should be typed.'); $resolver->onKernelControllerArguments($event); } public function testDefaultValueArgument() { $payload = new RequestPayload(50); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) ->method('validate'); $resolver = new RequestPayloadValueResolver(new Serializer(), $validator); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, true, $payload, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json']); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertEquals([$payload], $event->getArguments()); } public function testQueryDefaultValueArgument() { $payload = new RequestPayload(50); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) ->method('validate'); $resolver = new RequestPayloadValueResolver(new Serializer(), $validator); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, true, $payload, false, [ MapQueryString::class => new MapQueryString(), ]); $request = Request::create('/', 'GET'); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertEquals([$payload], $event->getArguments()); } public function testNullableValueArgument() { $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) ->method('validate'); $resolver = new RequestPayloadValueResolver(new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]), $validator); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, true, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json']); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertSame([null], $event->getArguments()); } public function testQueryNullableValueArgument() { $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) ->method('validate'); $resolver = new RequestPayloadValueResolver(new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]), $validator); $argument = new ArgumentMetadata('valid', QueryPayload::class, false, false, null, true, [ MapQueryString::class => new MapQueryString(), ]); $request = Request::create('/', 'GET'); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertSame([null], $event->getArguments()); } public function testMapQueryStringEmpty() { $payload = new RequestPayload(50); $denormalizer = new RequestPayloadDenormalizer($payload); $serializer = new Serializer([$denormalizer]); $resolver = new RequestPayloadValueResolver($serializer); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ MapQueryString::class => new MapQueryString(mapWhenEmpty: true), ]); $request = Request::create('/', 'GET'); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertSame([$payload], $event->getArguments()); } public function testMapRequestPayloadEmpty() { $payload = new RequestPayload(50); $denormalizer = new RequestPayloadDenormalizer($payload); $serializer = new Serializer([$denormalizer]); $resolver = new RequestPayloadValueResolver($serializer); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(mapWhenEmpty: true), ]); $request = Request::create('/', 'POST'); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertSame([$payload], $event->getArguments()); } public function testNullPayloadAndNotDefaultOrNullableArgument() { $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) ->method('validate'); $resolver = new RequestPayloadValueResolver(new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]), $validator); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json']); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $this->assertSame(400, $e->getStatusCode()); } } public function testRequestPayloadWithoutContentTypeOnNullableArgumentReturnsNull() { $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) ->method('validate'); $resolver = new RequestPayloadValueResolver(new Serializer(), $validator); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, true, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST'); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertSame([null], $event->getArguments()); } public function testQueryNullPayloadAndNotDefaultOrNullableArgument() { $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) ->method('validate'); $resolver = new RequestPayloadValueResolver(new Serializer([new ObjectNormalizer()]), $validator); $argument = new ArgumentMetadata('valid', QueryPayload::class, false, false, null, false, [ MapQueryString::class => new MapQueryString(), ]); $request = Request::create('/', 'GET'); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $this->assertSame(404, $e->getStatusCode()); } } public function testWithoutValidatorAndCouldNotDenormalize() { $content = '{"price": 50, "title": ["not a string"]}'; $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); $resolver = new RequestPayloadValueResolver($serializer); $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $this->assertInstanceOf(PartialDenormalizationException::class, $e->getPrevious()); } } public function testValidationNotPassed() { $content = '{"price": 50.0, "title": ["not a string"]}'; $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) ->method('validate'); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $validationFailedException = $e->getPrevious(); $this->assertSame(422, $e->getStatusCode()); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); $this->assertSame('This value should be of type string.', $validationFailedException->getViolations()[0]->getMessage()); } } public function testValidationFailedOnInvalidBackedEnum() { $content = '{"method": "INVALID"}'; $serializer = new Serializer([new BackedEnumNormalizer(), new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) ->method('validate'); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('invalid', RequestPayloadWithBackedEnum::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $validationFailedException = $e->getPrevious(); $this->assertSame(422, $e->getStatusCode()); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); $this->assertContains($validationFailedException->getViolations()[0]->getMessage(), [ 'This value should be of type int|string.', 'The data must belong to a backed enumeration of type Symfony\\Component\\HttpKernel\\Tests\\Controller\\ArgumentResolver\\RequestMethod', ]); } } public function testValidationNotPerformedWhenPartialDenormalizationReturnsViolation() { $content = '{"password": "abc"}'; $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) ->method('validate'); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('invalid', User::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $validationFailedException = $e->getPrevious(); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); $this->assertSame('This value should be of type string.', $validationFailedException->getViolations()[0]->getMessage()); } } public function testUnsupportedMedia() { $serializer = new Serializer(); $resolver = new RequestPayloadValueResolver($serializer); $argument = new ArgumentMetadata('invalid', \stdClass::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'foo/bar'], content: 'foo-bar'); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $this->assertSame(415, $e->getStatusCode()); } } public function testRequestContentValidationPassed() { $content = '{"price": 50}'; $payload = new RequestPayload(50); $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) ->method('validate') ->willReturn(new ConstraintViolationList()); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertEquals([$payload], $event->getArguments()); } #[TestWith([null])] #[TestWith([[]])] public function testRequestContentWithUntypedErrors(?array $types) { $this->expectException(HttpException::class); $this->expectExceptionMessage('This value was of an unexpected type.'); $serializer = $this->createStub(SerializerDenormalizer::class); if (null === $types) { $exception = new NotNormalizableValueException('Error with no types'); } else { $exception = NotNormalizableValueException::createForUnexpectedDataType('Error with no types', '', []); } $serializer->method('deserialize')->willThrowException(new PartialDenormalizationException([], [$exception])); $resolver = new RequestPayloadValueResolver($serializer, (new ValidatorBuilder())->getValidator()); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: '{"price": 50}'); $arguments = $resolver->resolve($request, new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ])); $event = new ControllerArgumentsEvent($this->createStub(HttpKernelInterface::class), static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); } public function testQueryStringValidationPassed() { $payload = new RequestPayload(50); $query = ['price' => '50']; $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) ->method('validate') ->willReturn(new ConstraintViolationList()); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ MapQueryString::class => new MapQueryString(), ]); $request = Request::create('/', 'GET', $query); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertEquals([$payload], $event->getArguments()); } public function testQueryStringParameterTypeMismatch() { $query = ['price' => 'not a float']; $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); $serializer = new Serializer([$normalizer], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never())->method('validate'); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [ MapQueryString::class => new MapQueryString(), ]); $request = Request::create('/', 'GET', $query); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $validationFailedException = $e->getPrevious(); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); $this->assertSame('This value should be of type float.', $validationFailedException->getViolations()[0]->getMessage()); } } public function testRequestInputValidationPassed() { $input = ['price' => '50']; $payload = new RequestPayload(50); $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) ->method('validate') ->willReturn(new ConstraintViolationList()); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', $input); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertEquals([$payload], $event->getArguments()); } public function testRequestArrayDenormalization() { $input = [ ['price' => '50'], ['price' => '23'], ]; $payload = [ new RequestPayload(50), new RequestPayload(23), ]; $serializer = new Serializer([new ArrayDenormalizer(), new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) ->method('validate') ->willReturn(new ConstraintViolationList()); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('prices', 'array', false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(type: RequestPayload::class), ]); $request = Request::create('/', 'POST', $input); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertEquals([$payload], $event->getArguments()); } public function testRequestInputTypeMismatch() { $input = ['price' => 'not a float']; $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); $serializer = new Serializer([$normalizer], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never())->method('validate'); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', $input); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $validationFailedException = $e->getPrevious(); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); $this->assertSame('This value should be of type float.', $validationFailedException->getViolations()[0]->getMessage()); } } public function testItThrowsOnMissingAttributeType() { $serializer = new Serializer(); $validator = (new ValidatorBuilder())->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('prices', 'array', false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST'); $request->attributes->set('_controller', 'App\Controller\SomeController::someMethod'); $this->expectException(NearMissValueResolverException::class); $this->expectExceptionMessage('Please set the $type argument of the #[Symfony\Component\HttpKernel\Attribute\MapRequestPayload] attribute to the type of the objects in the expected array.'); $resolver->resolve($request, $argument); } public function testItThrowsOnInvalidAttributeTypeUsage() { $serializer = new Serializer(); $validator = (new ValidatorBuilder())->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('prices', null, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(type: RequestPayload::class), ]); $request = Request::create('/', 'POST'); $request->attributes->set('_controller', 'App\Controller\SomeController::someMethod'); $this->expectException(NearMissValueResolverException::class); $this->expectExceptionMessage('Please set its type to "array" when using argument $type of #[Symfony\Component\HttpKernel\Attribute\MapRequestPayload].'); $resolver->resolve($request, $argument); } public function testItThrowsOnVariadicArgument() { $serializer = new Serializer(); $validator = (new ValidatorBuilder())->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('variadic', RequestPayload::class, true, false, null, false, [ MapQueryString::class => new MapQueryString(), ]); $request = Request::create('/', 'POST'); $this->expectException(\LogicException::class); $this->expectExceptionMessage('Mapping variadic argument "$variadic" is not supported.'); $resolver->resolve($request, $argument); } #[DataProvider('provideMatchedFormatContext')] public function testAcceptFormatPassed(mixed $acceptFormat, string $contentType, string $content) { $encoders = ['json' => new JsonEncoder(), 'xml' => new XmlEncoder()]; $serializer = new Serializer([new ObjectNormalizer()], $encoders); $validator = (new ValidatorBuilder())->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => $contentType], content: $content); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(acceptFormat: $acceptFormat), ]); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertEquals([new RequestPayload(50)], $event->getArguments()); } public static function provideMatchedFormatContext(): iterable { yield 'configure with json as string, sends json' => [ 'acceptFormat' => 'json', 'contentType' => 'application/json', 'content' => '{"price": 50}', ]; yield 'configure with json as array, sends json' => [ 'acceptFormat' => ['json'], 'contentType' => 'application/json', 'content' => '{"price": 50}', ]; yield 'configure with xml as string, sends xml' => [ 'acceptFormat' => 'xml', 'contentType' => 'application/xml', 'content' => '50', ]; yield 'configure with xml as array, sends xml' => [ 'acceptFormat' => ['xml'], 'contentType' => 'application/xml', 'content' => '50', ]; yield 'configure with json or xml, sends json' => [ 'acceptFormat' => ['json', 'xml'], 'contentType' => 'application/json', 'content' => '{"price": 50}', ]; yield 'configure with json or xml, sends xml' => [ 'acceptFormat' => ['json', 'xml'], 'contentType' => 'application/xml', 'content' => '50', ]; } #[DataProvider('provideMismatchedFormatContext')] public function testAcceptFormatNotPassed(mixed $acceptFormat, string $contentType, string $content, string $expectedExceptionMessage) { $serializer = new Serializer([new ObjectNormalizer()]); $validator = (new ValidatorBuilder())->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => $contentType], content: $content); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(acceptFormat: $acceptFormat), ]); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $this->assertSame(415, $e->getStatusCode()); $this->assertSame($expectedExceptionMessage, $e->getMessage()); } } public static function provideMismatchedFormatContext(): iterable { yield 'configure with json as string, sends xml' => [ 'acceptFormat' => 'json', 'contentType' => 'application/xml', 'content' => '50', 'expectedExceptionMessage' => 'Unsupported format, expects "json", but "xml" given.', ]; yield 'configure with json as array, sends xml' => [ 'acceptFormat' => ['json'], 'contentType' => 'application/xml', 'content' => '50', 'expectedExceptionMessage' => 'Unsupported format, expects "json", but "xml" given.', ]; yield 'configure with xml as string, sends json' => [ 'acceptFormat' => 'xml', 'contentType' => 'application/json', 'content' => '{"price": 50}', 'expectedExceptionMessage' => 'Unsupported format, expects "xml", but "json" given.', ]; yield 'configure with xml as array, sends json' => [ 'acceptFormat' => ['xml'], 'contentType' => 'application/json', 'content' => '{"price": 50}', 'expectedExceptionMessage' => 'Unsupported format, expects "xml", but "json" given.', ]; yield 'configure with json or xml, sends jsonld' => [ 'acceptFormat' => ['json', 'xml'], 'contentType' => 'application/ld+json', 'content' => '{"@context": "https://schema.org", "@type": "FakeType", "price": 50}', 'expectedExceptionMessage' => 'Unsupported format, expects "json", "xml", but "jsonld" given.', ]; } #[DataProvider('provideValidationGroupsOnManyTypes')] public function testValidationGroupsPassed(string $method, ValueResolver $attribute) { $input = ['price' => '50', 'title' => 'A long title, so the validation passes']; $payload = new RequestPayload(50); $payload->title = 'A long title, so the validation passes'; $serializer = new Serializer([new ObjectNormalizer()]); $validator = (new ValidatorBuilder())->enableAttributeMapping()->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); $request = Request::create('/', $method, $input); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ $attribute::class => $attribute, ]); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertEquals([$payload], $event->getArguments()); } #[DataProvider('provideValidationGroupsOnManyTypes')] public function testValidationGroupsNotPassed(string $method, ValueResolver $attribute) { $input = ['price' => '50', 'title' => 'Too short']; $serializer = new Serializer([new ObjectNormalizer()]); $validator = (new ValidatorBuilder())->enableAttributeMapping()->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('valid', RequestPayload::class, false, false, null, false, [ $attribute::class => $attribute, ]); $request = Request::create('/', $method, $input); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $validationFailedException = $e->getPrevious(); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); $this->assertSame('title', $validationFailedException->getViolations()[0]->getPropertyPath()); $this->assertSame('This value is too short. It should have 10 characters or more.', $validationFailedException->getViolations()[0]->getMessage()); } } public static function provideValidationGroupsOnManyTypes(): iterable { yield 'request payload with validation group as string' => [ 'POST', new MapRequestPayload(validationGroups: 'strict'), ]; yield 'request payload with validation group as array' => [ 'POST', new MapRequestPayload(validationGroups: ['strict']), ]; yield 'request payload with validation group as GroupSequence' => [ 'POST', new MapRequestPayload(validationGroups: new Assert\GroupSequence(['strict'])), ]; yield 'query with validation group as string' => [ 'GET', new MapQueryString(validationGroups: 'strict'), ]; yield 'query with validation group as array' => [ 'GET', new MapQueryString(validationGroups: ['strict']), ]; yield 'query with validation group as GroupSequence' => [ 'GET', new MapQueryString(validationGroups: new Assert\GroupSequence(['strict'])), ]; } public function testQueryValidationErrorCustomStatusCode() { $serializer = new Serializer([new ObjectNormalizer()], []); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) ->method('validate') ->willReturn(new ConstraintViolationList([new ConstraintViolation('Page is invalid', null, [], '', null, '')])); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('page', QueryPayload::class, false, false, null, false, [ MapQueryString::class => new MapQueryString(validationFailedStatusCode: 400), ]); $request = Request::create('/?page=123'); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $validationFailedException = $e->getPrevious(); $this->assertSame(400, $e->getStatusCode()); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); $this->assertSame('Page is invalid', $validationFailedException->getViolations()[0]->getMessage()); } } public function testRequestPayloadValidationErrorCustomStatusCode() { $content = '{"price": 50, "title": ["not a string"]}'; $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) ->method('validate'); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(validationFailedStatusCode: 400), ]); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $validationFailedException = $e->getPrevious(); $this->assertSame(400, $e->getStatusCode()); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); $this->assertSame('This value should be of type string.', $validationFailedException->getViolations()[0]->getMessage()); } } #[DataProvider('provideBoolArgument')] public function testBoolArgumentInQueryString(mixed $expectedValue, ?string $parameterValue) { $serializer = new Serializer([new ObjectNormalizer()]); $validator = (new ValidatorBuilder())->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('filtered', ObjectWithBoolArgument::class, false, false, null, false, [ MapQueryString::class => new MapQueryString(), ]); $request = Request::create('/', 'GET', ['value' => $parameterValue]); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertSame($expectedValue, $event->getArguments()[0]->value); } #[DataProvider('provideBoolArgument')] public function testBoolArgumentInBody(mixed $expectedValue, ?string $parameterValue) { $serializer = new Serializer([new ObjectNormalizer()]); $validator = (new ValidatorBuilder())->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('filtered', ObjectWithBoolArgument::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', ['value' => $parameterValue], server: ['CONTENT_TYPE' => 'multipart/form-data']); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertSame($expectedValue, $event->getArguments()[0]->value); } public static function provideBoolArgument() { yield 'default value' => [null, null]; yield '"0"' => [false, '0']; yield '"false"' => [false, 'false']; yield '"no"' => [false, 'no']; yield '"off"' => [false, 'off']; yield '"1"' => [true, '1']; yield '"true"' => [true, 'true']; yield '"yes"' => [true, 'yes']; yield '"on"' => [true, 'on']; } /** * Boolean filtering must be disabled for content types other than form data. */ public function testBoolArgumentInJsonBody() { $serializer = new Serializer([new ObjectNormalizer()]); $validator = (new ValidatorBuilder())->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('filtered', ObjectWithBoolArgument::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', ['value' => 'off'], server: ['CONTENT_TYPE' => 'application/json']); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertTrue($event->getArguments()[0]->value); } public function testConfigKeyForQueryString() { $serializer = new Serializer([new ObjectNormalizer()]); $validator = (new ValidatorBuilder())->getValidator(); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('filtered', QueryPayload::class, false, false, null, false, [ MapQueryString::class => new MapQueryString(key: 'value'), ]); $request = Request::create('/', 'GET', ['value' => ['page' => 1.0]]); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertInstanceOf(QueryPayload::class, $event->getArguments()[0]); $this->assertSame(1.0, $event->getArguments()[0]->page); } public function testMapRequestPayloadVariadic() { $input = [ ['price' => '50'], ['price' => '23'], ]; $payload = [ new RequestPayload(50), new RequestPayload(23), ]; $serializer = new Serializer([new ArrayDenormalizer(), new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) ->method('validate') ->willReturn(new ConstraintViolationList()); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('prices', RequestPayload::class, true, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', $input); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertEquals($payload, $event->getArguments()); } public function testMapRequestPayloadVariadicJson() { $payload = [ new RequestPayload(50), new RequestPayload(23), ]; $serializer = new Serializer([new ArrayDenormalizer(), new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) ->method('validate') ->willReturn(new ConstraintViolationList()); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('prices', RequestPayload::class, true, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: '[{"price": 50}, {"price": 23}]'); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertEquals($payload, $event->getArguments()); } public function testMapRequestPayloadWithUploadedFiles() { $image = new UploadedFile(self::FIXTURES_BASE_PATH.'/file-small.txt', 'file-small.txt'); $input = ['price' => '50']; $files = ['image' => $image]; $payload = new RequestPayloadWithFile(50); $payload->image = $image; $serializer = new Serializer([new ObjectNormalizer()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) ->method('validate') ->willReturn(new ConstraintViolationList()); $resolver = new RequestPayloadValueResolver($serializer, $validator); $argument = new ArgumentMetadata('data', RequestPayloadWithFile::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = Request::create('/', 'POST', $input, [], $files); $kernel = $this->createStub(HttpKernelInterface::class); $argument = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static function () {}, $argument, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertEquals([$payload], $event->getArguments()); } public function testExpressionAsValidationGroup() { $content = '{"price": 24}'; $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) ->method('validate') ->with(new RequestPayload(24.0), null, 'foo'); $resolver = new RequestPayloadValueResolver($serializer, $validator, expressionLanguage: new ExpressionLanguage()); $argument = new ArgumentMetadata('payload', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(validationGroups: new Expression('args["foo"]')), ]); $request = Request::create('/{foo}/{bar}/{baz}', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content); $arguments = (array) $resolver->resolve($request, $argument); array_unshift($arguments, 'foo', 15, 1.23); $kernel = $this->createStub(HttpKernelInterface::class); $controllerEvent = new ControllerEvent($kernel, [new BasicTypesController(), 'action'], $request, HttpKernelInterface::MAIN_REQUEST); $event = new ControllerArgumentsEvent($kernel, $controllerEvent, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); } public function testExpressionAsValidationGroupCanUseController() { $content = '{"price": 24}'; $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) ->method('validate') ->with(new RequestPayload(24.0), null, 'foo'); $resolver = new RequestPayloadValueResolver($serializer, $validator, expressionLanguage: new ExpressionLanguage()); $argument = new ArgumentMetadata('payload', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(validationGroups: new Expression('this ? args["foo"] : "bar"')), ]); $request = Request::create('/{foo}/{bar}/{baz}', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content); $arguments = (array) $resolver->resolve($request, $argument); array_unshift($arguments, 'foo', 15, 1.23); $kernel = $this->createStub(HttpKernelInterface::class); $controllerEvent = new ControllerEvent($kernel, [new BasicTypesController(), 'action'], $request, HttpKernelInterface::MAIN_REQUEST); $event = new ControllerArgumentsEvent($kernel, $controllerEvent, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); } public function testClosureAsValidationGroup() { $content = '{"price": 24}'; $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) ->method('validate') ->with(new RequestPayload(24.0), null, 'foo'); $resolver = new RequestPayloadValueResolver($serializer, $validator, expressionLanguage: new ExpressionLanguage()); $asserted = false; $self = $this; $argument = new ArgumentMetadata('payload', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(validationGroups: static function (array $args, Request $request, ?object $controller) use (&$asserted, $self): string { $self->assertInstanceOf(BasicTypesController::class, $controller); $asserted = true; return $args['foo']; }), ]); $request = Request::create('/{foo}/{bar}/{baz}', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content); $arguments = (array) $resolver->resolve($request, $argument); array_unshift($arguments, 'foo', 15, 1.23); $kernel = $this->createStub(HttpKernelInterface::class); $controllerEvent = new ControllerEvent($kernel, [new BasicTypesController(), 'action'], $request, HttpKernelInterface::MAIN_REQUEST); $event = new ControllerArgumentsEvent($kernel, $controllerEvent, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); $this->assertTrue($asserted); } public function testExpressionAsValidationGroupForQueryString() { $serializer = new Serializer([new ObjectNormalizer()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->once()) ->method('validate') ->with(new QueryPayload(1.0), null, 'foo') ->willReturn(new ConstraintViolationList()); $resolver = new RequestPayloadValueResolver($serializer, $validator, expressionLanguage: new ExpressionLanguage()); $argument = new ArgumentMetadata('payload', QueryPayload::class, false, false, null, false, [ MapQueryString::class => new MapQueryString(validationGroups: new Expression('args["foo"]')), ]); $request = Request::create('/', 'GET', ['page' => 1.0]); $arguments = (array) $resolver->resolve($request, $argument); array_unshift($arguments, 'foo', 15, 1.23); $kernel = $this->createStub(HttpKernelInterface::class); $controllerEvent = new ControllerEvent($kernel, [new BasicTypesController(), 'action'], $request, HttpKernelInterface::MAIN_REQUEST); $event = new ControllerArgumentsEvent($kernel, $controllerEvent, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); } public function testNestedExpressionsInValidationGroupsAreNotSupported() { $content = '{"price": 24}'; $serializer = new Serializer([new ObjectNormalizer()], ['json' => new JsonEncoder()]); $validator = $this->createMock(ValidatorInterface::class); $validator->expects($this->never()) ->method('validate'); $resolver = new RequestPayloadValueResolver($serializer, $validator, expressionLanguage: new ExpressionLanguage()); $argument = new ArgumentMetadata('payload', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(validationGroups: ['foo', new Expression('args["foo"]')]), ]); $request = Request::create('/', 'POST', server: ['CONTENT_TYPE' => 'application/json'], content: $content); $arguments = (array) $resolver->resolve($request, $argument); array_unshift($arguments, 'foo', 15, 1.23); $kernel = $this->createStub(HttpKernelInterface::class); $controllerEvent = new ControllerEvent($kernel, [new BasicTypesController(), 'action'], $request, HttpKernelInterface::MAIN_REQUEST); $event = new ControllerArgumentsEvent($kernel, $controllerEvent, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $this->expectException(\LogicException::class); $resolver->onKernelControllerArguments($event); } public function testMapRequestPayloadWithPreParsedJsonIntCoercesToFloat() { $serializer = new Serializer( [new ObjectNormalizer(null, null, null, new ReflectionExtractor())], ['json' => new JsonEncoder()] ); $resolver = new RequestPayloadValueResolver($serializer); $argument = new ArgumentMetadata('payload', RequestPayload::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = new Request([], ['price' => 0], [], [], [], ['CONTENT_TYPE' => 'application/json']); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); /** @var RequestPayload $payload */ [$payload] = $event->getArguments(); $this->assertInstanceOf(RequestPayload::class, $payload); $this->assertSame(0.0, $payload->price); } public function testMapRequestPayloadWithFormDataCoercesStringToBool() { $serializer = new Serializer( [new ObjectNormalizer(null, null, null, new ReflectionExtractor())], [] ); $resolver = new RequestPayloadValueResolver($serializer); $argument = new ArgumentMetadata('payload', FormPayloadWithBool::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = new Request([], ['active' => '0'], [], [], [], ['CONTENT_TYPE' => 'application/x-www-form-urlencoded']); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); /** @var FormPayloadWithBool $payload */ [$payload] = $event->getArguments(); $this->assertInstanceOf(FormPayloadWithBool::class, $payload); $this->assertFalse($payload->active); } public function testMapRequestPayloadWithJsonContentTypeStringValuesCoercesToBool() { $serializer = new Serializer( [new ObjectNormalizer(null, null, null, new ReflectionExtractor())], [] ); $resolver = new RequestPayloadValueResolver($serializer); $argument = new ArgumentMetadata('payload', FormPayloadWithBool::class, false, false, null, false, [ MapRequestPayload::class => new MapRequestPayload(), ]); $request = new Request([], ['active' => '0'], [], [], [], ['CONTENT_TYPE' => 'application/json']); $kernel = $this->createStub(HttpKernelInterface::class); $arguments = $resolver->resolve($request, $argument); $event = new ControllerArgumentsEvent($kernel, static fn () => null, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); $resolver->onKernelControllerArguments($event); /** @var FormPayloadWithBool $payload */ [$payload] = $event->getArguments(); $this->assertInstanceOf(FormPayloadWithBool::class, $payload); $this->assertFalse($payload->active); } } class RequestPayload { #[Assert\Length(min: 10, groups: ['strict'])] public string $title; public function __construct(public readonly float $price) { } } class RequestPayloadWithFile extends RequestPayload { public ?UploadedFile $image = null; } interface SerializerDenormalizer extends SerializerInterface, DenormalizerInterface { } class QueryPayload { public function __construct(public readonly float $page) { } } class User { public function __construct( #[Assert\NotBlank, Assert\Email] private string $email, #[Assert\NotBlank] private string $password, ) { } public function getEmail(): string { return $this->email; } public function getPassword(): string { return $this->password; } } class RequestPayloadWithBackedEnum { public function __construct(public readonly RequestMethod $method) { } } enum RequestMethod: string { case GET = 'GET'; case POST = 'POST'; } class ObjectWithBoolArgument { public function __construct(public readonly ?bool $value = null) { } } class FormPayloadWithBool { public function __construct(public readonly bool $active) { } } class RequestPayloadDenormalizer implements DenormalizerInterface { public function __construct(private RequestPayload $payload) { } public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed { return $this->payload; } public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool { return RequestPayload::class === $type; } public function getSupportedTypes(?string $format = null): array { return [RequestPayload::class => true]; } } ================================================ FILE: Tests/Controller/ArgumentResolver/RequestValueResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\TestCase; use Symfony\Component\BrowserKit\Request as RandomRequest; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; class RequestValueResolverTest extends TestCase { public function testSameRequestReturned() { $resolver = new RequestValueResolver(); $expectedRequest = Request::create('/'); $actualRequest = $resolver->resolve($expectedRequest, new ArgumentMetadata('request', Request::class, false, false, null)); self::assertCount(1, $actualRequest); self::assertSame($expectedRequest, $actualRequest[0] ?? null); } public function testRequestIsNotResolvedForRandomClass() { $resolver = new RequestValueResolver(); $expectedRequest = Request::create('/'); $actualRequest = $resolver->resolve($expectedRequest, new ArgumentMetadata('request', self::class, false, false, null)); self::assertCount(0, $actualRequest); } public function testExceptionThrownForRandomRequestClass() { $resolver = new RequestValueResolver(); $expectedRequest = Request::create('/'); $this->expectException(NearMissValueResolverException::class); $resolver->resolve($expectedRequest, new ArgumentMetadata('request', RandomRequest::class, false, false, null)); } } ================================================ FILE: Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass; use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; class ServiceValueResolverTest extends TestCase { public function testDoNotSupportWhenControllerDoNotExists() { $resolver = new ServiceValueResolver(new ServiceLocator([])); $argument = new ArgumentMetadata('dummy', DummyService::class, false, false, null); $request = $this->requestWithAttributes(['_controller' => 'my_controller']); $this->assertSame([], $resolver->resolve($request, $argument)); } public function testExistingController() { $resolver = new ServiceValueResolver(new ServiceLocator([ 'App\\Controller\\Mine::method' => static fn () => new ServiceLocator([ 'dummy' => static fn () => new DummyService(), ]), ])); $request = $this->requestWithAttributes(['_controller' => 'App\\Controller\\Mine::method']); $argument = new ArgumentMetadata('dummy', DummyService::class, false, false, null); $this->assertEquals([new DummyService()], $resolver->resolve($request, $argument)); } public function testExistingControllerWithATrailingBackSlash() { $resolver = new ServiceValueResolver(new ServiceLocator([ 'App\\Controller\\Mine::method' => static fn () => new ServiceLocator([ 'dummy' => static fn () => new DummyService(), ]), ])); $request = $this->requestWithAttributes(['_controller' => '\\App\\Controller\\Mine::method']); $argument = new ArgumentMetadata('dummy', DummyService::class, false, false, null); $this->assertEquals([new DummyService()], $resolver->resolve($request, $argument)); } public function testExistingControllerWithMethodNameStartUppercase() { $resolver = new ServiceValueResolver(new ServiceLocator([ 'App\\Controller\\Mine::method' => static fn () => new ServiceLocator([ 'dummy' => static fn () => new DummyService(), ]), ])); $request = $this->requestWithAttributes(['_controller' => 'App\\Controller\\Mine::Method']); $argument = new ArgumentMetadata('dummy', DummyService::class, false, false, null); $this->assertEquals([new DummyService()], $resolver->resolve($request, $argument)); } public function testControllerNameIsAnArray() { $resolver = new ServiceValueResolver(new ServiceLocator([ 'App\\Controller\\Mine::method' => static fn () => new ServiceLocator([ 'dummy' => static fn () => new DummyService(), ]), ])); $request = $this->requestWithAttributes(['_controller' => ['App\\Controller\\Mine', 'method']]); $argument = new ArgumentMetadata('dummy', DummyService::class, false, false, null); $this->assertEquals([new DummyService()], $resolver->resolve($request, $argument)); } public function testErrorIsTruncated() { $this->expectException(NearMissValueResolverException::class); $this->expectExceptionMessage('Cannot autowire argument $dummy required by "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyController::index()": it references class "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyService" but no such service exists.'); $container = new ContainerBuilder(); $container->addCompilerPass(new RegisterControllerArgumentLocatorsPass()); $container->register('argument_resolver.service', ServiceValueResolver::class)->addArgument(null)->setPublic(true); $container->register(DummyController::class)->addTag('controller.service_arguments')->setPublic(true); $container->compile(); $request = $this->requestWithAttributes(['_controller' => [DummyController::class, 'index']]); $argument = new ArgumentMetadata('dummy', DummyService::class, false, false, null); $container->get('argument_resolver.service')->resolve($request, $argument)->current(); } private function requestWithAttributes(array $attributes) { $request = Request::create('/'); foreach ($attributes as $name => $value) { $request->attributes->set($name, $value); } return $request; } } class DummyService { } class DummyController { public function index(DummyService $dummy) { } } ================================================ FILE: Tests/Controller/ArgumentResolver/TraceableValueResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Stopwatch\Stopwatch; class TraceableValueResolverTest extends TestCase { public function testTimingsInResolve() { $stopwatch = new Stopwatch(); $resolver = new TraceableValueResolver(new ResolverStub(), $stopwatch); $argument = new ArgumentMetadata('dummy', 'string', false, false, null); $request = new Request(); $iterable = $resolver->resolve($request, $argument); foreach ($iterable as $index => $resolved) { $event = $stopwatch->getEvent(ResolverStub::class.'::resolve'); $this->assertTrue($event->isStarted()); $this->assertSame([], $event->getPeriods()); switch ($index) { case 0: $this->assertEquals('first', $resolved); break; case 1: $this->assertEquals('second', $resolved); break; } } $event = $stopwatch->getEvent(ResolverStub::class.'::resolve'); $this->assertCount(1, $event->getPeriods()); } } class ResolverStub implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): iterable { yield 'first'; yield 'second'; } } ================================================ FILE: Tests/Controller/ArgumentResolver/UidValueResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\UidValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Uid\AbstractUid; use Symfony\Component\Uid\Factory\UlidFactory; use Symfony\Component\Uid\Ulid; use Symfony\Component\Uid\UuidV1; use Symfony\Component\Uid\UuidV4; class UidValueResolverTest extends TestCase { #[DataProvider('provideSupports')] public function testSupports(bool $expected, Request $request, ArgumentMetadata $argument) { $this->assertCount((int) $expected, (new UidValueResolver())->resolve($request, $argument)); } public static function provideSupports() { return [ 'Variadic argument' => [false, new Request([], [], ['foo' => (string) $uuidV4 = new UuidV4()]), new ArgumentMetadata('foo', UuidV4::class, true, false, null)], 'No attribute for argument' => [false, new Request([], [], []), new ArgumentMetadata('foo', UuidV4::class, false, false, null)], 'Attribute is not a string' => [false, new Request([], [], ['foo' => ['bar']]), new ArgumentMetadata('foo', UuidV4::class, false, false, null)], 'Argument has no type' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', null, false, false, null)], 'Argument type is not a class' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', 'string', false, false, null)], 'Argument type is not a subclass of AbstractUid' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', UlidFactory::class, false, false, null)], 'AbstractUid is not supported' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', AbstractUid::class, false, false, null)], 'Known subclass' => [true, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', UuidV4::class, false, false, null)], 'Format does not matter' => [true, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', Ulid::class, false, false, null)], ]; } #[DataProvider('provideResolveOK')] public function testResolveOK(AbstractUid $expected, string $requestUid) { $this->assertEquals([$expected], (new UidValueResolver())->resolve( new Request([], [], ['id' => $requestUid]), new ArgumentMetadata('id', $expected::class, false, false, null) )); } public static function provideResolveOK() { return [ [$uuidV1 = new UuidV1(), (string) $uuidV1], [$uuidV1, $uuidV1->toBase58()], [$uuidV1, $uuidV1->toBase32()], [$ulid = Ulid::fromBase32('01FQC6Y03WDZ73DQY9RXQMPHB1'), (string) $ulid], [$ulid, $ulid->toBase58()], [$ulid, $ulid->toRfc4122()], [$customUid = new TestCustomUid(), (string) $customUid], [$customUid, $customUid->toBase58()], [$customUid, $customUid->toBase32()], ]; } #[DataProvider('provideResolveKO')] public function testResolveKO(string $requestUid, string $argumentType) { $this->expectException(NotFoundHttpException::class); $this->expectExceptionMessage('The uid for the "id" parameter is invalid.'); (new UidValueResolver())->resolve( new Request([], [], ['id' => $requestUid]), new ArgumentMetadata('id', $argumentType, false, false, null) ); } public static function provideResolveKO() { return [ 'Bad value for UUID' => ['ccc', UuidV1::class], 'Bad value for ULID' => ['ccc', Ulid::class], 'Bad value for custom UID' => ['ccc', TestCustomUid::class], 'Bad UUID version' => [(string) new UuidV4(), UuidV1::class], ]; } public function testResolveAbstractClass() { $this->expectException(\Error::class); $this->expectExceptionMessage('Cannot instantiate abstract class Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\TestAbstractCustomUid'); (new UidValueResolver())->resolve( new Request([], [], ['id' => (string) new UuidV1()]), new ArgumentMetadata('id', TestAbstractCustomUid::class, false, false, null) ); } } class TestCustomUid extends UuidV1 { } abstract class TestAbstractCustomUid extends UuidV1 { } ================================================ FILE: Tests/Controller/ArgumentResolver/UploadedFileValueResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\MapUploadedFile; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestPayloadValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Exception\ValidationFailedException; use Symfony\Component\Validator\ValidatorBuilder; class UploadedFileValueResolverTest extends TestCase { private const FIXTURES_BASE_PATH = __DIR__.'/../../Fixtures/Controller/ArgumentResolver/UploadedFile'; #[DataProvider('provideContext')] public function testDefaults(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(); $argument = new ArgumentMetadata( 'foo', UploadedFile::class, false, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $resolver->onKernelControllerArguments($event); /** @var UploadedFile $data */ $data = $event->getArguments()[0]; $this->assertInstanceOf(UploadedFile::class, $data); $this->assertSame('file-small.txt', $data->getFilename()); $this->assertSame(36, $data->getSize()); } #[DataProvider('provideContext')] public function testEmpty(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(); $argument = new ArgumentMetadata( 'qux', UploadedFile::class, false, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $this->expectException(HttpException::class); $resolver->onKernelControllerArguments($event); } public function testEmptyArrayUploadedFileArgument() { $resolver = new RequestPayloadValueResolver( new Serializer(), (new ValidatorBuilder())->getValidator() ); $request = Request::create( '/', 'POST', files: [ 'qux' => [], ], server: ['HTTP_CONTENT_TYPE' => 'multipart/form-data'] ); $attribute = new MapUploadedFile(); $argument = new ArgumentMetadata( 'qux', UploadedFile::class, false, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $this->expectException(HttpException::class); $resolver->onKernelControllerArguments($event); } #[DataProvider('provideContext')] public function testEmptyArrayArgument(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(); $argument = new ArgumentMetadata( 'qux', 'array', false, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $resolver->onKernelControllerArguments($event); $data = $event->getArguments()[0]; $this->assertSame([], $data); } #[DataProvider('provideContext')] public function testCustomName(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(name: 'bar'); $argument = new ArgumentMetadata( 'foo', UploadedFile::class, false, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $resolver->onKernelControllerArguments($event); /** @var UploadedFile $data */ $data = $event->getArguments()[0]; $this->assertInstanceOf(UploadedFile::class, $data); $this->assertSame('file-big.txt', $data->getFilename()); $this->assertSame(71, $data->getSize()); } #[DataProvider('provideContext')] public function testConstraintsWithoutViolation(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(constraints: new Assert\File(maxSize: 100)); $argument = new ArgumentMetadata( 'bar', UploadedFile::class, false, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $resolver->onKernelControllerArguments($event); /** @var UploadedFile $data */ $data = $event->getArguments()[0]; $this->assertInstanceOf(UploadedFile::class, $data); $this->assertSame('file-big.txt', $data->getFilename()); $this->assertSame(71, $data->getSize()); } #[DataProvider('provideContext')] public function testConstraintsWithViolation(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(constraints: new Assert\File(maxSize: 50)); $argument = new ArgumentMetadata( 'bar', UploadedFile::class, false, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $this->expectException(HttpException::class); $this->expectExceptionMessageMatches('/^The file is too large/'); $resolver->onKernelControllerArguments($event); } #[DataProvider('provideContext')] public function testConstraintsViolationHasArgumentNameAsPropertyPath(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(constraints: new Assert\File(maxSize: 50)); $argument = new ArgumentMetadata( 'bar', UploadedFile::class, false, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); try { $resolver->onKernelControllerArguments($event); $this->fail(\sprintf('Expected "%s" to be thrown.', HttpException::class)); } catch (HttpException $e) { $validationFailedException = $e->getPrevious(); $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); $this->assertSame('bar', $validationFailedException->getViolations()[0]->getPropertyPath()); } } #[DataProvider('provideContext')] public function testMultipleFilesArray(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(); $argument = new ArgumentMetadata( 'baz', UploadedFile::class, false, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $resolver->onKernelControllerArguments($event); /** @var UploadedFile[] $data */ $data = $event->getArguments()[0]; $this->assertCount(2, $data); $this->assertSame('file-small.txt', $data[0]->getFilename()); $this->assertSame(36, $data[0]->getSize()); $this->assertSame('file-big.txt', $data[1]->getFilename()); $this->assertSame(71, $data[1]->getSize()); } #[DataProvider('provideContext')] public function testMultipleFilesArrayConstraints(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(constraints: new Assert\File(maxSize: 50)); $argument = new ArgumentMetadata( 'baz', UploadedFile::class, false, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $this->expectException(HttpException::class); $this->expectExceptionMessageMatches('/^The file is too large/'); $resolver->onKernelControllerArguments($event); } #[DataProvider('provideContext')] public function testSingleFileVariadic(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(); $argument = new ArgumentMetadata( 'foo', UploadedFile::class, true, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $resolver->onKernelControllerArguments($event); /** @var UploadedFile[] $data */ $data = $event->getArguments(); $this->assertCount(1, $data); $this->assertSame('file-small.txt', $data[0]->getFilename()); $this->assertSame(36, $data[0]->getSize()); } #[DataProvider('provideContext')] public function testMultipleFilesVariadic(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(); $argument = new ArgumentMetadata( 'baz', UploadedFile::class, true, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $resolver->onKernelControllerArguments($event); /** @var UploadedFile[] $data */ $data = $event->getArguments(); $this->assertCount(2, $data); $this->assertSame('file-small.txt', $data[0]->getFilename()); $this->assertSame(36, $data[0]->getSize()); $this->assertSame('file-big.txt', $data[1]->getFilename()); $this->assertSame(71, $data[1]->getSize()); } #[DataProvider('provideContext')] public function testMultipleFilesVariadicConstraints(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(constraints: new Assert\File(maxSize: 50)); $argument = new ArgumentMetadata( 'baz', UploadedFile::class, true, false, null, false, [$attribute::class => $attribute] ); $event = new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $this->expectException(HttpException::class); $this->expectExceptionMessageMatches('/^The file is too large/'); $resolver->onKernelControllerArguments($event); } #[DataProvider('provideContext')] public function testShouldAllowEmptyWhenNullable(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(); $argument = new ArgumentMetadata( 'qux', UploadedFile::class, false, false, null, true, [$attribute::class => $attribute] ); /** @var HttpKernelInterface&MockObject $httpKernel */ $httpKernel = $this->createStub(HttpKernelInterface::class); $event = new ControllerArgumentsEvent( $httpKernel, static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $resolver->onKernelControllerArguments($event); $data = $event->getArguments()[0]; $this->assertNull($data); } #[DataProvider('provideContext')] public function testShouldAllowEmptyWhenNullableArray(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(); $argument = new ArgumentMetadata( 'qux', 'array', false, false, null, true, [$attribute::class => $attribute] ); /** @var HttpKernelInterface&MockObject $httpKernel */ $httpKernel = $this->createStub(HttpKernelInterface::class); $event = new ControllerArgumentsEvent( $httpKernel, static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $resolver->onKernelControllerArguments($event); $data = $event->getArguments()[0]; $this->assertNull($data); } #[DataProvider('provideContext')] public function testShouldAllowEmptyWhenHasDefaultValue(RequestPayloadValueResolver $resolver, Request $request) { $attribute = new MapUploadedFile(); $argument = new ArgumentMetadata( 'qux', UploadedFile::class, false, true, 'default-value', false, [$attribute::class => $attribute] ); /** @var HttpKernelInterface&MockObject $httpKernel */ $httpKernel = $this->createStub(HttpKernelInterface::class); $event = new ControllerArgumentsEvent( $httpKernel, static function () {}, $resolver->resolve($request, $argument), $request, HttpKernelInterface::MAIN_REQUEST ); $resolver->onKernelControllerArguments($event); $data = $event->getArguments()[0]; $this->assertSame('default-value', $data); } public static function provideContext(): iterable { $resolver = new RequestPayloadValueResolver( new Serializer(), (new ValidatorBuilder())->getValidator() ); $small = new UploadedFile( self::FIXTURES_BASE_PATH.'/file-small.txt', 'file-small.txt', 'text/plain', null, true ); $big = new UploadedFile( self::FIXTURES_BASE_PATH.'/file-big.txt', 'file-big.txt', 'text/plain', null, true ); $request = Request::create( '/', 'POST', files: [ 'foo' => $small, 'bar' => $big, 'baz' => [$small, $big], ], server: ['HTTP_CONTENT_TYPE' => 'multipart/form-data'] ); yield 'standard' => [$resolver, $request]; } } ================================================ FILE: Tests/Controller/ArgumentResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpKernel\Attribute\ValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; use Symfony\Component\HttpKernel\Exception\ResolverNotFoundException; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ExtendingRequest; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ExtendingSession; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController; class ArgumentResolverTest extends TestCase { public static function getResolver(array $chainableResolvers = [], ?array $namedResolvers = null): ArgumentResolver { if (null !== $namedResolvers) { $namedResolvers = new ServiceLocator(array_map(static fn ($resolver) => static fn () => $resolver, $namedResolvers)); } return new ArgumentResolver(new ArgumentMetadataFactory(), $chainableResolvers, $namedResolvers); } public function testDefaultState() { $this->assertEquals(self::getResolver(), new ArgumentResolver()); $this->assertNotEquals(self::getResolver(), new ArgumentResolver(null, [new RequestAttributeValueResolver()])); } public function testGetArguments() { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $controller = [new ArgumentResolverTestController(), 'controllerWithFoo']; $this->assertEquals(['foo'], self::getResolver()->getArguments($request, $controller), '->getArguments() returns an array of arguments for the controller method'); } public function testGetArgumentsReturnsEmptyArrayWhenNoArguments() { $request = Request::create('/'); $controller = [new ArgumentResolverTestController(), 'controllerWithoutArguments']; $this->assertEquals([], self::getResolver()->getArguments($request, $controller), '->getArguments() returns an empty array if the method takes no arguments'); } public function testGetArgumentsUsesDefaultValue() { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $controller = [new ArgumentResolverTestController(), 'controllerWithFooAndDefaultBar']; $this->assertEquals(['foo', null], self::getResolver()->getArguments($request, $controller), '->getArguments() uses default values if present'); } public function testGetArgumentsOverrideDefaultValueByRequestAttribute() { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('bar', 'bar'); $controller = [new ArgumentResolverTestController(), 'controllerWithFooAndDefaultBar']; $this->assertEquals(['foo', 'bar'], self::getResolver()->getArguments($request, $controller), '->getArguments() overrides default values if provided in the request attributes'); } public function testGetArgumentsFromClosure() { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $controller = static function ($foo) {}; $this->assertEquals(['foo'], self::getResolver()->getArguments($request, $controller)); } public function testGetArgumentsUsesDefaultValueFromClosure() { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $controller = static function ($foo, $bar = 'bar') {}; $this->assertEquals(['foo', 'bar'], self::getResolver()->getArguments($request, $controller)); } public function testGetArgumentsFromInvokableObject() { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $controller = new ArgumentResolverTestController(); $this->assertEquals(['foo', null], self::getResolver()->getArguments($request, $controller)); // Test default bar overridden by request attribute $request->attributes->set('bar', 'bar'); $this->assertEquals(['foo', 'bar'], self::getResolver()->getArguments($request, $controller)); } public function testGetArgumentsFromFunctionName() { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('foobar', 'foobar'); $controller = __NAMESPACE__.'\controller_function'; $this->assertEquals(['foo', 'foobar'], self::getResolver()->getArguments($request, $controller)); } public function testGetArgumentsFailsOnUnresolvedValue() { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('foobar', 'foobar'); $controller = [new ArgumentResolverTestController(), 'controllerWithFooBarFoobar']; try { self::getResolver()->getArguments($request, $controller); $this->fail('->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); } catch (\Exception $e) { $this->assertInstanceOf(\RuntimeException::class, $e, '->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); } } public function testGetArgumentsInjectsRequest() { $request = Request::create('/'); $controller = [new ArgumentResolverTestController(), 'controllerWithRequest']; $this->assertEquals([$request], self::getResolver()->getArguments($request, $controller), '->getArguments() injects the request'); } public function testGetArgumentsInjectsExtendingRequest() { $request = ExtendingRequest::create('/'); $controller = [new ArgumentResolverTestController(), 'controllerWithExtendingRequest']; $this->assertEquals([$request], self::getResolver()->getArguments($request, $controller), '->getArguments() injects the request when extended'); } public function testGetVariadicArguments() { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('bar', ['foo', 'bar']); $controller = [new VariadicController(), 'action']; $this->assertEquals(['foo', 'foo', 'bar'], self::getResolver()->getArguments($request, $controller)); } public function testGetVariadicArgumentsWithoutArrayInRequest() { $this->expectException(\InvalidArgumentException::class); $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('bar', 'foo'); $controller = [new VariadicController(), 'action']; self::getResolver()->getArguments($request, $controller); } public function testIfExceptionIsThrownWhenMissingAnArgument() { $request = Request::create('/'); $controller = (new ArgumentResolverTestController())->controllerWithFoo(...); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Controller "'.ArgumentResolverTestController::class.'::controllerWithFoo" requires the "$foo" argument that could not be resolved. Either the argument is nullable and no null value has been provided, no default value has been provided or there is a non-optional argument after this one.'); self::getResolver()->getArguments($request, $controller); } public function testGetNullableArguments() { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('bar', new \stdClass()); $request->attributes->set('last', 'last'); $controller = [new NullableController(), 'action']; $this->assertEquals(['foo', new \stdClass(), 'value', 'last'], self::getResolver()->getArguments($request, $controller)); } public function testGetNullableArgumentsWithDefaults() { $request = Request::create('/'); $request->attributes->set('last', 'last'); $controller = [new NullableController(), 'action']; $this->assertEquals([null, null, 'value', 'last'], self::getResolver()->getArguments($request, $controller)); } public function testGetSessionArguments() { $session = new Session(new MockArraySessionStorage()); $request = Request::create('/'); $request->setSession($session); $controller = (new ArgumentResolverTestController())->controllerWithSession(...); $this->assertEquals([$session], self::getResolver()->getArguments($request, $controller)); } public function testGetSessionArgumentsWithExtendedSession() { $session = new ExtendingSession(new MockArraySessionStorage()); $request = Request::create('/'); $request->setSession($session); $controller = (new ArgumentResolverTestController())->controllerWithExtendingSession(...); $this->assertEquals([$session], self::getResolver()->getArguments($request, $controller)); } public function testGetSessionArgumentsWithInterface() { $session = new Session(new MockArraySessionStorage()); $request = Request::create('/'); $request->setSession($session); $controller = (new ArgumentResolverTestController())->controllerWithSessionInterface(...); $this->assertEquals([$session], self::getResolver()->getArguments($request, $controller)); } public function testGetSessionMissMatchWithInterface() { $this->expectException(\RuntimeException::class); $session = new Session(new MockArraySessionStorage()); $request = Request::create('/'); $request->setSession($session); $controller = (new ArgumentResolverTestController())->controllerWithExtendingSession(...); self::getResolver()->getArguments($request, $controller); } public function testGetSessionMissMatchWithImplementation() { $this->expectException(\RuntimeException::class); $session = new Session(new MockArraySessionStorage()); $request = Request::create('/'); $request->setSession($session); $controller = (new ArgumentResolverTestController())->controllerWithExtendingSession(...); self::getResolver()->getArguments($request, $controller); } public function testGetSessionMissMatchOnNull() { $this->expectException(\RuntimeException::class); $request = Request::create('/'); $controller = (new ArgumentResolverTestController())->controllerWithExtendingSession(...); self::getResolver()->getArguments($request, $controller); } public function testTargetedResolver() { $resolver = self::getResolver([], [DefaultValueResolver::class => new DefaultValueResolver()]); $request = Request::create('/'); $request->attributes->set('foo', 'bar'); $controller = (new ArgumentResolverTestController())->controllerTargetingResolver(...); $this->assertSame([1], $resolver->getArguments($request, $controller)); } public function testTargetedResolverWithDefaultValue() { $resolver = self::getResolver([], [TestEntityValueResolver::class => new TestEntityValueResolver()]); $request = Request::create('/'); $controller = (new ArgumentResolverTestController())->controllerTargetingResolverWithDefaultValue(...); /** @var Post[] $arguments */ $arguments = $resolver->getArguments($request, $controller); $this->assertCount(1, $arguments); $this->assertSame('Default', $arguments[0]->title); } public function testTargetedResolverWithNullableValue() { $resolver = self::getResolver([], [TestEntityValueResolver::class => new TestEntityValueResolver()]); $request = Request::create('/'); $controller = (new ArgumentResolverTestController())->controllerTargetingResolverWithNullableValue(...); $this->assertSame([null], $resolver->getArguments($request, $controller)); } public function testTargetedResolverWithRequestAttributeValue() { $resolver = self::getResolver([], [TestEntityValueResolver::class => new TestEntityValueResolver()]); $request = Request::create('/'); $request->attributes->set('foo', $object = new Post('Random '.time())); $controller = (new ArgumentResolverTestController())->controllerTargetingResolverWithTestEntity(...); $this->assertSame([$object], $resolver->getArguments($request, $controller)); } public function testDisabledResolver() { $resolver = self::getResolver(namedResolvers: []); $request = Request::create('/'); $request->attributes->set('foo', 'bar'); $controller = (new ArgumentResolverTestController())->controllerDisablingResolver(...); $this->assertSame([1], $resolver->getArguments($request, $controller)); } public function testManyTargetedResolvers() { $resolver = self::getResolver(namedResolvers: []); $request = Request::create('/'); $controller = (new ArgumentResolverTestController())->controllerTargetingManyResolvers(...); $this->expectException(\LogicException::class); $resolver->getArguments($request, $controller); } public function testUnknownTargetedResolver() { $resolver = self::getResolver(namedResolvers: []); $request = Request::create('/'); $controller = (new ArgumentResolverTestController())->controllerTargetingUnknownResolver(...); $this->expectException(ResolverNotFoundException::class); $resolver->getArguments($request, $controller); } public function testResolversChainCompletionWhenResolverThrowsSpecialException() { $failingValueResolver = new class implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): iterable { throw new NearMissValueResolverException('This resolver throws an exception'); } }; // Put failing value resolver in the beginning $expectedToCallValueResolver = $this->createMock(ValueResolverInterface::class); $expectedToCallValueResolver->expects($this->once())->method('resolve')->willReturn([123]); $resolver = self::getResolver([$failingValueResolver, ...ArgumentResolver::getDefaultArgumentValueResolvers(), $expectedToCallValueResolver]); $request = Request::create('/'); $controller = [new ArgumentResolverTestController(), 'controllerWithFoo']; $actualArguments = $resolver->getArguments($request, $controller); self::assertEquals([123], $actualArguments); } public function testExceptionListSingle() { $failingValueResolverOne = new class implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): iterable { throw new NearMissValueResolverException('Some reason why value could not be resolved.'); } }; $resolver = self::getResolver([$failingValueResolverOne]); $request = Request::create('/'); $controller = [new ArgumentResolverTestController(), 'controllerWithFoo']; $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Controller "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolverTestController::controllerWithFoo" requires the "$foo" argument that could not be resolved. Some reason why value could not be resolved.'); $resolver->getArguments($request, $controller); } public function testExceptionListMultiple() { $failingValueResolverOne = new class implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): iterable { throw new NearMissValueResolverException('Some reason why value could not be resolved.'); } }; $failingValueResolverTwo = new class implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): iterable { throw new NearMissValueResolverException('Another reason why value could not be resolved.'); } }; $resolver = self::getResolver([$failingValueResolverOne, $failingValueResolverTwo]); $request = Request::create('/'); $controller = [new ArgumentResolverTestController(), 'controllerWithFoo']; $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Controller "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolverTestController::controllerWithFoo" requires the "$foo" argument that could not be resolved. Possible reasons: 1) Some reason why value could not be resolved. 2) Another reason why value could not be resolved.'); $resolver->getArguments($request, $controller); } } class ArgumentResolverTestController { public function __invoke($foo, $bar = null) { } public function controllerWithFoo($foo) { } public function controllerWithoutArguments() { } public function controllerWithFooAndDefaultBar($foo, $bar = null) { } public function controllerWithFooBarFoobar($foo, $bar, $foobar) { } public function controllerWithRequest(Request $request) { } public function controllerWithExtendingRequest(ExtendingRequest $request) { } public function controllerWithSession(Session $session) { } public function controllerWithSessionInterface(SessionInterface $session) { } public function controllerWithExtendingSession(ExtendingSession $session) { } public function controllerTargetingResolver(#[ValueResolver(DefaultValueResolver::class)] int $foo = 1) { } public function controllerTargetingResolverWithDefaultValue(#[ValueResolver(TestEntityValueResolver::class)] Post $foo = new Post('Default')) { } public function controllerTargetingResolverWithNullableValue(#[ValueResolver(TestEntityValueResolver::class)] ?Post $foo) { } public function controllerTargetingResolverWithTestEntity(#[ValueResolver(TestEntityValueResolver::class)] Post $foo) { } public function controllerDisablingResolver(#[ValueResolver(RequestAttributeValueResolver::class, disabled: true)] int $foo = 1) { } public function controllerTargetingManyResolvers( #[ValueResolver(RequestAttributeValueResolver::class)] #[ValueResolver(DefaultValueResolver::class)] int $foo, ) { } public function controllerTargetingUnknownResolver( #[ValueResolver('foo')] int $bar, ) { } } function controller_function($foo, $foobar) { } class TestEntityValueResolver implements ValueResolverInterface { public function resolve(Request $request, ArgumentMetadata $argument): iterable { return Post::class === $argument->getType() && $request->request->has('title') ? [new Post($request->request->get('title'))] : []; } } class Post { public function __construct( public readonly string $title, ) { } } ================================================ FILE: Tests/Controller/ContainerControllerResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller; use PHPUnit\Framework\Attributes\DataProvider; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ContainerControllerResolver; class ContainerControllerResolverTest extends ControllerResolverTest { public function testGetControllerService() { $service = new ControllerTestService('foo'); $container = new Container(); $container->set('foo', $service); $resolver = $this->createControllerResolver(null, $container); $request = Request::create('/'); $request->attributes->set('_controller', 'foo::action'); $controller = $resolver->getController($request); $this->assertSame($service, $controller[0]); $this->assertSame('action', $controller[1]); } public function testGetControllerInvokableService() { $service = new InvokableControllerService('bar'); $container = new Container(); $container->set('foo', $service); $resolver = $this->createControllerResolver(null, $container); $request = Request::create('/'); $request->attributes->set('_controller', 'foo'); $controller = $resolver->getController($request); $this->assertSame($service, $controller); } public function testGetControllerInvokableServiceWithClassNameAsName() { $service = new InvokableControllerService('bar'); $container = new Container(); $container->set(InvokableControllerService::class, $service); $resolver = $this->createControllerResolver(null, $container); $request = Request::create('/'); $request->attributes->set('_controller', InvokableControllerService::class); $controller = $resolver->getController($request); $this->assertSame($service, $controller); } #[DataProvider('getControllers')] public function testInstantiateControllerWhenControllerStartsWithABackslash($controller) { $service = new ControllerTestService('foo'); $class = ControllerTestService::class; $container = new Container(); $container->set($class, $service); $resolver = $this->createControllerResolver(null, $container); $request = Request::create('/'); $request->attributes->set('_controller', $controller); $controller = $resolver->getController($request); $this->assertInstanceOf(ControllerTestService::class, $controller[0]); $this->assertSame('action', $controller[1]); } public static function getControllers() { return [ ['\\'.ControllerTestService::class.'::action'], ]; } public function testExceptionWhenUsingRemovedControllerServiceWithClassNameAsName() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerTestService" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?'); $container = $this->createMock(Container::class); $container->expects($this->once()) ->method('has') ->with(ControllerTestService::class) ->willReturn(false) ; $container->expects($this->atLeastOnce()) ->method('getRemovedIds') ->with() ->willReturn([ControllerTestService::class => true]) ; $resolver = $this->createControllerResolver(null, $container); $request = Request::create('/'); $request->attributes->set('_controller', [ControllerTestService::class, 'action']); $resolver->getController($request); } public function testExceptionWhenUsingRemovedControllerService() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Controller "app.my_controller" cannot be fetched from the container because it is private. Did you forget to tag the service with "controller.service_arguments"?'); $container = $this->createMock(Container::class); $container->expects($this->once()) ->method('has') ->with('app.my_controller') ->willReturn(false) ; $container->expects($this->atLeastOnce()) ->method('getRemovedIds') ->with() ->willReturn(['app.my_controller' => true]) ; $resolver = $this->createControllerResolver(null, $container); $request = Request::create('/'); $request->attributes->set('_controller', 'app.my_controller'); $resolver->getController($request); } public static function getUndefinedControllers(): array { $tests = parent::getUndefinedControllers(); $tests[0] = ['foo', \InvalidArgumentException::class, 'Controller "foo" does neither exist as service nor as class']; $tests[1] = ['oof::bar', \InvalidArgumentException::class, 'Controller "oof" does neither exist as service nor as class']; $tests[2] = [['oof', 'bar'], \InvalidArgumentException::class, 'Controller "oof" does neither exist as service nor as class']; $tests[] = [ [ControllerTestService::class, 'action'], \InvalidArgumentException::class, 'Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerTestService" has required constructor arguments and does not exist in the container. Did you forget to define the controller as a service?', ]; $tests[] = [ ControllerTestService::class.'::action', \InvalidArgumentException::class, 'Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerTestService" has required constructor arguments and does not exist in the container. Did you forget to define the controller as a service?', ]; $tests[] = [ InvokableControllerService::class, \InvalidArgumentException::class, 'Controller "Symfony\Component\HttpKernel\Tests\Controller\InvokableControllerService" has required constructor arguments and does not exist in the container. Did you forget to define the controller as a service?', ]; return $tests; } protected function createControllerResolver(?LoggerInterface $logger = null, ?ContainerInterface $container = null) { if (!$container) { $container = new Container(); } return new ContainerControllerResolver($container, $logger); } } class InvokableControllerService { public function __construct($bar) // mandatory argument to prevent automatic instantiation { } public function __invoke() { } } class ControllerTestService { public function __construct($foo) { } public function action() { } } ================================================ FILE: Tests/Controller/ControllerResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Controller\ControllerResolver; class ControllerResolverTest extends TestCase { public function testGetControllerWithoutControllerParameter() { $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->once())->method('warning')->with('Unable to look for the controller as the "_controller" parameter is missing.'); $resolver = $this->createControllerResolver($logger); $request = Request::create('/'); $this->assertFalse($resolver->getController($request), '->getController() returns false when the request has no _controller attribute'); } public function testGetControllerWithLambda() { $resolver = $this->createControllerResolver(); $request = Request::create('/'); $request->attributes->set('_controller', $lambda = static function () {}); $controller = $resolver->getController($request); $this->assertSame($lambda, $controller); } public function testGetControllerWithObjectAndInvokeMethod() { $resolver = $this->createControllerResolver(); $object = new InvokableController(); $request = Request::create('/'); $request->attributes->set('_controller', $object); $controller = $resolver->getController($request); $this->assertSame($object, $controller); } public function testGetControllerWithObjectAndMethod() { $resolver = $this->createControllerResolver(); $object = new ControllerTest(); $request = Request::create('/'); $request->attributes->set('_controller', [$object, 'publicAction']); $controller = $resolver->getController($request); $this->assertSame([$object, 'publicAction'], $controller); } public function testGetControllerWithClassAndMethodAsArray() { $resolver = $this->createControllerResolver(); $request = Request::create('/'); $request->attributes->set('_controller', [ControllerTest::class, 'publicAction']); $controller = $resolver->getController($request); $this->assertInstanceOf(ControllerTest::class, $controller[0]); $this->assertSame('publicAction', $controller[1]); } public function testGetControllerWithClassAndMethodAsString() { $resolver = $this->createControllerResolver(); $request = Request::create('/'); $request->attributes->set('_controller', ControllerTest::class.'::publicAction'); $controller = $resolver->getController($request); $this->assertInstanceOf(ControllerTest::class, $controller[0]); $this->assertSame('publicAction', $controller[1]); } public function testGetControllerWithInvokableClass() { $resolver = $this->createControllerResolver(); $request = Request::create('/'); $request->attributes->set('_controller', InvokableController::class); $controller = $resolver->getController($request); $this->assertInstanceOf(InvokableController::class, $controller); } public function testGetControllerOnObjectWithoutInvokeMethod() { $this->expectException(\InvalidArgumentException::class); $resolver = $this->createControllerResolver(); $request = Request::create('/'); $request->attributes->set('_controller', new \stdClass()); $resolver->getController($request); } public function testGetControllerWithFunction() { $resolver = $this->createControllerResolver(); $request = Request::create('/'); $request->attributes->set('_controller', 'Symfony\Component\HttpKernel\Tests\Controller\some_controller_function'); $controller = $resolver->getController($request); $this->assertSame('Symfony\Component\HttpKernel\Tests\Controller\some_controller_function', $controller); } public function testGetControllerWithClosure() { $resolver = $this->createControllerResolver(); $closure = static fn () => 'test'; $request = Request::create('/'); $request->attributes->set('_controller', $closure); $controller = $resolver->getController($request); $this->assertInstanceOf(\Closure::class, $controller); $this->assertSame('test', $controller()); } #[DataProvider('getStaticControllers')] public function testGetControllerWithStaticController($staticController, $returnValue) { $resolver = $this->createControllerResolver(); $request = Request::create('/'); $request->attributes->set('_controller', $staticController); $controller = $resolver->getController($request); $this->assertSame($staticController, $controller); $this->assertSame($returnValue, $controller()); } public static function getStaticControllers() { return [ [TestAbstractController::class.'::staticAction', 'foo'], [[TestAbstractController::class, 'staticAction'], 'foo'], [PrivateConstructorController::class.'::staticAction', 'bar'], [[PrivateConstructorController::class, 'staticAction'], 'bar'], ]; } #[DataProvider('getUndefinedControllers')] public function testGetControllerWithUndefinedController($controller, $exceptionName = null, $exceptionMessage = null) { $resolver = $this->createControllerResolver(); $this->expectException($exceptionName); $this->expectExceptionMessage($exceptionMessage); $request = Request::create('/'); $request->attributes->set('_controller', $controller); $resolver->getController($request); } public static function getUndefinedControllers() { $controller = new ControllerTest(); return [ ['foo', \Error::class, 'Class "foo" not found'], ['oof::bar', \Error::class, 'Class "oof" not found'], [['oof', 'bar'], \Error::class, 'Class "oof" not found'], ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::staticsAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Expected method "staticsAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest", did you mean "staticAction"?'], ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::privateAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Method "privateAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::protectedAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Method "protectedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::undefinedAction', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Expected method "undefinedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest". Available methods: "publicAction", "staticAction"'], ['Symfony\Component\HttpKernel\Tests\Controller\ControllerTest', \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Controller class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" cannot be called without a method name. You need to implement "__invoke" or use one of the available methods: "publicAction", "staticAction".'], [[$controller, 'staticsAction'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Expected method "staticsAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest", did you mean "staticAction"?'], [[$controller, 'privateAction'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Method "privateAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], [[$controller, 'protectedAction'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Method "protectedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'], [[$controller, 'undefinedAction'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Expected method "undefinedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest". Available methods: "publicAction", "staticAction"'], [$controller, \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Controller class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" cannot be called without a method name. You need to implement "__invoke" or use one of the available methods: "publicAction", "staticAction".'], [['a' => 'foo', 'b' => 'bar'], \InvalidArgumentException::class, 'The controller for URI "/" is not callable: Invalid array callable, expected [controller, method].'], ]; } public function testAllowedControllerTypes() { $resolver = $this->createControllerResolver(); $request = Request::create('/'); $controller = new ControllerTest(); $request->attributes->set('_controller', [$controller, 'publicAction']); $request->attributes->set('_check_controller_is_allowed', true); try { $resolver->getController($request); $this->expectException(BadRequestException::class); } catch (BadRequestException) { // expected } $resolver->allowControllers(types: [ControllerTest::class]); $this->assertSame([$controller, 'publicAction'], $resolver->getController($request)); $request->attributes->set('_controller', $action = $controller->publicAction(...)); $this->assertSame($action, $resolver->getController($request)); } public function testAllowedControllerAttributes() { $resolver = $this->createControllerResolver(); $request = Request::create('/'); $controller = some_controller_function(...); $request->attributes->set('_controller', $controller); $request->attributes->set('_check_controller_is_allowed', true); try { $resolver->getController($request); $this->expectException(BadRequestException::class); } catch (BadRequestException) { // expected } $resolver->allowControllers(attributes: [DummyController::class]); $this->assertSame($controller, $resolver->getController($request)); $controller = some_controller_function::class; $request->attributes->set('_controller', $controller); $this->assertSame($controller, $resolver->getController($request)); } public function testAllowedAsControllerAttribute() { $resolver = $this->createControllerResolver(); $request = Request::create('/'); $controller = new InvokableController(); $request->attributes->set('_controller', [$controller, '__invoke']); $request->attributes->set('_check_controller_is_allowed', true); $this->assertSame([$controller, '__invoke'], $resolver->getController($request)); $request->attributes->set('_controller', $controller); $this->assertSame($controller, $resolver->getController($request)); } protected function createControllerResolver(?LoggerInterface $logger = null) { return new ControllerResolver($logger); } } #[DummyController] function some_controller_function($foo, $foobar) { } class ControllerTest { public function __construct() { } public function __toString(): string { return ''; } public function publicAction() { } private function privateAction() { } protected function protectedAction() { } public static function staticAction() { } } #[AsController] class InvokableController { public function __invoke($foo, $bar = null) { } } abstract class TestAbstractController { public static function staticAction() { return 'foo'; } } class PrivateConstructorController { private function __construct() { } public static function staticAction() { return 'bar'; } } ================================================ FILE: Tests/Controller/ErrorControllerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Controller; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ErrorController; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; class ErrorControllerTest extends TestCase { #[DataProvider('getInvokeControllerDataProvider')] public function testInvokeController(Request $request, \Exception $exception, int $statusCode, string $content) { $kernel = $this->createStub(HttpKernelInterface::class); $errorRenderer = new HtmlErrorRenderer(); $controller = new ErrorController($kernel, null, $errorRenderer); $response = $controller($exception); $this->assertSame($statusCode, $response->getStatusCode()); self::assertStringContainsString($content, strtr($response->getContent(), ["\n" => '', ' ' => ''])); } public static function getInvokeControllerDataProvider() { yield 'default status code and HTML format' => [ new Request(), new \Exception(), 500, 'The server returned a "500 Internal Server Error".', ]; yield 'custom status code' => [ new Request(), new NotFoundHttpException('Page not found.'), 404, 'The server returned a "404 Not Found".', ]; $request = new Request(); $request->attributes->set('_format', 'unknown'); yield 'default HTML format for unknown formats' => [ $request, new HttpException(405, 'Invalid request.'), 405, 'The server returned a "405 Method Not Allowed".', ]; } public function testPreviewController() { $_controller = 'error_controller'; $code = 404; $kernel = $this->createMock(HttpKernelInterface::class); $kernel ->expects($this->once()) ->method('handle') ->with( $this->callback(function (Request $request) use ($_controller, $code) { $exception = $request->attributes->get('exception'); $this->assertSame($_controller, $request->attributes->get('_controller')); $this->assertInstanceOf(\Throwable::class, $exception); $this->assertSame($code, $exception->getStatusCode()); $this->assertFalse($request->attributes->get('showException')); return true; }), $this->equalTo(HttpKernelInterface::SUB_REQUEST) ) ->willReturn($response = new Response()); $controller = new ErrorController($kernel, $_controller, new HtmlErrorRenderer()); $this->assertSame($response, $controller->preview(new Request(), $code)); } } ================================================ FILE: Tests/Controller/TraceableArgumentResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Controller; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\TraceableArgumentResolver; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\Stopwatch\StopwatchEvent; class TraceableArgumentResolverTest extends TestCase { public function testStopwatchEventIsStoppedWhenResolverThrows() { $stopwatchEvent = $this->createMock(StopwatchEvent::class); $stopwatchEvent->expects(self::once())->method('stop'); $stopwatch = $this->createStub(Stopwatch::class); $stopwatch->method('start')->willReturn($stopwatchEvent); $resolver = new class implements ArgumentResolverInterface { public function getArguments(Request $request, callable $controller, ?\ReflectionFunctionAbstract $reflector = null): array { throw new \Exception(); } }; $traceableResolver = new TraceableArgumentResolver($resolver, $stopwatch); try { $traceableResolver->getArguments(new Request(), static function () {}); } catch (\Exception $ex) { } } } ================================================ FILE: Tests/Controller/TraceableControllerResolverTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Controller; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Controller\TraceableControllerResolver; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\Stopwatch\StopwatchEvent; class TraceableControllerResolverTest extends TestCase { public function testStopwatchEventIsStoppedWhenResolverThrows() { $stopwatchEvent = $this->createMock(StopwatchEvent::class); $stopwatchEvent->expects(self::once())->method('stop'); $stopwatch = $this->createStub(Stopwatch::class); $stopwatch->method('start')->willReturn($stopwatchEvent); $resolver = new class implements ControllerResolverInterface { public function getController(Request $request): callable|false { throw new \Exception(); } }; $traceableResolver = new TraceableControllerResolver($resolver, $stopwatch); try { $traceableResolver->getController(new Request()); } catch (\Exception $ex) { } } } ================================================ FILE: Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\ControllerMetadata; use Fake\ImportedAndFake; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController; class ArgumentMetadataFactoryTest extends TestCase { private ArgumentMetadataFactory $factory; protected function setUp(): void { $this->factory = new ArgumentMetadataFactory(); } public function testSignature1() { $arguments = $this->factory->createArgumentMetadata([$this, 'signature1']); $this->assertEquals([ new ArgumentMetadata('foo', self::class, false, false, null, controllerName: $this::class.'::signature1'), new ArgumentMetadata('bar', 'array', false, false, null, controllerName: $this::class.'::signature1'), new ArgumentMetadata('baz', 'callable', false, false, null, controllerName: $this::class.'::signature1'), ], $arguments); } public function testSignature2() { $arguments = $this->factory->createArgumentMetadata($this->signature2(...)); $this->assertEquals([ new ArgumentMetadata('foo', self::class, false, true, null, true, controllerName: $this::class.'::signature2'), new ArgumentMetadata('bar', FakeClassThatDoesNotExist::class, false, true, null, true, controllerName: $this::class.'::signature2'), new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, true, null, true, controllerName: $this::class.'::signature2'), ], $arguments); } public function testSignature3() { $arguments = $this->factory->createArgumentMetadata($this->signature3(...)); $this->assertEquals([ new ArgumentMetadata('bar', FakeClassThatDoesNotExist::class, false, false, null, controllerName: $this::class.'::signature3'), new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, false, null, controllerName: $this::class.'::signature3'), ], $arguments); } public function testSignature4() { $arguments = $this->factory->createArgumentMetadata($this->signature4(...)); $this->assertEquals([ new ArgumentMetadata('foo', null, false, true, 'default', controllerName: $this::class.'::signature4'), new ArgumentMetadata('bar', null, false, true, 500, controllerName: $this::class.'::signature4'), new ArgumentMetadata('baz', null, false, true, [], controllerName: $this::class.'::signature4'), ], $arguments); } public function testSignature5() { $arguments = $this->factory->createArgumentMetadata($this->signature5(...)); $this->assertEquals([ new ArgumentMetadata('foo', 'array', false, true, null, true, controllerName: $this::class.'::signature5'), new ArgumentMetadata('bar', null, false, true, null, true, controllerName: $this::class.'::signature5'), ], $arguments); } public function testVariadicSignature() { $arguments = $this->factory->createArgumentMetadata([new VariadicController(), 'action']); $this->assertEquals([ new ArgumentMetadata('foo', null, false, false, null, controllerName: VariadicController::class.'::action'), new ArgumentMetadata('bar', null, true, false, null, controllerName: VariadicController::class.'::action'), ], $arguments); } public function testBasicTypesSignature() { $arguments = $this->factory->createArgumentMetadata([new BasicTypesController(), 'action']); $this->assertEquals([ new ArgumentMetadata('foo', 'string', false, false, null, controllerName: BasicTypesController::class.'::action'), new ArgumentMetadata('bar', 'int', false, false, null, controllerName: BasicTypesController::class.'::action'), new ArgumentMetadata('baz', 'float', false, false, null, controllerName: BasicTypesController::class.'::action'), ], $arguments); } public function testNamedClosure() { $arguments = $this->factory->createArgumentMetadata($this->signature1(...)); $this->assertEquals([ new ArgumentMetadata('foo', self::class, false, false, null, controllerName: $this::class.'::signature1'), new ArgumentMetadata('bar', 'array', false, false, null, controllerName: $this::class.'::signature1'), new ArgumentMetadata('baz', 'callable', false, false, null, controllerName: $this::class.'::signature1'), ], $arguments); } public function testNullableTypesSignature() { $arguments = $this->factory->createArgumentMetadata([new NullableController(), 'action']); $this->assertEquals([ new ArgumentMetadata('foo', 'string', false, false, null, true, controllerName: NullableController::class.'::action'), new ArgumentMetadata('bar', \stdClass::class, false, false, null, true, controllerName: NullableController::class.'::action'), new ArgumentMetadata('baz', 'string', false, true, 'value', true, controllerName: NullableController::class.'::action'), new ArgumentMetadata('last', 'string', false, true, '', false, controllerName: NullableController::class.'::action'), ], $arguments); } public function testAttributeSignature() { $arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'action']); $this->assertEquals([ new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')], controllerName: AttributeController::class.'::action'), ], $arguments); } public function testMultipleAttributes() { $this->factory->createArgumentMetadata([new AttributeController(), 'multiAttributeArg']); $this->assertCount(1, $this->factory->createArgumentMetadata([new AttributeController(), 'multiAttributeArg'])[0]->getAttributes()); } public function testIssue41478() { $arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'issue41478']); $this->assertEquals([ new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')], controllerName: AttributeController::class.'::issue41478'), new ArgumentMetadata('bat', 'string', false, false, null, false, [], controllerName: AttributeController::class.'::issue41478'), ], $arguments); } public function signature1(self $foo, array $bar, callable $baz) { } public function signature2(?self $foo = null, ?FakeClassThatDoesNotExist $bar = null, ?ImportedAndFake $baz = null) { } public function signature3(FakeClassThatDoesNotExist $bar, ImportedAndFake $baz) { } public function signature4($foo = 'default', $bar = 500, $baz = []) { } public function signature5(?array $foo = null, $bar = null) { } } ================================================ FILE: Tests/ControllerMetadata/ArgumentMetadataTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\ControllerMetadata; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo; class ArgumentMetadataTest extends TestCase { public function testWithBcLayerWithDefault() { $argument = new ArgumentMetadata('foo', 'string', false, true, 'default value'); $this->assertFalse($argument->isNullable()); } public function testDefaultValueAvailable() { $argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true); $this->assertTrue($argument->isNullable()); $this->assertTrue($argument->hasDefaultValue()); $this->assertSame('default value', $argument->getDefaultValue()); } public function testDefaultValueUnavailable() { $this->expectException(\LogicException::class); $argument = new ArgumentMetadata('foo', 'string', false, false, null, false); $this->assertFalse($argument->isNullable()); $this->assertFalse($argument->hasDefaultValue()); $argument->getDefaultValue(); } public function testGetAttributes() { $argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, [new Foo('bar')]); $this->assertEquals([new Foo('bar')], $argument->getAttributes()); } public function testGetAttributesOfType() { $argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, [new Foo('bar')]); $this->assertEquals([new Foo('bar')], $argument->getAttributesOfType(Foo::class)); } } ================================================ FILE: Tests/DataCollector/Compiler.log ================================================ Symfony\Component\DependencyInjection\Compiler\RemovePrivateAliasesPass: Removed service "Psr\Container\ContainerInterface"; reason: private alias. Symfony\Component\DependencyInjection\Compiler\RemovePrivateAliasesPass: Removed service "Symfony\Component\DependencyInjection\ContainerInterface"; reason: private alias. Some custom logging message With ending : ================================================ FILE: Tests/DataCollector/ConfigDataCollectorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\ConfigDataCollector; use Symfony\Component\HttpKernel\Kernel; class ConfigDataCollectorTest extends TestCase { public function testCollect() { $kernel = new KernelForTest('test', true); $c = new ConfigDataCollector(); $c->setKernel($kernel); $c->collect(new Request(), new Response()); $this->assertSame('test', $c->getEnv()); $this->assertTrue($c->isDebug()); $this->assertSame('config', $c->getName()); $this->assertMatchesRegularExpression('~^'.preg_quote($c->getPhpVersion(), '~').'~', \PHP_VERSION); $this->assertMatchesRegularExpression('~'.preg_quote((string) $c->getPhpVersionExtra(), '~').'$~', \PHP_VERSION); $this->assertSame(\PHP_INT_SIZE * 8, $c->getPhpArchitecture()); $this->assertSame(class_exists(\Locale::class, false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a', $c->getPhpIntlLocale()); $this->assertSame(date_default_timezone_get(), $c->getPhpTimezone()); $this->assertSame(Kernel::VERSION, $c->getSymfonyVersion()); $this->assertSame(4 === Kernel::MINOR_VERSION, $c->isSymfonyLts()); $this->assertNull($c->getToken()); $this->assertSame(\extension_loaded('xdebug'), $c->hasXDebug()); $this->assertSame(\extension_loaded('Zend OPcache') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL), $c->hasZendOpcache()); $this->assertSame(\extension_loaded('apcu') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL), $c->hasApcu()); $this->assertSame(\sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), $c->getSymfonyMinorVersion()); $this->assertContains($c->getSymfonyState(), ['eol', 'eom', 'dev', 'stable']); $eom = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE)->format('F Y'); $eol = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE)->format('F Y'); $this->assertSame($eom, $c->getSymfonyEom()); $this->assertSame($eol, $c->getSymfonyEol()); } public function testCollectWithoutKernel() { $c = new ConfigDataCollector(); $c->collect(new Request(), new Response()); $this->assertSame('n/a', $c->getEnv()); $this->assertSame('n/a', $c->isDebug()); $this->assertSame('config', $c->getName()); $this->assertMatchesRegularExpression('~^'.preg_quote($c->getPhpVersion(), '~').'~', \PHP_VERSION); $this->assertMatchesRegularExpression('~'.preg_quote((string) $c->getPhpVersionExtra(), '~').'$~', \PHP_VERSION); $this->assertSame(\PHP_INT_SIZE * 8, $c->getPhpArchitecture()); $this->assertSame(class_exists(\Locale::class, false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a', $c->getPhpIntlLocale()); $this->assertSame(date_default_timezone_get(), $c->getPhpTimezone()); $this->assertSame(Kernel::VERSION, $c->getSymfonyVersion()); $this->assertSame(4 === Kernel::MINOR_VERSION, $c->isSymfonyLts()); $this->assertNull($c->getToken()); $this->assertSame(\extension_loaded('xdebug'), $c->hasXDebug()); $this->assertSame(\extension_loaded('Zend OPcache') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL), $c->hasZendOpcache()); $this->assertSame(\extension_loaded('apcu') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL), $c->hasApcu()); $this->assertSame(\sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), $c->getSymfonyMinorVersion()); $this->assertContains($c->getSymfonyState(), ['eol', 'eom', 'dev', 'stable']); $eom = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE)->format('F Y'); $eol = \DateTimeImmutable::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE)->format('F Y'); $this->assertSame($eom, $c->getSymfonyEom()); $this->assertSame($eol, $c->getSymfonyEol()); } } class KernelForTest extends Kernel { public function registerBundles(): iterable { } public function getBundles(): array { return []; } public function registerContainerConfiguration(LoaderInterface $loader): void { } public function getProjectDir(): string { return __DIR__.'/../Fixtures'; } } ================================================ FILE: Tests/DataCollector/DataCollectorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Tests\Fixtures\DataCollector\CloneVarDataCollector; use Symfony\Component\HttpKernel\Tests\Fixtures\UsePropertyInDestruct; use Symfony\Component\HttpKernel\Tests\Fixtures\WithPublicObjectProperty; use Symfony\Component\VarDumper\Cloner\VarCloner; class DataCollectorTest extends TestCase { public function testCloneVarStringWithScheme() { $c = new CloneVarDataCollector('scheme://foo'); $c->collect(new Request(), new Response()); $cloner = new VarCloner(); $this->assertEquals($cloner->cloneVar('scheme://foo'), $c->getData()); } public function testCloneVarExistingFilePath() { $c = new CloneVarDataCollector([$filePath = tempnam(sys_get_temp_dir(), 'clone_var_data_collector_')]); $c->collect(new Request(), new Response()); $this->assertSame($filePath, $c->getData()[0]); } public function testClassPublicObjectProperty() { $parent = new WithPublicObjectProperty(); $child = new WithPublicObjectProperty(); $child->parent = $parent; $c = new CloneVarDataCollector($child); $c->collect(new Request(), new Response()); $this->assertNotNull($c->getData()->parent); } public function testClassPublicObjectPropertyAsReference() { $parent = new WithPublicObjectProperty(); $child = new WithPublicObjectProperty(); $child->parent = &$parent; $c = new CloneVarDataCollector($child); $c->collect(new Request(), new Response()); $this->assertNotNull($c->getData()->parent); } public function testClassUsePropertyInDestruct() { $parent = new UsePropertyInDestruct(); $child = new UsePropertyInDestruct(); $child->parent = $parent; $c = new CloneVarDataCollector($child); $c->collect(new Request(), new Response()); $this->assertNotNull($c->getData()->parent); } public function testClassUsePropertyAsReferenceInDestruct() { $parent = new UsePropertyInDestruct(); $child = new UsePropertyInDestruct(); $child->parent = &$parent; $c = new CloneVarDataCollector($child); $c->collect(new Request(), new Response()); $this->assertNotNull($c->getData()->parent); } } ================================================ FILE: Tests/DataCollector/DumpDataCollectorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use PHPUnit\Framework\TestCase; use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\Server\Connection; /** * @author Nicolas Grekas */ class DumpDataCollectorTest extends TestCase { public function testDump() { $data = new Data([[123]]); $data = $data->withContext(['label' => 'foo']); $collector = new DumpDataCollector(null, new FileLinkFormatter([])); $this->assertSame('dump', $collector->getName()); $collector->dump($data); $line = __LINE__ - 1; $this->assertSame(1, $collector->getDumpsCount()); $dump = $collector->getDumps('html'); $this->assertArrayHasKey('data', $dump[0]); $dump[0]['data'] = preg_replace('/^.*?
 "
123\n
\n", 'name' => 'DumpDataCollectorTest.php', 'file' => __FILE__, 'line' => $line, 'fileExcerpt' => false, 'label' => 'foo', ], ]; $this->assertEquals($xDump, $dump); $this->assertStringMatchesFormat('%a;a:%d:{i:0;a:6:{s:4:"data";%c:39:"Symfony\Component\VarDumper\Cloner\Data":%a', serialize($collector)); $this->assertSame(0, $collector->getDumpsCount()); $serialized = serialize($collector); $this->assertSame("O:60:\"Symfony\Component\HttpKernel\DataCollector\DumpDataCollector\":1:{s:4:\"data\";a:2:{i:0;b:0;i:1;s:5:\"UTF-8\";}}", $serialized); $this->assertInstanceOf(DumpDataCollector::class, unserialize($serialized)); } public function testDumpWithServerConnection() { $data = new Data([[123]]); // Server is up, server dumper is used $serverDumper = $this->createMock(Connection::class); $serverDumper->expects($this->once())->method('write')->willReturn(true); $collector = new DumpDataCollector(null, null, null, null, $serverDumper); $collector->dump($data); // Collect doesn't re-trigger dump ob_start(); $collector->collect(new Request(), new Response()); $this->assertSame('', ob_get_clean()); $this->assertStringMatchesFormat('%a;a:%d:{i:0;a:6:{s:4:"data";%c:39:"Symfony\Component\VarDumper\Cloner\Data":%a', serialize($collector)); } public function testCollectDefault() { $data = new Data([[123]]); $collector = new DumpDataCollector(); $collector->dump($data); $line = __LINE__ - 1; ob_start(); $collector->collect(new Request(), new Response()); $output = preg_replace("/\033\[[^m]*m/", '', ob_get_clean()); $this->assertSame("DumpDataCollectorTest.php on line {$line}:\n123\n", $output); $this->assertSame(1, $collector->getDumpsCount()); serialize($collector); } public function testCollectHtml() { $data = new Data([[123]]); $collector = new DumpDataCollector(null, 'test://%f:%l'); $collector->dump($data); $line = __LINE__ - 1; $file = __FILE__; $xOutput = <<DumpDataCollectorTest.php on line {$line}: 123
EOTXT; ob_start(); $response = new Response(); $response->headers->set('Content-Type', 'text/html'); $collector->collect(new Request(), $response); $output = ob_get_clean(); $output = preg_replace('#<(script|style).*?#s', '', $output); $output = preg_replace('/sf-dump-\d+/', 'sf-dump', $output); $this->assertSame($xOutput, trim($output)); $this->assertSame(1, $collector->getDumpsCount()); serialize($collector); } public function testFlush() { $data = new Data([[456]]); $collector = new DumpDataCollector(); $collector->dump($data); $line = __LINE__ - 1; ob_start(); $collector->__destruct(); $output = preg_replace("/\033\[[^m]*m/", '', ob_get_clean()); $this->assertSame("DumpDataCollectorTest.php on line {$line}:\n456\n", $output); } public function testFlushNothingWhenDataDumperIsProvided() { $data = new Data([[456]]); $dumper = new CliDumper('php://output'); $collector = new DumpDataCollector(null, null, null, null, $dumper); ob_start(); $collector->dump($data); $line = __LINE__ - 1; $output = preg_replace("/\033\[[^m]*m/", '', ob_get_clean()); $this->assertSame("DumpDataCollectorTest.php on line {$line}:\n456\n", $output); ob_start(); $collector->__destruct(); $this->assertSame('', ob_get_clean()); } public function testNullContentTypeWithNoDebugEnv() { $request = new Request(); $requestStack = new RequestStack(); $requestStack->push($request); $response = new Response(''); $response->headers->set('Content-Type', null); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $collector = new DumpDataCollector(null, null, null, $requestStack); $collector->collect($request, $response); ob_start(); $collector->__destruct(); $this->assertSame('', ob_get_clean()); } } ================================================ FILE: Tests/DataCollector/ExceptionDataCollectorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use PHPUnit\Framework\TestCase; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector; class ExceptionDataCollectorTest extends TestCase { public function testCollect() { $e = new \Exception('foo', 500); $c = new ExceptionDataCollector(); $flattened = FlattenException::createWithDataRepresentation($e); $trace = $flattened->getTrace(); $this->assertFalse($c->hasException()); $c->collect(new Request(), new Response(), $e); $this->assertTrue($c->hasException()); $this->assertEquals($flattened, $c->getException()); $this->assertSame('foo', $c->getMessage()); $this->assertSame(500, $c->getCode()); $this->assertSame('exception', $c->getName()); $this->assertSame($trace, $c->getTrace()); $c->collect(new Request(), new Response(), new class extends \Exception { protected $code = 'non-integer-code'; }); $this->assertSame('non-integer-code', $c->getCode()); } public function testCollectWithoutException() { $c = new ExceptionDataCollector(); $c->collect(new Request(), new Response()); $this->assertFalse($c->hasException()); } public function testReset() { $c = new ExceptionDataCollector(); $c->collect(new Request(), new Response(), new \Exception()); $c->reset(); $c->collect(new Request(), new Response()); $this->assertFalse($c->hasException()); } } ================================================ FILE: Tests/DataCollector/LoggerDataCollectorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; use Symfony\Component\VarDumper\Cloner\Data; class LoggerDataCollectorTest extends TestCase { public function testCollectWithUnexpectedFormat() { $logger = $this ->getMockBuilder(DebugLoggerInterface::class) ->onlyMethods(['countErrors', 'getLogs', 'clear']) ->getMock(); $logger->expects($this->once())->method('countErrors')->willReturn(123); $logger->expects($this->exactly(2))->method('getLogs')->willReturn([]); $c = new LoggerDataCollector($logger, __DIR__.'/'); $c->lateCollect(); $compilerLogs = $c->getCompilerLogs()->getValue(true); $this->assertSame([ ['message' => 'Removed service "Psr\Container\ContainerInterface"; reason: private alias.'], ['message' => 'Removed service "Symfony\Component\DependencyInjection\ContainerInterface"; reason: private alias.'], ], $compilerLogs['Symfony\Component\DependencyInjection\Compiler\RemovePrivateAliasesPass']); $this->assertSame([ ['message' => 'Some custom logging message'], ['message' => 'With ending :'], ], $compilerLogs['Unknown Compiler Pass']); } public function testCollectFromDeprecationsLog() { $containerPathPrefix = __DIR__.'/'; $path = $containerPathPrefix.'Deprecations.log'; touch($path); file_put_contents($path, serialize([[ 'type' => 16384, 'message' => 'The "Symfony\Bundle\FrameworkBundle\Controller\Controller" class is deprecated since Symfony 4.2, use Symfony\Bundle\FrameworkBundle\Controller\AbstractController instead.', 'file' => '/home/hamza/project/contrib/sf/vendor/symfony/framework-bundle/Controller/Controller.php', 'line' => 17, 'trace' => [[ 'file' => '/home/hamza/project/contrib/sf/src/Controller/DefaultController.php', 'line' => 9, 'function' => 'spl_autoload_call', ]], 'count' => 1, ]])); $logger = $this ->getMockBuilder(DebugLoggerInterface::class) ->onlyMethods(['countErrors', 'getLogs', 'clear']) ->getMock(); $logger->expects($this->once())->method('countErrors')->willReturn(0); $logger->expects($this->exactly(2))->method('getLogs')->willReturn([]); $c = new LoggerDataCollector($logger, $containerPathPrefix); $c->lateCollect(); $processedLogs = $c->getProcessedLogs(); $this->assertCount(1, $processedLogs); $this->assertSame('deprecation', $processedLogs[0]['type']); $this->assertSame(1, $processedLogs[0]['errorCount']); $this->assertSame($processedLogs[0]['timestamp'], (new \DateTimeImmutable())->setTimestamp(filemtime($path))->format(\DateTimeInterface::RFC3339_EXTENDED)); $this->assertSame(100, $processedLogs[0]['priority']); $this->assertSame('DEBUG', $processedLogs[0]['priorityName']); $this->assertNull($processedLogs[0]['channel']); $this->assertInstanceOf(Data::class, $processedLogs[0]['message']); $this->assertInstanceOf(Data::class, $processedLogs[0]['context']); @unlink($path); } public function testWithMainRequest() { $mainRequest = new Request(); $stack = new RequestStack(); $stack->push($mainRequest); $logger = $this ->getMockBuilder(DebugLoggerInterface::class) ->onlyMethods(['countErrors', 'getLogs', 'clear']) ->getMock(); $logger->expects($this->once())->method('countErrors')->with(null); $logger->expects($this->exactly(2))->method('getLogs')->with(null)->willReturn([]); $c = new LoggerDataCollector($logger, __DIR__.'/', $stack); $c->collect($mainRequest, new Response()); $c->lateCollect(); } public function testWithSubRequest() { $mainRequest = new Request(); $subRequest = new Request(); $stack = new RequestStack(); $stack->push($mainRequest); $stack->push($subRequest); $logger = $this ->getMockBuilder(DebugLoggerInterface::class) ->onlyMethods(['countErrors', 'getLogs', 'clear']) ->getMock(); $logger->expects($this->once())->method('countErrors')->with($subRequest); $logger->expects($this->exactly(2))->method('getLogs')->with($subRequest)->willReturn([]); $c = new LoggerDataCollector($logger, __DIR__.'/', $stack); $c->collect($subRequest, new Response()); $c->lateCollect(); } #[DataProvider('getCollectTestData')] public function testCollect($nb, $logs, $expectedLogs, $expectedDeprecationCount, $expectedScreamCount, $expectedPriorities = null) { $logger = $this ->getMockBuilder(DebugLoggerInterface::class) ->onlyMethods(['countErrors', 'getLogs', 'clear']) ->getMock(); $logger->expects($this->once())->method('countErrors')->willReturn($nb); $logger->expects($this->exactly(2))->method('getLogs')->willReturn($logs); $c = new LoggerDataCollector($logger); $c->lateCollect(); $this->assertEquals('logger', $c->getName()); $this->assertEquals($nb, $c->countErrors()); $logs = array_map(static function ($v) { if (isset($v['context']['exception'])) { $e = &$v['context']['exception']; $e = isset($e["\0*\0message"]) ? [$e["\0*\0message"], $e["\0*\0severity"]] : [$e["\0Symfony\Component\ErrorHandler\Exception\SilencedErrorContext\0severity"]]; } return $v; }, $c->getLogs()->getValue(true)); $this->assertEquals($expectedLogs, $logs); $this->assertEquals($expectedDeprecationCount, $c->countDeprecations()); $this->assertEquals($expectedScreamCount, $c->countScreams()); if (isset($expectedPriorities)) { $this->assertSame($expectedPriorities, $c->getPriorities()->getValue(true)); } } public static function getCollectTestData() { yield 'simple log' => [ 1, [['message' => 'foo', 'context' => [], 'priority' => 100, 'priorityName' => 'DEBUG']], [['message' => 'foo', 'context' => [], 'priority' => 100, 'priorityName' => 'DEBUG']], 0, 0, ]; yield 'log with a context' => [ 1, [['message' => 'foo', 'context' => ['foo' => 'bar'], 'priority' => 100, 'priorityName' => 'DEBUG']], [['message' => 'foo', 'context' => ['foo' => 'bar'], 'priority' => 100, 'priorityName' => 'DEBUG']], 0, 0, ]; yield 'logs with some deprecations' => [ 1, [ ['message' => 'foo3', 'context' => ['exception' => new \ErrorException('warning', 0, \E_USER_WARNING)], 'priority' => 100, 'priorityName' => 'DEBUG'], ['message' => 'foo', 'context' => ['exception' => new \ErrorException('deprecated', 0, \E_DEPRECATED)], 'priority' => 100, 'priorityName' => 'DEBUG'], ['message' => 'foo2', 'context' => ['exception' => new \ErrorException('deprecated', 0, \E_USER_DEPRECATED)], 'priority' => 100, 'priorityName' => 'DEBUG'], ], [ ['message' => 'foo3', 'context' => ['exception' => ['warning', \E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG'], ['message' => 'foo', 'context' => ['exception' => ['deprecated', \E_DEPRECATED]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => false], ['message' => 'foo2', 'context' => ['exception' => ['deprecated', \E_USER_DEPRECATED]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => false], ], 2, 0, [100 => ['count' => 3, 'name' => 'DEBUG']], ]; yield 'logs with some silent errors' => [ 1, [ ['message' => 'foo3', 'context' => ['exception' => new \ErrorException('warning', 0, \E_USER_WARNING)], 'priority' => 100, 'priorityName' => 'DEBUG'], ['message' => 'foo3', 'context' => ['exception' => new SilencedErrorContext(\E_USER_WARNING, __FILE__, __LINE__)], 'priority' => 100, 'priorityName' => 'DEBUG'], ['message' => '0', 'context' => ['exception' => new SilencedErrorContext(\E_USER_WARNING, __FILE__, __LINE__)], 'priority' => 100, 'priorityName' => 'DEBUG'], ], [ ['message' => 'foo3', 'context' => ['exception' => ['warning', \E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG'], ['message' => 'foo3', 'context' => ['exception' => [\E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => true], ['message' => '0', 'context' => ['exception' => [\E_USER_WARNING]], 'priority' => 100, 'priorityName' => 'DEBUG', 'errorCount' => 1, 'scream' => true], ], 0, 2, ]; } } ================================================ FILE: Tests/DataCollector/MemoryDataCollectorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\MemoryDataCollector; class MemoryDataCollectorTest extends TestCase { public function testCollect() { $collector = new MemoryDataCollector(); $collector->collect(new Request(), new Response()); $this->assertIsInt($collector->getMemory()); $this->assertIsInt($collector->getMemoryLimit()); $this->assertSame('memory', $collector->getName()); } #[DataProvider('getBytesConversionTestData')] public function testBytesConversion($limit, $bytes) { $collector = new MemoryDataCollector(); $method = new \ReflectionMethod($collector, 'convertToBytes'); $this->assertEquals($bytes, $method->invoke($collector, $limit)); } public static function getBytesConversionTestData() { return [ ['2k', 2048], ['2 k', 2048], ['8m', 8 * 1024 * 1024], ['+2 k', 2048], ['+2???k', 2048], ['0x10', 16], ['0xf', 15], ['010', 8], ['+0x10 k', 16 * 1024], ['1g', 1024 * 1024 * 1024], ['1G', 1024 * 1024 * 1024], ['-1', -1], ['0', 0], ['2mk', 2048], // the unit must be the last char, so in this case 'k', not 'm' ]; } } ================================================ FILE: Tests/DataCollector/RequestDataCollectorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Tests\Fixtures\DataCollector\DummyController; class RequestDataCollectorTest extends TestCase { public function testCollect() { $c = new RequestDataCollector(); $c->collect($request = $this->createRequest(), $this->createResponse()); $c->lateCollect(); $attributes = $c->getRequestAttributes(); $this->assertSame('request', $c->getName()); $this->assertInstanceOf(ParameterBag::class, $c->getRequestHeaders()); $this->assertInstanceOf(ParameterBag::class, $c->getRequestServer()); $this->assertInstanceOf(ParameterBag::class, $c->getRequestCookies()); $this->assertInstanceOf(ParameterBag::class, $attributes); $this->assertInstanceOf(ParameterBag::class, $c->getRequestRequest()); $this->assertInstanceOf(ParameterBag::class, $c->getRequestQuery()); $this->assertInstanceOf(ParameterBag::class, $c->getResponseCookies()); $this->assertSame('html', $c->getFormat()); $this->assertEquals('foobar', $c->getRoute()); $this->assertEquals(['name' => 'foo'], $c->getRouteParams()); $this->assertSame([], $c->getSessionAttributes()); $this->assertSame('en', $c->getLocale()); $this->assertContainsEquals(__FILE__, $attributes->get('resource')); $this->assertSame('stdClass', $attributes->get('object')->getType()); $this->assertInstanceOf(ParameterBag::class, $c->getResponseHeaders()); $this->assertSame('OK', $c->getStatusText()); $this->assertSame(200, $c->getStatusCode()); $this->assertSame('application/json', $c->getContentType()); } public function testCollectWithoutRouteParams() { $request = $this->createRequest([]); $c = new RequestDataCollector(); $c->collect($request, $this->createResponse()); $c->lateCollect(); $this->assertEquals([], $c->getRouteParams()); } #[DataProvider('provideControllerCallables')] public function testControllerInspection($name, $callable, $expected) { $c = new RequestDataCollector(); $request = $this->createRequest(); $response = $this->createResponse(); $this->injectController($c, $callable, $request); $c->collect($request, $response); $c->lateCollect(); $this->assertSame($expected, $c->getController()->getValue(true), \sprintf('Testing: %s', $name)); } public static function provideControllerCallables(): array { // make sure we always match the line number $controller = new DummyController(); $r1 = new \ReflectionMethod($controller, 'regularCallable'); $r2 = new \ReflectionMethod($controller, 'staticControllerMethod'); $r3 = new \ReflectionClass($controller); // test name, callable, expected return [ [ '"Regular" callable', [$controller, 'regularCallable'], [ 'class' => DummyController::class, 'method' => 'regularCallable', 'file' => $r1->getFileName(), 'line' => $r1->getStartLine(), ], ], [ 'Closure', static fn () => 'foo', [ 'class' => \sprintf('{closure:%s():%d}', __METHOD__, __LINE__ - 2), 'method' => null, 'file' => __FILE__, 'line' => __LINE__ - 5, ], ], [ 'First-class callable closure', $controller->regularCallable(...), [ 'class' => DummyController::class, 'method' => 'regularCallable', 'file' => $r1->getFileName(), 'line' => $r1->getStartLine(), ], ], [ 'Static callback as string', DummyController::class.'::staticControllerMethod', [ 'class' => DummyController::class, 'method' => 'staticControllerMethod', 'file' => $r2->getFileName(), 'line' => $r2->getStartLine(), ], ], [ 'Static callable with instance', [$controller, 'staticControllerMethod'], [ 'class' => DummyController::class, 'method' => 'staticControllerMethod', 'file' => $r2->getFileName(), 'line' => $r2->getStartLine(), ], ], [ 'Static callable with class name', [DummyController::class, 'staticControllerMethod'], [ 'class' => DummyController::class, 'method' => 'staticControllerMethod', 'file' => $r2->getFileName(), 'line' => $r2->getStartLine(), ], ], [ 'Callable with instance depending on __call()', [$controller, 'magicMethod'], [ 'class' => DummyController::class, 'method' => 'magicMethod', 'file' => 'n/a', 'line' => 'n/a', ], ], [ 'Callable with class name depending on __callStatic()', [DummyController::class, 'magicMethod'], [ 'class' => DummyController::class, 'method' => 'magicMethod', 'file' => 'n/a', 'line' => 'n/a', ], ], [ 'Invokable controller', $controller, [ 'class' => DummyController::class, 'method' => null, 'file' => $r3->getFileName(), 'line' => $r3->getStartLine(), ], ], ]; } public function testItIgnoresInvalidCallables() { $request = $this->createRequestWithSession(); $response = new RedirectResponse('/'); $c = new RequestDataCollector(); $c->collect($request, $response); $this->assertSame('n/a', $c->getController()); } public function testItAddsRedirectedAttributesWhenRequestContainsSpecificCookie() { $request = $this->createRequest(); $request->cookies->add([ 'sf_redirect' => '{}', ]); $kernel = $this->createStub(HttpKernelInterface::class); $c = new RequestDataCollector(); $c->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $this->createResponse())); $this->assertTrue($request->attributes->get('_redirected')); } public function testItSetsARedirectCookieIfTheResponseIsARedirection() { $c = new RequestDataCollector(); $response = $this->createResponse(); $response->setStatusCode(302); $response->headers->set('Location', '/somewhere-else'); $c->collect($request = $this->createRequest(), $response); $c->lateCollect(); $cookie = $this->getCookieByName($response, 'sf_redirect'); $this->assertNotEmpty($cookie->getValue()); $this->assertSame('lax', $cookie->getSameSite()); $this->assertFalse($cookie->isSecure()); } public function testItCollectsTheRedirectionAndClearTheCookie() { $c = new RequestDataCollector(); $request = $this->createRequest(); $request->attributes->set('_redirected', true); $request->cookies->add([ 'sf_redirect' => '{"method": "POST"}', ]); $c->collect($request, $response = $this->createResponse()); $c->lateCollect(); $this->assertEquals('POST', $c->getRedirect()['method']); $cookie = $this->getCookieByName($response, 'sf_redirect'); $this->assertNull($cookie->getValue()); } public function testItCollectsTheSessionTraceProperly() { $collector = new RequestDataCollector(); $request = $this->createRequest(); // RequestDataCollectorTest doesn't implement SessionInterface or SessionBagInterface, therefore should do nothing. $collector->collectSessionUsage(); $collector->collect($request, $this->createResponse()); $this->assertSame([], $collector->getSessionUsages()); $collector->reset(); $session = $this->createStub(SessionInterface::class); $session->method('getMetadataBag')->willReturnCallback(static function () use ($collector) { $collector->collectSessionUsage(); return new MetadataBag(); }); $session->getMetadataBag(); $collector->collect($request, $this->createResponse()); $collector->lateCollect(); $usages = $collector->getSessionUsages(); $this->assertCount(1, $usages); $this->assertSame(__FILE__, $usages[0]['file']); $this->assertSame(__LINE__ - 9, $line = $usages[0]['line']); $trace = $usages[0]['trace']; $this->assertSame('getMetadataBag', $trace[0]['function']); $this->assertSame(self::class, $class = $trace[1]['class']); $this->assertSame(\sprintf('%s:%s', $class, $line), $usages[0]['name']); } public function testStatelessCheck() { $requestStack = new RequestStack(); $request = $this->createRequest(); $requestStack->push($request); $collector = new RequestDataCollector($requestStack); $collector->collect($request, $response = $this->createResponse()); $collector->lateCollect(); $this->assertFalse($collector->getStatelessCheck()); $requestStack = new RequestStack(); $request = $this->createRequest(); $request->attributes->set('_stateless', true); $requestStack->push($request); $collector = new RequestDataCollector($requestStack); $collector->collect($request, $response = $this->createResponse()); $collector->lateCollect(); $this->assertTrue($collector->getStatelessCheck()); $requestStack = new RequestStack(); $request = $this->createRequest(); $collector = new RequestDataCollector($requestStack); $collector->collect($request, $response = $this->createResponse()); $collector->lateCollect(); $this->assertFalse($collector->getStatelessCheck()); } public function testItHidesPassword() { $c = new RequestDataCollector(); $request = Request::create( 'http://test.com/login', 'POST', ['_password' => ' _password@123'], [], [], [], '_password=%20_password%40123' ); $c->collect($request, $this->createResponse()); $c->lateCollect(); $this->assertEquals('******', $c->getRequestRequest()->get('_password')); $this->assertEquals('_password=******', $c->getContent()); } protected function createRequest($routeParams = ['name' => 'foo']) { $request = Request::create('http://test.com/foo?bar=baz'); $request->attributes->set('foo', 'bar'); $request->attributes->set('_route', 'foobar'); $request->attributes->set('_route_params', $routeParams); $request->attributes->set('resource', fopen(__FILE__, 'r')); $request->attributes->set('object', new \stdClass()); return $request; } private function createRequestWithSession() { $request = $this->createRequest(); $request->attributes->set('_controller', 'Foo::bar'); $request->setSession(new Session(new MockArraySessionStorage())); $request->getSession()->start(); return $request; } protected function createResponse() { $response = new Response(); $response->setStatusCode(200); $response->headers->set('Content-Type', 'application/json'); $response->headers->set('X-Foo-Bar', null); $response->headers->setCookie(new Cookie('foo', 'bar', 1, '/foo', 'localhost', true, true, false, null)); $response->headers->setCookie(new Cookie('bar', 'foo', new \DateTimeImmutable('@946684800'), '/', null, false, true, false, null)); $response->headers->setCookie(new Cookie('bazz', 'foo', '2000-12-12', '/', null, false, true, false, null)); return $response; } /** * Inject the given controller callable into the data collector. */ protected function injectController($collector, $controller, $request) { $resolver = $this->createStub(ControllerResolverInterface::class); $httpKernel = new HttpKernel(new EventDispatcher(), $resolver, null, $this->createStub(ArgumentResolverInterface::class)); $event = new ControllerEvent($httpKernel, $controller, $request, HttpKernelInterface::MAIN_REQUEST); $collector->onKernelController($event); } private function getCookieByName(Response $response, $name) { foreach ($response->headers->getCookies() as $cookie) { if ($cookie->getName() == $name) { return $cookie; } } throw new \InvalidArgumentException(\sprintf('Cookie named "%s" is not in response', $name)); } #[DataProvider('provideJsonContentTypes')] public function testIsJson($contentType, $expected) { $response = $this->createResponse(); $request = $this->createRequest(); $request->headers->set('Content-Type', $contentType); $c = new RequestDataCollector(); $c->collect($request, $response); $this->assertSame($expected, $c->isJsonRequest()); } public static function provideJsonContentTypes(): array { return [ ['text/csv', false], ['application/json', true], ['application/JSON', true], ['application/hal+json', true], ['application/xml+json', true], ['application/xml', false], ['', false], ]; } #[DataProvider('providePrettyJson')] public function testGetPrettyJsonValidity($content, $expected) { $response = $this->createResponse(); $request = Request::create('/', 'POST', [], [], [], [], $content); $c = new RequestDataCollector(); $c->collect($request, $response); $this->assertSame($expected, $c->getPrettyJson()); } public static function providePrettyJson(): array { return [ ['null', 'null'], ['{ "foo": "bar" }', '{ "foo": "bar" }'], ['{ "abc" }', null], ['', null], ]; } public function testCurlCommandGet() { $request = Request::create('http://test.com/foo?bar=baz'); $c = new RequestDataCollector(); $c->collect($request, $this->createResponse()); $curlCommand = $c->getCurlCommand(); $this->assertStringStartsWith("curl \\\n --compressed", $curlCommand); $this->assertStringContainsString('--url '.('\\' === \DIRECTORY_SEPARATOR ? '"http://test.com/foo?bar=baz"' : "'http://test.com/foo?bar=baz'"), $curlCommand); $this->assertStringNotContainsString('--request', $curlCommand); } public function testCurlCommandPost() { $request = Request::create('http://test.com/foo', 'POST', [], [], [], [], '{"key":"value"}'); $c = new RequestDataCollector(); $c->collect($request, $this->createResponse()); $curlCommand = $c->getCurlCommand(); $this->assertStringContainsString('--request POST', $curlCommand); $this->assertStringContainsString('--data-raw', $curlCommand); $this->assertStringContainsString('\\' === \DIRECTORY_SEPARATOR ? '"{""key"":""value""}"' : '\'{"key":"value"}\'', $curlCommand); } public function testCurlCommandHead() { $request = Request::create('http://test.com/foo', 'HEAD'); $c = new RequestDataCollector(); $c->collect($request, $this->createResponse()); $curlCommand = $c->getCurlCommand(); $this->assertStringContainsString('--head', $curlCommand); $this->assertStringNotContainsString('--request', $curlCommand); } public function testCurlCommandWithHeaders() { $request = Request::create('http://test.com/foo'); $request->headers->set('Accept', 'application/json'); $request->headers->set('X-Custom-Header', 'custom-value'); $c = new RequestDataCollector(); $c->collect($request, $this->createResponse()); $curlCommand = $c->getCurlCommand(); $this->assertStringContainsString('--header '.('\\' === \DIRECTORY_SEPARATOR ? '"Accept: application/json"' : "'Accept: application/json'"), $curlCommand); $this->assertStringContainsString('--header '.('\\' === \DIRECTORY_SEPARATOR ? '"X-Custom-Header: custom-value"' : "'X-Custom-Header: custom-value'"), $curlCommand); $this->assertStringNotContainsString('Host:', $curlCommand); } public function testCurlCommandWithCookies() { $request = Request::create('http://test.com/foo', 'GET', [], ['session' => 'abc123', 'lang' => 'en']); $c = new RequestDataCollector(); $c->collect($request, $this->createResponse()); $curlCommand = $c->getCurlCommand(); $this->assertStringContainsString('--cookie', $curlCommand); $this->assertStringContainsString('session=abc123', $curlCommand); $this->assertStringContainsString('lang=en', $curlCommand); } public function testCurlCommandPutWithBody() { $request = Request::create('http://test.com/resource/1', 'PUT', [], [], [], [], 'updated data'); $c = new RequestDataCollector(); $c->collect($request, $this->createResponse()); $curlCommand = $c->getCurlCommand(); $this->assertStringContainsString('--request PUT', $curlCommand); $this->assertStringContainsString('--data-raw', $curlCommand); } public function testCurlCommandDoesNotDuplicateQueryString() { $request = Request::create('http://test.com/path?foo=bar&baz=qux'); $c = new RequestDataCollector(); $c->collect($request, $this->createResponse()); $curlCommand = $c->getCurlCommand(); $this->assertSame(1, substr_count($curlCommand, 'foo=bar')); $this->assertSame(1, substr_count($curlCommand, 'baz=qux')); } public function testCurlCommandGetWithNoBody() { $request = Request::create('http://test.com/foo', 'GET'); $c = new RequestDataCollector(); $c->collect($request, $this->createResponse()); $curlCommand = $c->getCurlCommand(); $this->assertStringNotContainsString('--data-raw', $curlCommand); } public function testCurlCommandIsEmptyStringWhenNotCollected() { $c = new RequestDataCollector(); $this->assertSame('', $c->getCurlCommand()); } } ================================================ FILE: Tests/DataCollector/RouterDataCollectorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\RouterDataCollector; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; class RouterDataCollectorTest extends TestCase { public function testRouteRedirectResponse() { $collector = new RouterDataCollector(); $request = Request::create('http://test.com/foo?bar=baz'); $response = new RedirectResponse('http://test.com/redirect'); $event = $this->createControllerEvent($request); $collector->onKernelController($event); $collector->collect($request, $response); $this->assertTrue($collector->getRedirect()); $this->assertEquals('http://test.com/redirect', $collector->getTargetUrl()); $this->assertEquals('n/a', $collector->getTargetRoute()); } public function testRouteNotRedirectResponse() { $collector = new RouterDataCollector(); $request = Request::create('http://test.com/foo?bar=baz'); $response = new Response('test'); $event = $this->createControllerEvent($request); $collector->onKernelController($event); $collector->collect($request, $response); $this->assertFalse($collector->getRedirect()); $this->assertNull($collector->getTargetUrl()); $this->assertNull($collector->getTargetRoute()); } public function testReset() { $collector = new RouterDataCollector(); // Fill Collector $request = Request::create('http://test.com/foo?bar=baz'); $response = new RedirectResponse('http://test.com/redirect'); $event = $this->createControllerEvent($request); $collector->onKernelController($event); $collector->collect($request, $response); $collector->reset(); $this->assertFalse($collector->getRedirect()); $this->assertNull($collector->getTargetUrl()); $this->assertNull($collector->getTargetRoute()); } public function testGetName() { $collector = new RouterDataCollector(); $this->assertEquals('router', $collector->getName()); } protected function createControllerEvent(Request $request): ControllerEvent { $kernel = $this->createStub(HttpKernelInterface::class); return new ControllerEvent($kernel, static function () {}, $request, null); } } ================================================ FILE: Tests/DataCollector/TimeDataCollectorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DataCollector; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\TimeDataCollector; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\Stopwatch\StopwatchEvent; #[Group('time-sensitive')] class TimeDataCollectorTest extends TestCase { public function testCollect() { $c = new TimeDataCollector(); $request = new Request(); $request->server->set('REQUEST_TIME', 1); $c->collect($request, new Response()); $this->assertEquals(0, $c->getStartTime()); $request->server->set('REQUEST_TIME_FLOAT', 2); $c->collect($request, new Response()); $this->assertEquals(2000, $c->getStartTime()); $request = new Request(); $c->collect($request, new Response()); $this->assertEquals(0, $c->getStartTime()); $kernel = $this->createMock(KernelInterface::class); $kernel->expects($this->once())->method('getStartTime')->willReturn(123456.0); $c = new TimeDataCollector($kernel); $request = new Request(); $request->server->set('REQUEST_TIME', 1); $c->collect($request, new Response()); $this->assertEquals(123456000, $c->getStartTime()); $this->assertSame(class_exists(Stopwatch::class, false), $c->isStopwatchInstalled()); } public function testReset() { $collector = new TimeDataCollector(); // Fill Collector $request = Request::create('http://test.com/foo?bar=baz'); $response = new Response('test'); $collector->collect($request, $response); $collector->reset(); $this->assertEquals([], $collector->getEvents()); $this->assertEquals(0, $collector->getStartTime()); $this->assertFalse($collector->isStopwatchInstalled()); } public function testLateCollect() { $stopwatch = new Stopwatch(); $stopwatch->start('test'); $collector = new TimeDataCollector(null, $stopwatch); $request = new Request(); $request->attributes->set('_stopwatch_token', '__root__'); $collector->collect($request, new Response()); $collector->lateCollect(); $this->assertEquals(['test'], array_keys($collector->getEvents())); } public function testSetEvents() { $collector = new TimeDataCollector(); $event = $this->createMock(StopwatchEvent::class); $event->expects($this->once())->method('ensureStopped'); $events = [$event]; $collector->setEvents($events); $this->assertCount(1, $collector->getEvents()); } public function testGetDurationHasEvents() { $collector = new TimeDataCollector(); $request = new Request(); $request->server->set('REQUEST_TIME_FLOAT', 1); $collector->collect($request, new Response()); $event = $this->createMock(StopwatchEvent::class); $event->expects($this->once())->method('getDuration')->willReturn(2000.0); $event->expects($this->once())->method('getOrigin')->willReturn(1000.0); $events = ['__section__' => $event]; $collector->setEvents($events); $this->assertEquals(1000 + 2000 - 1000, $collector->getDuration()); } public function testGetDurationNotEvents() { $collector = new TimeDataCollector(); $this->assertEquals(0, $collector->getDuration()); } public function testGetInitTimeNotEvents() { $collector = new TimeDataCollector(); $this->assertEquals(0, $collector->getInitTime()); } public function testGetInitTimeHasEvents() { $collector = new TimeDataCollector(); $request = new Request(); $request->server->set('REQUEST_TIME_FLOAT', 1); $collector->collect($request, new Response()); $event = $this->createMock(StopwatchEvent::class); $event->expects($this->once())->method('getOrigin')->willReturn(2000.0); $events = ['__section__' => $event]; $collector->setEvents($events); $this->assertEquals(2000 - 1000, $collector->getInitTime()); } public function testGetName() { $collector = new TimeDataCollector(); $this->assertEquals('time', $collector->getName()); } } ================================================ FILE: Tests/Debug/ErrorHandlerConfiguratorTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Debug; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Psr\Log\LogLevel; use Psr\Log\NullLogger; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\HttpKernel\Debug\ErrorHandlerConfigurator; class ErrorHandlerConfiguratorTest extends TestCase { public function testConfigure() { $logger = new NullLogger(); $configurator = new ErrorHandlerConfigurator($logger); $handler = new ErrorHandler(); $configurator->configure($handler); $loggers = $handler->setLoggers([]); $this->assertArrayHasKey(\E_DEPRECATED, $loggers); $this->assertSame([$logger, LogLevel::INFO], $loggers[\E_DEPRECATED]); } #[DataProvider('provideLevelsAssignedToLoggers')] public function testLevelsAssignedToLoggers(bool $hasLogger, bool $hasDeprecationLogger, array|int $levels, array|int|null $expectedLoggerLevels, array|int|null $expectedDeprecationLoggerLevels) { $handler = $this->createMock(ErrorHandler::class); $expectedCalls = []; $logger = null; $deprecationLogger = null; if ($hasDeprecationLogger) { $deprecationLogger = new NullLogger(); if (null !== $expectedDeprecationLoggerLevels) { $expectedCalls[] = [$deprecationLogger, $expectedDeprecationLoggerLevels, false]; } } if ($hasLogger) { $logger = new NullLogger(); if (null !== $expectedLoggerLevels) { $expectedCalls[] = [$logger, $expectedLoggerLevels, false]; } } $handler ->expects($this->exactly(\count($expectedCalls))) ->method('setDefaultLogger') ->willReturnCallback(function (...$args) use (&$expectedCalls) { $this->assertSame(array_shift($expectedCalls), $args); }) ; $configurator = new ErrorHandlerConfigurator($logger, $levels, null, true, true, $deprecationLogger); $configurator->configure($handler); } public static function provideLevelsAssignedToLoggers(): iterable { yield [false, false, 0, null, null]; yield [false, false, \E_ALL, null, null]; yield [false, false, [], null, null]; yield [false, false, [\E_WARNING => LogLevel::WARNING, \E_USER_DEPRECATED => LogLevel::NOTICE], null, null]; yield [true, false, \E_ALL, \E_ALL, null]; yield [true, false, \E_DEPRECATED, \E_DEPRECATED, null]; yield [true, false, [], null, null]; yield [true, false, [\E_WARNING => LogLevel::WARNING, \E_DEPRECATED => LogLevel::NOTICE], [\E_WARNING => LogLevel::WARNING, \E_DEPRECATED => LogLevel::NOTICE], null]; yield [false, true, 0, null, null]; yield [false, true, \E_ALL, null, \E_DEPRECATED | \E_USER_DEPRECATED]; yield [false, true, \E_ERROR, null, null]; yield [false, true, [], null, null]; yield [false, true, [\E_ERROR => LogLevel::ERROR, \E_DEPRECATED => LogLevel::DEBUG], null, [\E_DEPRECATED => LogLevel::DEBUG]]; yield [true, true, 0, null, null]; yield [true, true, \E_ALL, \E_ALL & ~(\E_DEPRECATED | \E_USER_DEPRECATED), \E_DEPRECATED | \E_USER_DEPRECATED]; yield [true, true, \E_ERROR, \E_ERROR, null]; yield [true, true, \E_USER_DEPRECATED, null, \E_USER_DEPRECATED]; yield [true, true, [\E_ERROR => LogLevel::ERROR, \E_DEPRECATED => LogLevel::DEBUG], [\E_ERROR => LogLevel::ERROR], [\E_DEPRECATED => LogLevel::DEBUG]]; yield [true, true, [\E_ERROR => LogLevel::ALERT], [\E_ERROR => LogLevel::ALERT], null]; yield [true, true, [\E_USER_DEPRECATED => LogLevel::NOTICE], null, [\E_USER_DEPRECATED => LogLevel::NOTICE]]; } } ================================================ FILE: Tests/Debug/TraceableEventDispatcherTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Debug; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Contracts\EventDispatcher\Event; class TraceableEventDispatcherTest extends TestCase { public function testStopwatchSections() { $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), $stopwatch = new Stopwatch()); $kernel = $this->getHttpKernel($dispatcher); $request = Request::create('/'); $response = $kernel->handle($request); $kernel->terminate($request, $response); $events = $stopwatch->getSectionEvents($request->attributes->get('_stopwatch_token')); $this->assertEquals([ '__section__', 'kernel.request', 'kernel.controller', 'kernel.controller_arguments', 'controller', 'kernel.response', 'kernel.terminate', ], array_keys($events)); } public function testStopwatchCheckControllerOnRequestEvent() { $stopwatch = $this->getMockBuilder(Stopwatch::class) ->onlyMethods(['isStarted']) ->getMock(); $stopwatch->expects($this->once()) ->method('isStarted') ->willReturn(false); $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), $stopwatch); $kernel = $this->getHttpKernel($dispatcher); $request = Request::create('/'); $kernel->handle($request); } public function testStopwatchStopControllerOnRequestEvent() { $stopwatch = $this->getMockBuilder(Stopwatch::class) ->onlyMethods(['isStarted', 'stop']) ->getMock(); $stopwatch->expects($this->once()) ->method('isStarted') ->willReturn(true); $stopwatch->expects($this->exactly(3)) ->method('stop'); $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), $stopwatch); $kernel = $this->getHttpKernel($dispatcher); $request = Request::create('/'); $kernel->handle($request); } public function testAddListenerNested() { $called1 = false; $called2 = false; $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $dispatcher->addListener('my-event', static function () use ($dispatcher, &$called1, &$called2) { $called1 = true; $dispatcher->addListener('my-event', static function () use (&$called2) { $called2 = true; }); }); $dispatcher->dispatch(new Event(), 'my-event'); $this->assertTrue($called1); $this->assertFalse($called2); $dispatcher->dispatch(new Event(), 'my-event'); $this->assertTrue($called2); } public function testListenerCanRemoveItselfWhenExecuted() { $eventDispatcher = new TraceableEventDispatcher(new EventDispatcher(), new Stopwatch()); $listener1 = static function () use ($eventDispatcher, &$listener1) { $eventDispatcher->removeListener('foo', $listener1); }; $eventDispatcher->addListener('foo', $listener1); $eventDispatcher->addListener('foo', static function () {}); $eventDispatcher->dispatch(new Event(), 'foo'); $this->assertCount(1, $eventDispatcher->getListeners('foo'), 'expected listener1 to be removed'); } protected function getHttpKernel($dispatcher) { $controllerResolver = $this->createMock(ControllerResolverInterface::class); $controllerResolver->expects($this->once())->method('getController')->willReturn(static fn () => new Response()); $argumentResolver = $this->createMock(ArgumentResolverInterface::class); $argumentResolver->expects($this->once())->method('getArguments')->willReturn([]); return new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver); } } ================================================ FILE: Tests/DependencyInjection/ControllerArgumentValueResolverPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass; use Symfony\Component\Stopwatch\Stopwatch; class ControllerArgumentValueResolverPassTest extends TestCase { public function testServicesAreOrderedAccordingToPriority() { $services = [ 'n3' => [[]], 'n1' => [['priority' => 200]], 'n2' => [['priority' => 100]], ]; $expected = [ new Reference('n1'), new Reference('n2'), new Reference('n3'), ]; $definition = new Definition(ArgumentResolver::class, [null, []]); $container = new ContainerBuilder(); $container->setDefinition('argument_resolver', $definition); foreach ($services as $id => [$tag]) { $container->register($id)->addTag('controller.argument_value_resolver', $tag); } $container->setParameter('kernel.debug', false); (new ControllerArgumentValueResolverPass())->process($container); $this->assertEquals($expected, $definition->getArgument(1)->getValues()); $this->assertFalse($container->hasDefinition('n1.traceable')); $this->assertFalse($container->hasDefinition('n2.traceable')); $this->assertFalse($container->hasDefinition('n3.traceable')); } public function testInDebugWithStopWatchDefinition() { $services = [ 'n3' => [[]], 'n1' => [['priority' => 200]], 'n2' => [['priority' => 100]], ]; $expected = [ new Reference('.debug.value_resolver.n1'), new Reference('.debug.value_resolver.n2'), new Reference('.debug.value_resolver.n3'), ]; $definition = new Definition(ArgumentResolver::class, [null, []]); $container = new ContainerBuilder(); $container->register('debug.stopwatch', Stopwatch::class); $container->setDefinition('argument_resolver', $definition); foreach ($services as $id => [$tag]) { $container->register($id)->addTag('controller.argument_value_resolver', $tag); } $container->setParameter('kernel.debug', true); (new ControllerArgumentValueResolverPass())->process($container); $this->assertEquals($expected, $definition->getArgument(1)->getValues()); $this->assertTrue($container->hasDefinition('.debug.value_resolver.n1')); $this->assertTrue($container->hasDefinition('.debug.value_resolver.n2')); $this->assertTrue($container->hasDefinition('.debug.value_resolver.n3')); $this->assertTrue($container->hasDefinition('n1')); $this->assertTrue($container->hasDefinition('n2')); $this->assertTrue($container->hasDefinition('n3')); } public function testInDebugWithouStopWatchDefinition() { $expected = [new Reference('n1')]; $definition = new Definition(ArgumentResolver::class, [null, []]); $container = new ContainerBuilder(); $container->register('n1')->addTag('controller.argument_value_resolver'); $container->setDefinition('argument_resolver', $definition); $container->setParameter('kernel.debug', true); (new ControllerArgumentValueResolverPass())->process($container); $this->assertEquals($expected, $definition->getArgument(1)->getValues()); $this->assertFalse($container->hasDefinition('debug.n1')); $this->assertTrue($container->hasDefinition('n1')); } public function testReturningEmptyArrayWhenNoService() { $definition = new Definition(ArgumentResolver::class, [null, []]); $container = new ContainerBuilder(); $container->setDefinition('argument_resolver', $definition); $container->setParameter('kernel.debug', false); (new ControllerArgumentValueResolverPass())->process($container); $this->assertEquals([], $definition->getArgument(1)->getValues()); } public function testNoArgumentResolver() { $container = new ContainerBuilder(); (new ControllerArgumentValueResolverPass())->process($container); $this->assertFalse($container->hasDefinition('argument_resolver')); } } ================================================ FILE: Tests/DependencyInjection/ControllerAttributesListenerPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\ControllerAttributesListenerPass; use Symfony\Component\HttpKernel\EventListener\ControllerAttributesListener; use Symfony\Component\HttpKernel\KernelEvents; class ControllerAttributesListenerPassTest extends TestCase { public function testCollectsAttributeListenersByKernelEvent() { $container = new ContainerBuilder(); $dispatcher = new Definition(); $dispatcher->addMethodCall('addListener', [KernelEvents::CONTROLLER.'.'.TestAttribute::class, [new Reference('listener.service'), 'onKernelController'], 0]); $dispatcher->addMethodCall('addListener', [KernelEvents::RESPONSE.'.'.AnotherAttribute::class, [new Reference('listener.service'), 'onKernelResponse'], 0]); $dispatcher->addMethodCall('addListener', [KernelEvents::REQUEST, [new Reference('other.service'), 'onKernelRequest'], 0]); $container->setDefinition('event_dispatcher', $dispatcher); $listener = new Definition(ControllerAttributesListener::class, [[]]); $container->setDefinition('kernel.controller_attributes_listener', $listener); $pass = new ControllerAttributesListenerPass(); $pass->process($container); $this->assertSame([ KernelEvents::CONTROLLER => [TestAttribute::class => true], KernelEvents::RESPONSE => [AnotherAttribute::class => true], ], $listener->getArgument(0)); } public function testSetsEmptyConfigurationWhenNoAttributeListenersAreRegistered() { $container = new ContainerBuilder(); $dispatcher = new Definition(); $dispatcher->addMethodCall('addListener', [KernelEvents::REQUEST, [new Reference('listener.service'), 'onKernelRequest'], 0]); $container->setDefinition('event_dispatcher', $dispatcher); $listener = new Definition(ControllerAttributesListener::class, [[]]); $container->setDefinition('kernel.controller_attributes_listener', $listener); $pass = new ControllerAttributesListenerPass(); $pass->process($container); $this->assertSame([], $listener->getArgument(0)); } } #[\Attribute] class TestAttribute { } #[\Attribute] class AnotherAttribute extends TestAttribute { } ================================================ FILE: Tests/DependencyInjection/FragmentRendererPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DependencyInjection\FragmentRendererPass; use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface; class FragmentRendererPassTest extends TestCase { /** * Tests that content rendering not implementing FragmentRendererInterface * triggers an exception. */ public function testContentRendererWithoutInterface() { $this->expectException(\InvalidArgumentException::class); $builder = new ContainerBuilder(); $fragmentHandlerDefinition = $builder->register('fragment.handler'); $builder->register('my_content_renderer', 'Symfony\Component\DependencyInjection\Definition') ->addTag('kernel.fragment_renderer', ['alias' => 'foo']); $pass = new FragmentRendererPass(); $pass->process($builder); $this->assertEquals([['addRendererService', ['foo', 'my_content_renderer']]], $fragmentHandlerDefinition->getMethodCalls()); } public function testValidContentRenderer() { $builder = new ContainerBuilder(); $fragmentHandlerDefinition = $builder->register('fragment.handler') ->addArgument(null); $builder->register('my_content_renderer', 'Symfony\Component\HttpKernel\Tests\DependencyInjection\RendererService') ->addTag('kernel.fragment_renderer', ['alias' => 'foo']); $pass = new FragmentRendererPass(); $pass->process($builder); $serviceLocatorDefinition = $builder->getDefinition((string) $fragmentHandlerDefinition->getArgument(0)); $this->assertSame(ServiceLocator::class, $serviceLocatorDefinition->getClass()); $this->assertEquals(['foo' => new ServiceClosureArgument(new Reference('my_content_renderer'))], $serviceLocatorDefinition->getArgument(0)); } } class RendererService implements FragmentRendererInterface { public function render($uri, ?Request $request = null, array $options = []): Response { } public function getName(): string { return 'test'; } } ================================================ FILE: Tests/DependencyInjection/LazyLoadingFragmentHandlerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DependencyInjection\LazyLoadingFragmentHandler; use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface; class LazyLoadingFragmentHandlerTest extends TestCase { public function testRender() { $renderer = $this->createMock(FragmentRendererInterface::class); $renderer->expects($this->once())->method('getName')->willReturn('foo'); $renderer->method('render')->willReturn(new Response()); $requestStack = new RequestStack(); $requestStack->push(Request::create('/')); $container = new Container(); $container->set('foo', $renderer); $handler = new LazyLoadingFragmentHandler($container, $requestStack, false); $handler->render('/foo', 'foo'); // second call should not lazy-load anymore (see once() above on the get() method) $handler->render('/foo', 'foo'); } } ================================================ FILE: Tests/DependencyInjection/LoggerPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; use Symfony\Component\HttpKernel\Log\Logger; /** * @author Kévin Dunglas */ class LoggerPassTest extends TestCase { public function testAlwaysSetAutowiringAlias() { $container = new ContainerBuilder(); $container->register('logger', 'Foo'); (new LoggerPass())->process($container); $this->assertFalse($container->getAlias(LoggerInterface::class)->isPublic()); } public function testDoNotOverrideExistingLogger() { $container = new ContainerBuilder(); $container->register('logger', 'Foo'); (new LoggerPass())->process($container); $this->assertSame('Foo', $container->getDefinition('logger')->getClass()); } public function testRegisterLogger() { $container = new ContainerBuilder(); $container->setParameter('kernel.debug', false); (new LoggerPass())->process($container); $definition = $container->getDefinition('logger'); $this->assertSame(Logger::class, $definition->getClass()); $this->assertFalse($definition->isPublic()); } public function testAutowiringAliasIsPreserved() { $container = new ContainerBuilder(); $container->setParameter('kernel.debug', false); $container->setAlias(LoggerInterface::class, 'my_logger'); (new LoggerPass())->process($container); $this->assertSame('my_logger', (string) $container->getAlias(LoggerInterface::class)); } } ================================================ FILE: Tests/DependencyInjection/MergeExtensionConfigurationPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass; use Symfony\Component\HttpKernel\Tests\Fixtures\AcmeFooBundle\AcmeFooBundle; class MergeExtensionConfigurationPassTest extends TestCase { public function testAutoloadMainExtension() { $container = new ContainerBuilder(); $container->registerExtension(new LoadedExtension()); $container->registerExtension(new NotLoadedExtension()); $container->loadFromExtension('loaded', []); $configPass = new MergeExtensionConfigurationPass(['loaded', 'not_loaded']); $configPass->process($container); $this->assertTrue($container->hasDefinition('loaded.foo')); $this->assertTrue($container->hasDefinition('not_loaded.bar')); } public function testFooBundle() { $bundle = new AcmeFooBundle(); $container = new ContainerBuilder(new ParameterBag([ 'kernel.environment' => 'test', 'kernel.build_dir' => sys_get_temp_dir(), ])); $container->registerExtension(new LoadedExtension()); $container->registerExtension($bundle->getContainerExtension()); $configPass = new MergeExtensionConfigurationPass(['loaded', 'acme_foo']); $configPass->process($container); $this->assertSame([['bar' => 'baz'], []], $container->getExtensionConfig('loaded'), '->prependExtension() prepends an extension config'); $this->assertTrue($container->hasDefinition('acme_foo.foo'), '->loadExtension() registers a service'); $this->assertTrue($container->hasDefinition('acme_foo.bar'), '->loadExtension() imports a service'); $this->assertTrue($container->hasParameter('acme_foo.config'), '->loadExtension() sets a parameter'); $this->assertSame(['foo' => 'bar', 'ping' => 'pong'], $container->getParameter('acme_foo.config'), '->loadConfiguration() defines and loads configurations'); } } class LoadedExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { $container->register('loaded.foo'); } } class NotLoadedExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { $container->register('not_loaded.bar'); } } ================================================ FILE: Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\LazyClosure; use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\DependencyInjection\Attribute\AutowireCallable; use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass; use Symfony\Component\HttpKernel\Tests\Fixtures\DataCollector\DummyController; use Symfony\Component\HttpKernel\Tests\Fixtures\Suit; class RegisterControllerArgumentLocatorsPassTest extends TestCase { public function testInvalidClass() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Class "Symfony\Component\HttpKernel\Tests\DependencyInjection\NotFound" used for service "foo" cannot be found.'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', NotFound::class) ->addTag('controller.service_arguments') ; $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); } public function testNoAction() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Missing "action" attribute on tag "controller.service_arguments" {"argument":"bar"} for service "foo".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', RegisterTestController::class) ->addTag('controller.service_arguments', ['argument' => 'bar']) ; $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); } public function testNoArgument() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Missing "argument" attribute on tag "controller.service_arguments" {"action":"fooAction"} for service "foo".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', RegisterTestController::class) ->addTag('controller.service_arguments', ['action' => 'fooAction']) ; $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); } public function testNoService() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Missing "id" attribute on tag "controller.service_arguments" {"action":"fooAction","argument":"bar"} for service "foo".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', RegisterTestController::class) ->addTag('controller.service_arguments', ['action' => 'fooAction', 'argument' => 'bar']) ; $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); } public function testInvalidMethod() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid "action" attribute on tag "controller.service_arguments" for service "foo": no public "barAction()" method found on class "Symfony\Component\HttpKernel\Tests\DependencyInjection\RegisterTestController".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', RegisterTestController::class) ->addTag('controller.service_arguments', ['action' => 'barAction', 'argument' => 'bar', 'id' => 'bar_service']) ; $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); } public function testInvalidArgument() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid "controller.service_arguments" tag for service "foo": method "fooAction()" has no "baz" argument on class "Symfony\Component\HttpKernel\Tests\DependencyInjection\RegisterTestController".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', RegisterTestController::class) ->addTag('controller.service_arguments', ['action' => 'fooAction', 'argument' => 'baz', 'id' => 'bar']) ; $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); } public function testAllActions() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', RegisterTestController::class) ->addTag('controller.service_arguments') ; $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertEquals(['foo::fooAction'], array_keys($locator)); $this->assertInstanceof(ServiceClosureArgument::class, $locator['foo::fooAction']); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); $locator = $container->getDefinition((string) $locator->getFactory()[0]); $this->assertSame(ServiceLocator::class, $locator->getClass()); $this->assertFalse($locator->isPublic()); $expected = ['bar' => new ServiceClosureArgument(new TypedReference(ControllerDummy::class, ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE, 'bar'))]; $this->assertEquals($expected, $locator->getArgument(0)); } public function testExplicitArgument() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', RegisterTestController::class) ->addTag('controller.service_arguments', ['action' => 'fooAction', 'argument' => 'bar', 'id' => 'bar']) ->addTag('controller.service_arguments', ['action' => 'fooAction', 'argument' => 'bar', 'id' => 'baz']) // should be ignored, the first wins ; $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); $locator = $container->getDefinition((string) $locator->getFactory()[0]); $expected = ['bar' => new ServiceClosureArgument(new TypedReference('bar', ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE))]; $this->assertEquals($expected, $locator->getArgument(0)); } public function testOptionalArgument() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', RegisterTestController::class) ->addTag('controller.service_arguments', ['action' => 'fooAction', 'argument' => 'bar', 'id' => '?bar']) ; $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); $locator = $container->getDefinition((string) $locator->getFactory()[0]); $expected = ['bar' => new ServiceClosureArgument(new TypedReference('bar', ControllerDummy::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE))]; $this->assertEquals($expected, $locator->getArgument(0)); } public function testSkipSetContainer() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', ContainerAwareRegisterTestController::class) ->addTag('controller.service_arguments'); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertSame(['foo::fooAction'], array_keys($locator)); } public function testExceptionOnNonExistentTypeHint() { $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Cannot determine controller argument for "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClassController::fooAction()": the $nonExistent argument is type-hinted with the non-existent class or interface: "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClass". Did you forget to add a use statement?'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', NonExistentClassController::class) ->addTag('controller.service_arguments'); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $error = $container->getDefinition('argument_resolver.service')->getArgument(0); $error = $container->getDefinition($error)->getArgument(0)['foo::fooAction']->getValues()[0]; $error = $container->getDefinition($error)->getArgument(0)['nonExistent']->getValues()[0]; $container->get($error); } public function testExceptionOnNonExistentTypeHintDifferentNamespace() { $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Cannot determine controller argument for "Symfony\Component\HttpKernel\Tests\DependencyInjection\NonExistentClassDifferentNamespaceController::fooAction()": the $nonExistent argument is type-hinted with the non-existent class or interface: "Acme\NonExistentClass".'); $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', NonExistentClassDifferentNamespaceController::class) ->addTag('controller.service_arguments'); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $error = $container->getDefinition('argument_resolver.service')->getArgument(0); $error = $container->getDefinition($error)->getArgument(0)['foo::fooAction']->getValues()[0]; $error = $container->getDefinition($error)->getArgument(0)['nonExistent']->getValues()[0]; $container->get($error); } public function testNoExceptionOnNonExistentTypeHintOptionalArg() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', NonExistentClassOptionalController::class) ->addTag('controller.service_arguments'); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertEqualsCanonicalizing(['foo::barAction', 'foo::fooAction'], array_keys($locator)); } public function testArgumentWithNoTypeHintIsOk() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', ArgumentWithoutTypeController::class) ->addTag('controller.service_arguments'); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertSame([], array_keys($locator)); } public function testControllersAreMadePublic() { $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', ArgumentWithoutTypeController::class) ->addTag('controller.service_arguments'); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $this->assertTrue($container->getDefinition('foo')->isPublic()); } public function testControllersAreMadeNonLazy() { $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', DummyController::class) ->addTag('controller.service_arguments') ->setLazy(true); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $this->assertFalse($container->getDefinition('foo')->isLazy()); } #[DataProvider('provideBindings')] public function testBindings($bindingName) { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', RegisterTestController::class) ->setBindings([$bindingName => new Reference('foo')]) ->addTag('controller.service_arguments'); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); $locator = $container->getDefinition((string) $locator->getFactory()[0]); $expected = ['bar' => new ServiceClosureArgument(new Reference('foo'))]; $this->assertEquals($expected, $locator->getArgument(0)); } public static function provideBindings() { return [ [ControllerDummy::class.'$bar'], [ControllerDummy::class], ['$bar'], ]; } #[DataProvider('provideBindScalarValueToControllerArgument')] public function testBindScalarValueToControllerArgument($bindingKey) { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service', 'stdClass')->addArgument([]); $container->register('foo', ArgumentWithoutTypeController::class) ->setBindings([$bindingKey => '%foo%']) ->addTag('controller.service_arguments'); $container->setParameter('foo', 'foo_val'); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $locatorId = (string) $resolver->getArgument(0); $container->getDefinition($locatorId)->setPublic(true); $container->compile(); $locator = $container->get($locatorId); $this->assertSame('foo_val', $locator->get('foo::fooAction')->get('someArg')); } public static function provideBindScalarValueToControllerArgument() { yield ['$someArg']; yield ['string $someArg']; } public function testBindingsOnChildDefinitions() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('parent', ArgumentWithoutTypeController::class); $container->setDefinition('child', (new ChildDefinition('parent')) ->setBindings(['$someArg' => new Reference('parent')]) ->addTag('controller.service_arguments') ); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertInstanceOf(ServiceClosureArgument::class, $locator['child::fooAction']); $locator = $container->getDefinition((string) $locator['child::fooAction']->getValues()[0]); $locator = $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0); $this->assertInstanceOf(ServiceClosureArgument::class, $locator['someArg']); $this->assertEquals(new Reference('parent'), $locator['someArg']->getValues()[0]); } public function testNotTaggedControllerServiceReceivesLocatorArgument() { $container = new ContainerBuilder(); $container->register('argument_resolver.not_tagged_controller')->addArgument([]); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $locatorArgument = $container->getDefinition('argument_resolver.not_tagged_controller')->getArgument(0); $this->assertInstanceOf(Reference::class, $locatorArgument); } public function testAlias() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', RegisterTestController::class) ->addTag('controller.service_arguments'); $container->setAlias(RegisterTestController::class, 'foo')->setPublic(true); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertEqualsCanonicalizing([RegisterTestController::class.'::fooAction', 'foo::fooAction'], array_keys($locator)); } public function testEnumArgumentIsIgnored() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('foo', NonNullableEnumArgumentWithDefaultController::class) ->addTag('controller.service_arguments') ; $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertSame([], array_keys($locator), 'enum typed argument is ignored'); } public function testBindWithTarget() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register(ControllerDummy::class, 'bar'); $container->register(ControllerDummy::class.' $imageStorage', 'baz'); $container->register('foo', WithTarget::class) ->setBindings(['string $someApiKey' => new Reference('the_api_key')]) ->addTag('controller.service_arguments'); (new RegisterControllerArgumentLocatorsPass())->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); $locator = $container->getDefinition((string) $locator->getFactory()[0]); $expected = [ 'apiKey' => new ServiceClosureArgument(new Reference('the_api_key')), 'service1' => new ServiceClosureArgument(new TypedReference(ControllerDummy::class, ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE, 'imageStorage', [new Target('image.storage')])), 'service2' => new ServiceClosureArgument(new TypedReference(ControllerDummy::class, ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE, 'service2')), ]; $this->assertEquals($expected, $locator->getArgument(0)); } public function testTargetAttributeUsesShortNameForControllerArguments() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('limiter.anonymous_action', DummyRateLimiterFactory::class); $container->registerAliasForArgument('limiter.anonymous_action', DummyLimiterFactoryInterface::class, 'anonymous_action.limiter', 'anonymous_action'); $container->register('foo', WithTargetShortName::class) ->addTag('controller.service_arguments'); (new RegisterControllerArgumentLocatorsPass())->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); $locator = $container->getDefinition((string) $locator->getFactory()[0]); $argument = $locator->getArgument(0)['limiterFactory']->getValues()[0]; $this->assertInstanceOf(TypedReference::class, $argument); $this->assertSame(DummyLimiterFactoryInterface::class, $argument->getType()); $this->assertSame('anonymous_action', $argument->getName()); $this->assertEquals([new Target('anonymous_action')], $argument->getAttributes()); } public function testResponseArgumentIsIgnored() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service', 'stdClass')->addArgument([]); $container->register('foo', WithResponseArgument::class) ->addTag('controller.service_arguments'); (new RegisterControllerArgumentLocatorsPass())->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertSame([], array_keys($locator), 'Response typed argument is ignored'); } public function testAutowireAttribute() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service', 'stdClass')->addArgument([]); $container->register('some.id', \stdClass::class)->setPublic(true); $container->setParameter('some.parameter', 'foo'); $container->register('foo', WithAutowireAttribute::class) ->addTag('controller.service_arguments'); (new RegisterControllerArgumentLocatorsPass())->process($container); $locatorId = (string) $resolver->getArgument(0); $container->getDefinition($locatorId)->setPublic(true); $container->compile(); $locator = $container->get($locatorId)->get('foo::fooAction'); $this->assertCount(10, $locator->getProvidedServices()); $this->assertInstanceOf(\stdClass::class, $locator->get('service1')); $this->assertSame('foo/bar', $locator->get('value')); $this->assertSame('foo', $locator->get('expression')); $this->assertInstanceOf(\stdClass::class, $locator->get('serviceAsValue')); $this->assertInstanceOf(\stdClass::class, $locator->get('expressionAsValue')); $this->assertSame('bar', $locator->get('rawValue')); $this->assertStringContainsString('Symfony_Component_HttpKernel_Tests_Fixtures_Suit_APP_SUIT', $locator->get('suit')); $this->assertSame('@bar', $locator->get('escapedRawValue')); $this->assertSame('foo', $locator->get('customAutowire')); $this->assertInstanceOf(FooInterface::class, $autowireCallable = $locator->get('autowireCallable')); $this->assertInstanceOf(LazyClosure::class, $autowireCallable); $this->assertInstanceOf(\stdClass::class, $autowireCallable->service); $this->assertFalse($locator->has('service2')); } public function testAutowireIteratorAndAutowireLocatorAttributes() { $container = new ContainerBuilder(); $container->setParameter('some.parameter', 'bar'); $resolver = $container->register('argument_resolver.service', \stdClass::class)->addArgument([]); $container->register('bar', \stdClass::class)->addTag('foobar'); $container->register('baz', \stdClass::class)->addTag('foobar'); $container->register('foo', WithAutowireIteratorAndAutowireLocator::class) ->addTag('controller.service_arguments'); (new RegisterControllerArgumentLocatorsPass())->process($container); $locatorId = (string) $resolver->getArgument(0); $container->getDefinition($locatorId)->setPublic(true); $container->compile(); /** @var ServiceLocator $locator */ $locator = $container->get($locatorId)->get('foo::fooAction'); $this->assertCount(4, $locator->getProvidedServices()); $this->assertTrue($locator->has('iterator1')); $this->assertInstanceOf(RewindableGenerator::class, $argIterator = $locator->get('iterator1')); $this->assertCount(2, $argIterator); $this->assertTrue($locator->has('locator1')); $this->assertInstanceOf(ServiceLocator::class, $argLocator = $locator->get('locator1')); $this->assertCount(2, $argLocator); $this->assertTrue($argLocator->has('bar')); $this->assertTrue($argLocator->has('baz')); $this->assertSame(iterator_to_array($argIterator), [$argLocator->get('bar'), $argLocator->get('baz')]); $this->assertTrue($locator->has('container1')); $this->assertInstanceOf(ServiceLocator::class, $argLocator = $locator->get('container1')); $this->assertCount(2, $argLocator); $this->assertTrue($argLocator->has('bar')); $this->assertTrue($argLocator->has('baz')); $this->assertTrue($locator->has('container2')); $this->assertInstanceOf(ServiceLocator::class, $argLocator = $locator->get('container2')); $this->assertCount(1, $argLocator); $this->assertTrue($argLocator->has('foo')); $this->assertSame('bar', $argLocator->get('foo')); } public function testTaggedControllersAreRegisteredInControllerResolver() { $container = new ContainerBuilder(); $container->register('argument_resolver.service')->addArgument([]); $controllerResolver = $container->register('controller_resolver'); $container->register('foo', RegisterTestController::class) ->addTag('controller.service_arguments') ; // duplicates should be removed $container->register('bar', RegisterTestController::class) ->addTag('controller.service_arguments') ; // services with no tag should be ignored $container->register('baz', ControllerDummy::class); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $this->assertSame([['allowControllers', [[RegisterTestController::class]]]], $controllerResolver->getMethodCalls()); } } class RegisterTestController { public function __construct(ControllerDummy $bar) { } public function fooAction(ControllerDummy $bar) { } protected function barAction(ControllerDummy $bar) { } } class ContainerAwareRegisterTestController { protected ?ContainerInterface $container; public function setContainer(?ContainerInterface $container = null): void { $this->container = $container; } public function fooAction(ControllerDummy $bar) { } } class ControllerDummy { } class NonExistentClassController { public function fooAction(NonExistentClass $nonExistent) { } } class NonExistentClassDifferentNamespaceController { public function fooAction(\Acme\NonExistentClass $nonExistent) { } } class NonExistentClassOptionalController { public function fooAction(?NonExistentClass $nonExistent = null) { } public function barAction(?NonExistentClass $nonExistent, $bar) { } } class ArgumentWithoutTypeController { public function fooAction(string $someArg) { } } class NonNullableEnumArgumentWithDefaultController { public function fooAction(Suit $suit = Suit::Spades) { } } class WithTarget { public function fooAction( #[Target('some.api.key')] string $apiKey, #[Target('image.storage')] ControllerDummy $service1, ControllerDummy $service2, ) { } } class WithTargetShortName { public function fooAction( #[Target('anonymous_action')] DummyLimiterFactoryInterface $limiterFactory, ) { } } interface DummyLimiterFactoryInterface { public function create(mixed $key = null): object; } class DummyRateLimiterFactory implements DummyLimiterFactoryInterface { public function create(mixed $key = null): object { throw new \BadMethodCallException('Not used in tests.'); } } class WithResponseArgument { public function fooAction(Response $response, ?Response $nullableResponse) { } } #[\Attribute(\Attribute::TARGET_PARAMETER)] class CustomAutowire extends Autowire { public function __construct(string $parameter) { parent::__construct('%'.$parameter.'%'); } } interface FooInterface { public function foo(); } class WithAutowireAttribute { public function fooAction( #[Autowire(service: 'some.id')] \stdClass $service1, #[Autowire(value: '%some.parameter%/bar')] string $value, #[Autowire(expression: "parameter('some.parameter')")] string $expression, #[Autowire('@some.id')] \stdClass $serviceAsValue, #[Autowire("@=service('some.id')")] \stdClass $expressionAsValue, #[Autowire('bar')] string $rawValue, #[Autowire(env: 'enum:\Symfony\Component\HttpKernel\Tests\Fixtures\Suit:APP_SUIT')] Suit $suit, #[Autowire('@@bar')] string $escapedRawValue, #[CustomAutowire('some.parameter')] string $customAutowire, #[AutowireCallable(service: 'some.id', method: 'bar')] FooInterface $autowireCallable, #[Autowire(service: 'invalid.id')] ?\stdClass $service2 = null, ) { } } class WithAutowireIteratorAndAutowireLocator { public function fooAction( #[AutowireIterator('foobar')] iterable $iterator1, #[AutowireLocator('foobar')] ServiceLocator $locator1, #[AutowireLocator(['bar', 'baz'])] ContainerInterface $container1, #[AutowireLocator(['foo' => new Autowire('%some.parameter%')])] ContainerInterface $container2, ) { } } ================================================ FILE: Tests/DependencyInjection/RegisterLocaleAwareServicesPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\RegisterLocaleAwareServicesPass; use Symfony\Component\HttpKernel\EventListener\LocaleAwareListener; use Symfony\Contracts\Translation\LocaleAwareInterface; class RegisterLocaleAwareServicesPassTest extends TestCase { public function testCompilerPass() { $container = new ContainerBuilder(); $container->register('locale_aware_listener', LocaleAwareListener::class) ->setPublic(true) ->setArguments([null, null]); $container->register('some_locale_aware_service', LocaleAwareInterface::class) ->setPublic(true) ->addTag('kernel.locale_aware'); $container->register('another_locale_aware_service', LocaleAwareInterface::class) ->setPublic(true) ->addTag('kernel.locale_aware'); $container->addCompilerPass(new RegisterLocaleAwareServicesPass()); $container->compile(); $this->assertEquals( [ new IteratorArgument([ 0 => new Reference('some_locale_aware_service'), 1 => new Reference('another_locale_aware_service'), ]), null, ], $container->getDefinition('locale_aware_listener')->getArguments() ); } public function testListenerUnregisteredWhenNoLocaleAwareServices() { $container = new ContainerBuilder(); $container->register('locale_aware_listener', LocaleAwareListener::class) ->setPublic(true) ->setArguments([null, null]); $container->addCompilerPass(new RegisterLocaleAwareServicesPass()); $container->compile(); $this->assertFalse($container->hasDefinition('locale_aware_listener')); } } ================================================ FILE: Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Compiler\ResolveInvalidReferencesPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass; use Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass; class RemoveEmptyControllerArgumentLocatorsPassTest extends TestCase { public function testProcess() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('stdClass', 'stdClass'); $container->register(TestCase::class, 'stdClass'); $container->register('c1', RemoveTestController1::class)->addTag('controller.service_arguments'); $container->register('c2', RemoveTestController2::class)->addTag('controller.service_arguments') ->addMethodCall('setTestCase', [new Reference('c1')]); $pass = new RegisterControllerArgumentLocatorsPass(); $pass->process($container); $controllers = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $getLocator = static fn ($controllers, $k) => $container->getDefinition((string) $container->getDefinition((string) $controllers[$k]->getValues()[0])->getFactory()[0])->getArgument(0); $this->assertCount(2, $getLocator($controllers, 'c1::fooAction')); $this->assertCount(1, $getLocator($controllers, 'c2::setTestCase')); $this->assertCount(1, $getLocator($controllers, 'c2::fooAction')); (new ResolveInvalidReferencesPass())->process($container); $this->assertCount(1, $getLocator($controllers, 'c2::setTestCase')); $this->assertSame([], $getLocator($controllers, 'c2::fooAction')); (new RemoveEmptyControllerArgumentLocatorsPass())->process($container); $controllers = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertSame(['c1::fooAction', 'c1:fooAction'], array_keys($controllers)); $this->assertSame(['bar'], array_keys($getLocator($controllers, 'c1::fooAction'))); $expectedLog = [ 'Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass: Removing service-argument resolver for controller "c2::fooAction": no corresponding services exist for the referenced types.', 'Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass: Removing method "setTestCase" of service "c2" from controller candidates: the method is called at instantiation, thus cannot be an action.', ]; $this->assertEqualsCanonicalizing($expectedLog, $container->getCompiler()->getLog()); } public function testInvoke() { $container = new ContainerBuilder(); $resolver = $container->register('argument_resolver.service')->addArgument([]); $container->register('invokable', InvokableRegisterTestController::class) ->addTag('controller.service_arguments') ; (new RegisterControllerArgumentLocatorsPass())->process($container); (new RemoveEmptyControllerArgumentLocatorsPass())->process($container); $this->assertEquals( ['invokable::__invoke', 'invokable:__invoke', 'invokable'], array_keys($container->getDefinition((string) $resolver->getArgument(0))->getArgument(0)) ); } } class RemoveTestController1 { public function fooAction(\stdClass $bar, ?ClassNotInContainer $baz = null) { } } class RemoveTestController2 { public function setTestCase(TestCase $test) { } public function fooAction(?ClassNotInContainer $bar = null) { } } class InvokableRegisterTestController { public function __invoke(\stdClass $bar) { } } class ClassNotInContainer { } ================================================ FILE: Tests/DependencyInjection/ResettableServicePassTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass; use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService; use Symfony\Component\HttpKernel\Tests\Fixtures\MultiResettableService; use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService; class ResettableServicePassTest extends TestCase { public function testCompilerPass() { $container = new ContainerBuilder(); $container->register('one', ResettableService::class) ->setPublic(true) ->addTag('kernel.reset', ['method' => 'reset']); $container->register('two', ClearableService::class) ->setPublic(true) ->addTag('kernel.reset', ['method' => 'clear']); $container->register('three', MultiResettableService::class) ->setPublic(true) ->addTag('kernel.reset', ['method' => 'resetFirst']) ->addTag('kernel.reset', ['method' => 'resetSecond']); $container->register('services_resetter', ServicesResetter::class) ->setPublic(true) ->setArguments([null, []]); $container->addCompilerPass(new ResettableServicePass()); $container->compile(); $definition = $container->getDefinition('services_resetter'); $this->assertEquals( [ new IteratorArgument([ 'one' => new Reference('one', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE), 'two' => new Reference('two', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE), 'three' => new Reference('three', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE), ]), [ 'one' => ['reset'], 'two' => ['clear'], 'three' => ['resetFirst', 'resetSecond'], ], ], $definition->getArguments() ); } public function testMissingMethod() { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Tag "kernel.reset" requires the "method" attribute to be set on service "Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService".'); $container = new ContainerBuilder(); $container->register(ResettableService::class) ->addTag('kernel.reset'); $container->register('services_resetter', ServicesResetter::class) ->setArguments([null, []]); $container->addCompilerPass(new ResettableServicePass()); $container->compile(); } public function testIgnoreInvalidMethod() { $container = new ContainerBuilder(); $container->register(ResettableService::class) ->setPublic(true) ->addTag('kernel.reset', ['method' => 'missingMethod', 'on_invalid' => 'ignore']); $container->register('services_resetter', ServicesResetter::class) ->setPublic(true) ->setArguments([null, []]); $container->addCompilerPass(new ResettableServicePass()); $container->compile(); $this->assertSame([ResettableService::class => ['?missingMethod']], $container->getDefinition('services_resetter')->getArgument(1)); $resettable = $container->get(ResettableService::class); $resetter = $container->get('services_resetter'); $resetter->reset(); } public function testCompilerPassWithoutResetters() { $container = new ContainerBuilder(); $container->register('services_resetter', ServicesResetter::class) ->setArguments([null, []]); $container->addCompilerPass(new ResettableServicePass()); $container->compile(); $this->assertFalse($container->has('services_resetter')); } } ================================================ FILE: Tests/DependencyInjection/ServicesResetterTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService; use Symfony\Component\HttpKernel\Tests\Fixtures\LazyResettableService; use Symfony\Component\HttpKernel\Tests\Fixtures\MultiResettableService; use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService; use Symfony\Component\VarExporter\ProxyHelper; class ServicesResetterTest extends TestCase { protected function setUp(): void { ResettableService::$counter = 0; ClearableService::$counter = 0; MultiResettableService::$resetFirstCounter = 0; MultiResettableService::$resetSecondCounter = 0; } public function testResetServices() { $resetter = new ServicesResetter(new \ArrayIterator([ 'id1' => new ResettableService(), 'id2' => new ClearableService(), 'id3' => new MultiResettableService(), ]), [ 'id1' => ['reset'], 'id2' => ['clear'], 'id3' => ['resetFirst', 'resetSecond'], ]); $resetter->reset(); $this->assertSame(1, ResettableService::$counter); $this->assertSame(1, ClearableService::$counter); $this->assertSame(1, MultiResettableService::$resetFirstCounter); $this->assertSame(1, MultiResettableService::$resetSecondCounter); } public function testResetLazyServices() { $proxyCode = ProxyHelper::generateLazyProxy(new \ReflectionClass(LazyResettableService::class)); eval('class LazyResettableServiceProxy'.$proxyCode); $lazyService = \LazyResettableServiceProxy::createLazyProxy(static fn (): LazyResettableService => new LazyResettableService()); $resetter = new ServicesResetter(new \ArrayIterator([ 'lazy' => $lazyService, ]), [ 'lazy' => ['reset'], ]); $resetter->reset(); $this->assertSame(0, LazyResettableService::$counter); $resetter->reset(); $this->assertSame(0, LazyResettableService::$counter); $this->assertTrue($lazyService->foo()); $resetter->reset(); $this->assertSame(1, LazyResettableService::$counter); } } ================================================ FILE: Tests/Event/ControllerArgumentsEventTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Event; use PHPUnit\Framework\TestCase; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Bar; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Baz; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController; use Symfony\Component\HttpKernel\Tests\TestHttpKernel; class ControllerArgumentsEventTest extends TestCase { public function testControllerArgumentsEvent() { $event = new ControllerArgumentsEvent(new TestHttpKernel(), static function () {}, ['test'], new Request(), HttpKernelInterface::MAIN_REQUEST); $this->assertSame(['test'], $event->getArguments()); } public function testSetAttributes() { $controller = static function () {}; $event = new ControllerArgumentsEvent(new TestHttpKernel(), $controller, ['test'], new Request(), HttpKernelInterface::MAIN_REQUEST); $event->setController($controller, []); $this->assertSame([], $event->getAttributes()); } public function testGetAttributes() { $controller = new AttributeController(); $request = new Request(); $controllerEvent = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); $event = new ControllerArgumentsEvent(new TestHttpKernel(), $controllerEvent, ['test'], new Request(), HttpKernelInterface::MAIN_REQUEST); $expected = [ Bar::class => [ new Bar('class'), new Bar('method'), ], Baz::class => [ new Baz(), ], ]; $this->assertEquals($expected, $event->getAttributes()); $attributes = [ new Bar('class'), new Bar('method'), new Bar('foo'), new Baz(), ]; $event->setController($controller, $attributes); $grouped = [ Bar::class => [ new Bar('class'), new Bar('method'), new Bar('foo'), ], Baz::class => [ new Baz(), ], ]; $this->assertEquals($grouped, $event->getAttributes()); $this->assertEquals($attributes, $event->getAttributes('*')); $this->assertSame($controllerEvent->getAttributes(), $event->getAttributes()); } public function testGetAttributesByClassName() { $controller = new AttributeController(); $request = new Request(); $controllerEvent = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); $event = new ControllerArgumentsEvent(new TestHttpKernel(), $controllerEvent, ['test'], new Request(), HttpKernelInterface::MAIN_REQUEST); $expected = [ new Bar('class'), new Bar('method'), ]; $this->assertEquals($expected, $event->getAttributes(Bar::class)); // When setting attributes, provide as flat list $flatAttributes = [ new Bar('class'), new Bar('method'), new Bar('foo'), ]; $event->setController($controller, $flatAttributes); $expectedAfterSet = [ new Bar('class'), new Bar('method'), new Bar('foo'), ]; $this->assertEquals($expectedAfterSet, $event->getAttributes(Bar::class)); $this->assertSame($controllerEvent->getAttributes(Bar::class), $event->getAttributes(Bar::class)); } public function testEvaluateWithClosureUsesNamedArguments() { $request = new Request(); $controller = [new AttributeController(), 'action']; $controllerEvent = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); $event = new ControllerArgumentsEvent(new TestHttpKernel(), $controllerEvent, ['value'], $request, HttpKernelInterface::MAIN_REQUEST); $closure = function (array $args, Request $requestArg, ?object $controllerArg) use ($request): string { $this->assertSame(['baz' => 'value'], $args); $this->assertSame($request, $requestArg); $this->assertInstanceOf(AttributeController::class, $controllerArg); return 'ok'; }; $this->assertSame('ok', $event->evaluate($closure, null)); } public function testEvaluateWithExpressionDelegatesToExpressionLanguage() { $request = new Request(); $controller = [new AttributeController(), 'action']; $controllerEvent = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); $event = new ControllerArgumentsEvent(new TestHttpKernel(), $controllerEvent, ['value'], $request, HttpKernelInterface::MAIN_REQUEST); $expressionLanguage = $this->createMock(ExpressionLanguage::class); $expressionLanguage->expects($this->once()) ->method('evaluate') ->with(new Expression('args["baz"]'), [ 'request' => $request, 'args' => ['baz' => 'value'], 'this' => $controller[0], ]) ->willReturn('value'); $this->assertSame('value', $event->evaluate(new Expression('args["baz"]'), $expressionLanguage)); } } ================================================ FILE: Tests/Event/ControllerAttributeEventTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Event; use PHPUnit\Framework\TestCase; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerArgumentsMetadata; use Symfony\Component\HttpKernel\Event\ControllerAttributeEvent; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController; use Symfony\Component\HttpKernel\Tests\TestHttpKernel; class ControllerAttributeEventTest extends TestCase { public function testEvaluateReturnsValueForNonExpressionOrClosure() { $controllerEvent = new ControllerEvent(new TestHttpKernel(), static function () {}, new Request(), HttpKernelInterface::MAIN_REQUEST); $event = new ControllerAttributeEvent(new \stdClass(), $controllerEvent); $this->assertSame('value', $event->evaluate('value')); } public function testEvaluateDelegatesToControllerEvent() { $request = new Request(); $controller = [new AttributeController(), 'action']; $controllerEvent = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); $expressionLanguage = $this->createMock(ExpressionLanguage::class); $expressionLanguage->expects($this->once()) ->method('evaluate') ->with(new Expression('request'), [ 'request' => $request, 'args' => [], 'this' => $controller[0], ]) ->willReturn($request); $event = new ControllerAttributeEvent(new \stdClass(), $controllerEvent, $expressionLanguage); $this->assertSame($request, $event->evaluate(new Expression('request'))); } public function testEvaluateDelegatesToControllerArgumentsEvent() { $request = new Request(); $controller = [new AttributeController(), 'action']; $controllerEvent = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); $argumentsEvent = new ControllerArgumentsEvent(new TestHttpKernel(), $controllerEvent, ['value'], $request, HttpKernelInterface::MAIN_REQUEST); $expressionLanguage = $this->createMock(ExpressionLanguage::class); $expressionLanguage->expects($this->once()) ->method('evaluate') ->with(new Expression('args["baz"]'), [ 'request' => $request, 'args' => ['baz' => 'value'], 'this' => $controller[0], ]) ->willReturn('value'); $event = new ControllerAttributeEvent(new \stdClass(), $argumentsEvent, $expressionLanguage); $this->assertSame('value', $event->evaluate(new Expression('args["baz"]'))); } public function testEvaluateDelegatesToControllerMetadata() { $request = new Request(); $controller = [new AttributeController(), 'action']; $kernel = new TestHttpKernel(); $controllerEvent = new ControllerEvent($kernel, $controller, $request, HttpKernelInterface::MAIN_REQUEST); $argumentsEvent = new ControllerArgumentsEvent($kernel, $controllerEvent, ['value'], $request, HttpKernelInterface::MAIN_REQUEST); $metadata = new ControllerArgumentsMetadata($controllerEvent, $argumentsEvent); $responseEvent = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new Response(), $metadata); $expressionLanguage = $this->createMock(ExpressionLanguage::class); $expressionLanguage->expects($this->once()) ->method('evaluate') ->with(new Expression('args["baz"]'), [ 'request' => $request, 'args' => ['baz' => 'value'], 'this' => $controller[0], ]) ->willReturn('value'); $event = new ControllerAttributeEvent(new \stdClass(), $responseEvent, $expressionLanguage); $this->assertSame('value', $event->evaluate(new Expression('args["baz"]'))); } } ================================================ FILE: Tests/Event/ControllerEventTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Event; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Bar; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Baz; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController; use Symfony\Component\HttpKernel\Tests\TestHttpKernel; class ControllerEventTest extends TestCase { #[DataProvider('provideGetAttributes')] public function testGetAttributes(callable $controller) { $event = new ControllerEvent(new TestHttpKernel(), $controller, new Request(), HttpKernelInterface::MAIN_REQUEST); $expected = [ Bar::class => [ new Bar('class'), new Bar('method'), ], Baz::class => [ new Baz(), ], ]; $this->assertEquals($expected, $event->getAttributes()); } #[DataProvider('provideGetAttributes')] public function testGetAttributesByClassName(callable $controller) { $event = new ControllerEvent(new TestHttpKernel(), $controller, new Request(), HttpKernelInterface::MAIN_REQUEST); $expected = [ new Bar('class'), new Bar('method'), ]; $this->assertEquals($expected, $event->getAttributes(Bar::class)); } #[DataProvider('provideGetAttributes')] public function testGetAttributesByInvalidClassName(callable $controller) { $event = new ControllerEvent(new TestHttpKernel(), $controller, new Request(), HttpKernelInterface::MAIN_REQUEST); $this->assertEquals([], $event->getAttributes(\stdClass::class)); } public function testControllerAttributesAreStoredInRequestAttributes() { $request = new Request(); $controller = [new AttributeController(), '__invoke']; $event = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); // Initially, no controller attributes should be in request $this->assertFalse($request->attributes->has('_controller_attributes')); // After calling getAttributes(), they should be stored in request attributes $attributes = $event->getAttributes(); $this->assertTrue($request->attributes->has('_controller_attributes')); $stored = $request->attributes->get('_controller_attributes'); $this->assertIsArray($stored); $this->assertCount(3, $stored); $this->assertIsArray($attributes); $this->assertArrayHasKey(Bar::class, $attributes); } public function testSetControllerWithAttributesStoresInRequest() { $request = new Request(); $controller = [new AttributeController(), '__invoke']; $event = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); // Provide attributes as flat list $customAttributes = [new Bar('custom')]; $event->setController($controller, $customAttributes); $stored = $request->attributes->get('_controller_attributes'); $this->assertIsArray($stored); $this->assertCount(1, $stored); $this->assertInstanceOf(Bar::class, $stored[0]); } #[IgnoreDeprecations] #[Group('legacy')] public function testSetControllerWithGroupedAttributesConvertsToFlat() { $request = new Request(); $controller = [new AttributeController(), '__invoke']; $event = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); $groupedAttributes = [Bar::class => [new Bar('custom')]]; $event->setController($controller, $groupedAttributes); $stored = $request->attributes->get('_controller_attributes'); $this->assertIsArray($stored); $this->assertCount(1, $stored); $this->assertInstanceOf(Bar::class, $stored[0]); } public function testSetControllerWithoutAttributesRemovesFromRequestWhenControllerChanges() { $request = new Request(); $controller1 = [new AttributeController(), '__invoke']; $controller2 = static fn () => new Response('test'); $event = new ControllerEvent(new TestHttpKernel(), $controller1, $request, HttpKernelInterface::MAIN_REQUEST); // First set some attributes $customAttributes = [new Bar('custom')]; $event->setController($controller1, $customAttributes); $this->assertEquals($customAttributes, $request->attributes->get('_controller_attributes')); // Then set different controller without attributes - should remove attributes $event->setController($controller2); $this->assertFalse($request->attributes->has('_controller_attributes')); } public static function provideGetAttributes() { yield [[new AttributeController(), '__invoke']]; yield [new AttributeController()]; yield [(new AttributeController())->__invoke(...)]; yield [#[Bar('class'), Bar('method'), Baz] static function () {}]; } public function testEvaluateWithClosureUsesArgsRequestAndController() { $request = new Request(); $controller = [new AttributeController(), 'action']; $event = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); $closure = function (array $args, Request $requestArg, ?object $controllerArg): string { $this->assertSame(['baz' => 'value'], $args); $this->assertInstanceOf(Request::class, $requestArg); $this->assertInstanceOf(AttributeController::class, $controllerArg); return 'ok'; }; $this->assertSame('ok', $event->evaluate($closure, null, ['baz' => 'value'])); } public function testEvaluateWithExpressionUsesExpressionLanguage() { $request = new Request(); $controller = [new AttributeController(), 'action']; $event = new ControllerEvent(new TestHttpKernel(), $controller, $request, HttpKernelInterface::MAIN_REQUEST); $expressionLanguage = $this->createMock(ExpressionLanguage::class); $expressionLanguage->expects($this->once()) ->method('evaluate') ->with(new Expression('args["baz"]'), [ 'request' => $request, 'args' => ['baz' => 'value'], 'this' => $controller[0], ]) ->willReturn('value'); $this->assertSame('value', $event->evaluate(new Expression('args["baz"]'), $expressionLanguage, ['baz' => 'value'])); } public function testEvaluateWithExpressionRequiresExpressionLanguage() { $event = new ControllerEvent(new TestHttpKernel(), static function () {}, new Request(), HttpKernelInterface::MAIN_REQUEST); $this->expectException(\LogicException::class); $this->expectExceptionMessage('Cannot evaluate Expression for controllers since no ExpressionLanguage service was configured.'); $event->evaluate(new Expression('args["foo"]'), null, ['foo' => 'bar']); } } ================================================ FILE: Tests/Event/ExceptionEventTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Event; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Tests\TestHttpKernel; class ExceptionEventTest extends TestCase { public function testAllowSuccessfulResponseIsFalseByDefault() { $event = new ExceptionEvent(new TestHttpKernel(), new Request(), 1, new \Exception()); $this->assertFalse($event->isAllowingCustomResponseCode()); } } ================================================ FILE: Tests/EventListener/AddRequestFormatsListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\EventListener\AddRequestFormatsListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; /** * @author Gildas Quemener */ class AddRequestFormatsListenerTest extends TestCase { private AddRequestFormatsListener $listener; protected function setUp(): void { $this->listener = new AddRequestFormatsListener(['csv' => ['text/csv', 'text/plain']]); } public function testIsAnEventSubscriber() { $this->assertInstanceOf(EventSubscriberInterface::class, $this->listener); } public function testRegisteredEvent() { $this->assertSame( [KernelEvents::REQUEST => ['onKernelRequest', 100]], AddRequestFormatsListener::getSubscribedEvents() ); } public function testSetAdditionalFormats() { $request = $this->createMock(Request::class); $event = new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); $request->expects($this->once()) ->method('setFormat') ->with('csv', ['text/csv', 'text/plain']); $this->listener->onKernelRequest($event); } } ================================================ FILE: Tests/EventListener/CacheAttributeListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\MockObject\Stub; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\Cache; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\EventListener\CacheAttributeListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\CacheAttributeController; class CacheAttributeListenerTest extends TestCase { private CacheAttributeListener $listener; private Response $response; private Cache $cache; private Request $request; private ResponseEvent $event; protected function setUp(): void { $this->listener = new CacheAttributeListener(); $this->response = new Response(); $this->cache = new Cache(); $this->request = $this->createRequest($this->cache); $this->event = $this->createEventMock($this->request, $this->response); } public function testWontReassignResponseWhenResponseIsUnsuccessful() { $response = $this->event->getResponse(); $this->response->setStatusCode(500); $this->listener->onKernelResponse($this->event); $this->assertSame($response, $this->event->getResponse()); } public function testWontReassignResponseWhenNoConfigurationIsPresent() { $response = $this->event->getResponse(); $this->request->attributes->remove('_cache'); $this->listener->onKernelResponse($this->event); $this->assertSame($response, $this->event->getResponse()); } public function testResponseIsPublicIfSharedMaxAgeSetAndPublicNotOverridden() { $request = $this->createRequest(new Cache(smaxage: 1)); $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); $this->assertTrue($this->response->headers->hasCacheControlDirective('public')); $this->assertFalse($this->response->headers->hasCacheControlDirective('private')); } public function testResponseIsPublicIfConfigurationIsPublicTrue() { $request = $this->createRequest(new Cache(public: true)); $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); $this->assertTrue($this->response->headers->hasCacheControlDirective('public')); $this->assertFalse($this->response->headers->hasCacheControlDirective('private')); } public function testResponseIsPrivateIfConfigurationIsPublicFalse() { $request = $this->createRequest(new Cache(public: false)); $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); $this->assertFalse($this->response->headers->hasCacheControlDirective('public')); $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); } public function testResponseIsPublicIfConfigurationIsPublicTrueNoStoreFalse() { $request = $this->createRequest(new Cache(public: true, noStore: false)); $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); $this->assertTrue($this->response->headers->hasCacheControlDirective('public')); $this->assertFalse($this->response->headers->hasCacheControlDirective('private')); $this->assertFalse($this->response->headers->hasCacheControlDirective('no-store')); } public function testResponseKeepPublicIfConfigurationIsPublicTrueNoStoreTrue() { $request = $this->createRequest(new Cache(public: true, noStore: true)); $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); $this->assertTrue($this->response->headers->hasCacheControlDirective('public')); $this->assertFalse($this->response->headers->hasCacheControlDirective('private')); $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); } public function testResponseKeepPrivateNoStoreIfConfigurationIsNoStoreTrue() { $request = $this->createRequest(new Cache(noStore: true)); $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); $this->assertFalse($this->response->headers->hasCacheControlDirective('public')); $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); } public function testResponseIsPublicIfSharedMaxAgeSetAndNoStoreIsTrue() { $request = $this->createRequest(new Cache(smaxage: 1, noStore: true)); $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); $this->assertTrue($this->response->headers->hasCacheControlDirective('public')); $this->assertFalse($this->response->headers->hasCacheControlDirective('private')); $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); } public function testResponseVary() { $vary = ['foobar']; $request = $this->createRequest(new Cache(vary: $vary)); $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); $this->assertTrue($this->response->hasVary()); $result = $this->response->getVary(); $this->assertSame($vary, $result); } public function testResponseVaryWhenVaryNotSet() { $request = $this->createRequest(new Cache()); $vary = ['foobar']; $this->response->setVary($vary); $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); $this->assertTrue($this->response->hasVary()); $result = $this->response->getVary(); $this->assertNotEmpty($result, 'Existing vary headers should not be removed'); $this->assertSame($vary, $result, 'Vary header should not be changed'); } public function testResponseIsPrivateIfConfigurationIsPublicNotSet() { $request = $this->createRequest(new Cache()); $this->listener->onKernelResponse($this->createEventMock($request, $this->response)); $this->assertFalse($this->response->headers->hasCacheControlDirective('public')); } public function testAttributeConfigurationsAreSetOnResponse() { $this->assertNull($this->response->getMaxAge()); $this->assertNull($this->response->getExpires()); $this->assertFalse($this->response->headers->hasCacheControlDirective('s-maxage')); $this->assertFalse($this->response->headers->hasCacheControlDirective('max-stale')); $this->assertFalse($this->response->headers->hasCacheControlDirective('stale-while-revalidate')); $this->assertFalse($this->response->headers->hasCacheControlDirective('stale-if-error')); $this->assertFalse($this->response->headers->hasCacheControlDirective('no-store')); $this->request->attributes->set('_cache', [new Cache( expires: 'tomorrow', maxage: '15', smaxage: '15', maxStale: '5', staleWhileRevalidate: '6', staleIfError: '7', noStore: true, )]); $this->listener->onKernelResponse($this->event); $this->assertSame(15, $this->response->getMaxAge()); $this->assertSame('15', $this->response->headers->getCacheControlDirective('s-maxage')); $this->assertSame('5', $this->response->headers->getCacheControlDirective('max-stale')); $this->assertSame('6', $this->response->headers->getCacheControlDirective('stale-while-revalidate')); $this->assertSame('7', $this->response->headers->getCacheControlDirective('stale-if-error')); $this->assertTrue($this->response->headers->hasCacheControlDirective('no-store')); $this->assertInstanceOf(\DateTimeInterface::class, $this->response->getExpires()); } public function testCacheMaxAgeSupportsStrtotimeFormat() { $this->request->attributes->set('_cache', [new Cache( maxage: '1 day', smaxage: '1 day', maxStale: '1 day', staleWhileRevalidate: '1 day', staleIfError: '1 day', )]); $this->listener->onKernelResponse($this->event); $this->assertSame('86400', $this->response->headers->getCacheControlDirective('s-maxage')); $this->assertSame(86400, $this->response->getMaxAge()); $this->assertSame('86400', $this->response->headers->getCacheControlDirective('max-stale')); $this->assertSame('86400', $this->response->headers->getCacheControlDirective('stale-if-error')); } #[TestWith(['test.getDate()'])] #[TestWith(['date'])] #[TestWith(['args["test"].getDate()'])] #[TestWith(['request.attributes.get("date")'])] public function testLastModifiedNotModifiedResponse(string $expression) { $entity = new TestEntity(); $request = $this->createRequest(new Cache(lastModified: $expression)); $request->attributes->set('date', new \DateTimeImmutable('Fri, 23 Aug 2013 00:00:00 GMT')); $request->headers->add(['If-Modified-Since' => 'Fri, 23 Aug 2013 00:00:00 GMT']); $listener = new CacheAttributeListener(); $controllerArgumentsEvent = new ControllerArgumentsEvent($this->getKernel(), static fn (TestEntity $test) => new Response(), [$entity], $request, null); $listener->onKernelControllerArguments($controllerArgumentsEvent); $response = $controllerArgumentsEvent->getController()($entity); $this->assertSame(304, $response->getStatusCode()); } #[TestWith(['test.getDate()'])] #[TestWith(['date'])] #[TestWith(['args["test"].getDate()'])] #[TestWith(['request.attributes.get("date")'])] public function testLastModifiedHeader(string $expression) { $entity = new TestEntity(); $request = $this->createRequest(new Cache(lastModified: $expression)); $request->attributes->set('date', new \DateTimeImmutable('Fri, 23 Aug 2013 00:00:00 GMT')); $listener = new CacheAttributeListener(); $controllerArgumentsEvent = new ControllerArgumentsEvent($this->getKernel(), static fn (TestEntity $test) => new Response(), [$entity], $request, null); $listener->onKernelControllerArguments($controllerArgumentsEvent); $controllerResponse = $controllerArgumentsEvent->getController()($entity); $responseEvent = new ResponseEvent($this->getKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $controllerResponse); $listener->onKernelResponse($responseEvent); $response = $responseEvent->getResponse(); $this->assertSame(200, $response->getStatusCode()); $this->assertTrue($response->headers->has('Last-Modified')); $this->assertSame('Fri, 23 Aug 2013 00:00:00 GMT', $response->headers->get('Last-Modified')); } #[TestWith(['test.getId()'])] #[TestWith(['id'])] #[TestWith(['args["test"].getId()'])] #[TestWith(['request.attributes.get("id")'])] public function testEtagNotModifiedResponse(string $expression) { $entity = new TestEntity(); $request = $this->createRequest(new Cache(etag: $expression)); $request->attributes->set('id', '12345'); $request->headers->add(['If-None-Match' => \sprintf('"%s"', hash('sha256', $entity->getId()))]); $listener = new CacheAttributeListener(); $controllerArgumentsEvent = new ControllerArgumentsEvent($this->getKernel(), static fn (TestEntity $test) => new Response(), [$entity], $request, null); $listener->onKernelControllerArguments($controllerArgumentsEvent); $response = $controllerArgumentsEvent->getController()($entity); $this->assertSame(304, $response->getStatusCode()); } #[TestWith(['test.getId()'])] #[TestWith(['id'])] #[TestWith(['args["test"].getId()'])] #[TestWith(['request.attributes.get("id")'])] public function testEtagHeader(string $expression) { $entity = new TestEntity(); $request = $this->createRequest(new Cache(etag: $expression)); $request->attributes->set('id', '12345'); $listener = new CacheAttributeListener(); $controllerArgumentsEvent = new ControllerArgumentsEvent($this->getKernel(), static fn (TestEntity $test) => new Response(), [$entity], $request, null); $listener->onKernelControllerArguments($controllerArgumentsEvent); $controllerResponse = $controllerArgumentsEvent->getController()($entity); $responseEvent = new ResponseEvent($this->getKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $controllerResponse); $listener->onKernelResponse($responseEvent); $response = $responseEvent->getResponse(); $this->assertSame(200, $response->getStatusCode()); $this->assertTrue($response->headers->has('Etag')); $this->assertStringContainsString(hash('sha256', $entity->getId()), $response->headers->get('Etag')); } #[DataProvider('provideLastModifiedHeaderAndEtagClosureCases')] public function testLastModifiedHeaderAndEtagHeadersClosures(\Closure $lastModifiedClosure, \Closure $etagClosure) { $entity = new TestEntity(); $request = $this->createRequest(new Cache(lastModified: $lastModifiedClosure, etag: $etagClosure)); $request->attributes->set('date', new \DateTimeImmutable('Fri, 23 Aug 2013 00:00:00 GMT')); $request->attributes->set('id', '12345'); $listener = new CacheAttributeListener(); $controllerArgumentsEvent = new ControllerArgumentsEvent($this->getKernel(), static fn (TestEntity $test) => new Response(), [$entity], $request, null); $listener->onKernelControllerArguments($controllerArgumentsEvent); $controllerResponse = $controllerArgumentsEvent->getController()($entity); $responseEvent = new ResponseEvent($this->getKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $controllerResponse); $listener->onKernelResponse($responseEvent); $response = $responseEvent->getResponse(); $this->assertSame(200, $response->getStatusCode()); $this->assertTrue($response->headers->has('Last-Modified')); $this->assertSame('Fri, 23 Aug 2013 00:00:00 GMT', $response->headers->get('Last-Modified')); $this->assertTrue($response->headers->has('Etag')); $this->assertStringContainsString(hash('sha256', $entity->getId()), $response->headers->get('Etag')); } public static function provideLastModifiedHeaderAndEtagClosureCases(): iterable { yield 'using arguments' => [ static fn (array $arguments, Request $request) => $arguments['test']->getDate(), static fn (array $arguments, Request $request) => $arguments['test']->getId(), ]; yield 'using request attributes' => [ static fn (array $arguments, Request $request) => $request->attributes->get('date'), static fn (array $arguments, Request $request) => $request->attributes->get('id'), ]; } public function testConfigurationDoesNotOverrideAlreadySetResponseHeaders() { $request = $this->createRequest(new Cache( expires: 'Fri, 24 Aug 2013 00:00:00 GMT', maxage: '15', smaxage: '15', vary: ['foobar'], lastModified: 'Fri, 24 Aug 2013 00:00:00 GMT', etag: '"12345"', )); $response = new Response(); $response->setEtag('"54321"'); $response->setLastModified(new \DateTimeImmutable('Fri, 23 Aug 2014 00:00:00 GMT')); $response->setExpires(new \DateTimeImmutable('Fri, 24 Aug 2014 00:00:00 GMT')); $response->setSharedMaxAge(30); $response->setMaxAge(30); $response->setVary(['foobaz']); $listener = new CacheAttributeListener(); $responseEvent = new ResponseEvent($this->getKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $response); $listener->onKernelResponse($responseEvent); $this->assertSame('"54321"', $response->getEtag()); $this->assertEquals(new \DateTimeImmutable('Fri, 23 Aug 2014 00:00:00 GMT'), $response->getLastModified()); $this->assertEquals(new \DateTimeImmutable('Fri, 24 Aug 2014 00:00:00 GMT'), $response->getExpires()); $this->assertSame('30', $response->headers->getCacheControlDirective('s-maxage')); $this->assertSame(30, $response->getMaxAge()); $this->assertSame(['foobaz'], $response->getVary()); } public function testAttribute() { $request = new Request(); $event = new ControllerArgumentsEvent($this->getKernel(), [new CacheAttributeController(), 'foo'], [], $request, null); $this->listener->onKernelControllerArguments($event); $response = new Response(); $event = new ResponseEvent($this->getKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $response); $this->listener->onKernelResponse($event); $this->assertSame(CacheAttributeController::METHOD_SMAXAGE, $response->getMaxAge()); $request = new Request(); $event = new ControllerArgumentsEvent($this->getKernel(), [new CacheAttributeController(), 'bar'], [], $request, null); $this->listener->onKernelControllerArguments($event); $response = new Response(); $event = new ResponseEvent($this->getKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $response); $this->listener->onKernelResponse($event); $this->assertSame(CacheAttributeController::CLASS_SMAXAGE, $response->getMaxAge()); } #[DataProvider('provideVaryHeaderScenarios')] public function testHasRelevantVaryHeaderBehavior(array $responseVary, array $cacheVary, bool $varyByLanguage, array $expectedVary) { $request = $this->createRequest(new Cache(vary: $cacheVary)); $request->attributes->set('_vary_by_language', $varyByLanguage); $response = new Response(); $response->setVary($responseVary); $listener = new CacheAttributeListener(); $event = new ResponseEvent($this->getKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $response); $listener->onKernelResponse($event); $this->assertSame($expectedVary, $response->getVary()); } public function testAttributeRespectsExplicitPrivateFromController() { $request = $this->createRequest(new Cache(public: true)); $response = new Response(); $response->setPrivate(); $this->listener->onKernelResponse($this->createEventMock($request, $response)); $this->assertTrue($response->headers->hasCacheControlDirective('private')); $this->assertFalse($response->headers->hasCacheControlDirective('public')); } public function testAttributeRespectsExplicitPublicFromController() { $request = $this->createRequest(new Cache(public: false)); $response = new Response(); $response->setPublic(); $this->listener->onKernelResponse($this->createEventMock($request, $response)); $this->assertTrue($response->headers->hasCacheControlDirective('public')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); } public static function provideVaryHeaderScenarios(): \Traversable { yield 'no vary headers at all' => [ 'responseVary' => [], 'cacheVary' => ['X-Foo'], 'varyByLanguage' => false, 'expectedVary' => ['X-Foo'], ]; yield 'response vary accept-language only, vary_by_language true (append new)' => [ 'responseVary' => ['Accept-Language'], 'cacheVary' => ['X-Bar'], 'varyByLanguage' => true, 'expectedVary' => ['Accept-Language', 'X-Bar'], // X-Bar is added ]; yield 'response vary accept-language only, vary_by_language false (no append)' => [ 'responseVary' => ['Accept-Language'], 'cacheVary' => ['X-Bar'], 'varyByLanguage' => false, 'expectedVary' => ['Accept-Language'], // no append ]; yield 'response vary multiple including accept-language, vary_by_language true (no append)' => [ 'responseVary' => ['Accept-Language', 'User-Agent'], 'cacheVary' => ['X-Baz'], 'varyByLanguage' => true, 'expectedVary' => ['Accept-Language', 'User-Agent'], // no append ]; yield 'cache vary is empty' => [ 'responseVary' => ['X-Existing'], 'cacheVary' => [], 'varyByLanguage' => true, 'expectedVary' => ['X-Existing'], // nothing to add ]; yield 'vary * (no append) — vary_by_language=true' => [ 'responseVary' => ['*'], 'cacheVary' => ['X-Foo'], 'varyByLanguage' => true, 'expectedVary' => ['*'], ]; yield 'vary * (no append) — vary_by_language=false' => [ 'responseVary' => ['*'], 'cacheVary' => ['X-Foo'], 'varyByLanguage' => false, 'expectedVary' => ['*'], ]; } #[DataProvider('provideCacheIfCases')] public function testCacheAppliedOnlyWhenIfEvaluatesToTrue(string|\Closure $if1, string|\Closure $if2, bool $sMaxAge, bool $public, bool $maxAge, bool $private) { $entity = new TestEntity(); $request = $this->createRequest( new Cache(smaxage: '1 days', public: true, if: $if1), new Cache(maxage: '10 days', public: false, if: $if2), ); $listener = new CacheAttributeListener(); $controllerArgumentsEvent = new ControllerArgumentsEvent($this->getKernel(), [new TestController(true), '__invoke'], [$entity], $request, null); $listener->onKernelControllerArguments($controllerArgumentsEvent); $controllerResponse = $controllerArgumentsEvent->getController()($entity); $responseEvent = new ResponseEvent($this->getKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $controllerResponse); $listener->onKernelResponse($responseEvent); $response = $responseEvent->getResponse(); $this->assertSame(200, $response->getStatusCode()); $this->assertSame($sMaxAge, $response->headers->hasCacheControlDirective('s-maxage')); $this->assertSame($public, $response->headers->hasCacheControlDirective('public')); $this->assertSame($maxAge, $response->headers->hasCacheControlDirective('max-age')); $this->assertSame($private, $response->headers->hasCacheControlDirective('private')); } public static function provideCacheIfCases(): iterable { yield 'expression' => [ 'args["test"].getId() <= 0', 'args["test"].getId() > 0', false, false, true, true, ]; yield 'expression accessing controller' => [ 'this.cache', 'not this.cache', true, true, false, false, ]; yield 'closure' => [ static fn (array $arguments, Request $request, ?object $controller) => $arguments['test']->getDate() <= new \DateTimeImmutable(), static fn (array $arguments, Request $request, ?object $controller) => $arguments['test']->getDate() > new \DateTimeImmutable(), true, true, false, false, ]; yield 'closure accessing controller' => [ static fn (array $arguments, Request $request, ?object $controller) => $controller->cache, static fn (array $arguments, Request $request, ?object $controller) => !$controller->cache, true, true, false, false, ]; } public function testErrorIsThrownWhenIfEvaluatesToNonBool() { $entity = new TestEntity(); $request = $this->createRequest( new Cache(smaxage: '1 days', public: true, if: static fn (array $arguments, Request $request) => 'foo'), ); $listener = new CacheAttributeListener(); $controllerArgumentsEvent = new ControllerArgumentsEvent($this->getKernel(), static fn (TestEntity $test) => new Response(), [$entity], $request, null); $this->expectException(\TypeError::class); $this->expectExceptionMessage(\sprintf('The value of the "$if" option of the "%s" attribute must evaluate to a boolean, "string" given.', Cache::class)); $listener->onKernelControllerArguments($controllerArgumentsEvent); } private function createRequest(Cache ...$cache): Request { return new Request([], [], ['_cache' => $cache]); } private function createEventMock(Request $request, Response $response): ResponseEvent { return new ResponseEvent($this->getKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $response); } private function getKernel(): Stub&HttpKernelInterface { return $this->createStub(HttpKernelInterface::class); } } class TestEntity { public function getDate() { return new \DateTimeImmutable('Fri, 23 Aug 2013 00:00:00 GMT'); } public function getId() { return '12345'; } } class TestController { public function __construct(public bool $cache) { } public function __invoke(TestEntity $test) { return new Response(); } } ================================================ FILE: Tests/EventListener/ControllerAttributesListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerArgumentsMetadata; use Symfony\Component\HttpKernel\Event\ControllerAttributeEvent; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\EventListener\ControllerAttributesListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Buz; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Qux; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ControllerAttributesController; class ControllerAttributesListenerTest extends TestCase { public function testOnKernelControllerArgumentsDispatchesEventsForEachAttribute() { $dispatchedEvents = []; $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS.'.'.Buz::class, static function ($event, $name) use (&$dispatchedEvents) { $dispatchedEvents[] = $name; }); $dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS.'.'.Qux::class, static function ($event, $name) use (&$dispatchedEvents) { $dispatchedEvents[] = $name; }); $listener = $this->createListener(); $event = $this->createControllerArgumentsEvent('buzQuxAction'); $listener->beforeController($event, KernelEvents::CONTROLLER_ARGUMENTS, $dispatcher); $this->assertSame([ KernelEvents::CONTROLLER_ARGUMENTS.'.'.Buz::class, KernelEvents::CONTROLLER_ARGUMENTS.'.'.Buz::class, KernelEvents::CONTROLLER_ARGUMENTS.'.'.Qux::class, ], $dispatchedEvents); } public function testOnKernelResponseDispatchesEventsInReverseOrder() { $dispatchedEvents = []; $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::RESPONSE.'.'.Buz::class, static function ($event, $name) use (&$dispatchedEvents) { $dispatchedEvents[] = $name; }); $dispatcher->addListener(KernelEvents::RESPONSE.'.'.Qux::class, static function ($event, $name) use (&$dispatchedEvents) { $dispatchedEvents[] = $name; }); $listener = $this->createListener(); $event = $this->createResponseEvent('buzQuxAction'); $listener->afterController($event, KernelEvents::RESPONSE, $dispatcher); $this->assertSame([ KernelEvents::RESPONSE.'.'.Qux::class, KernelEvents::RESPONSE.'.'.Buz::class, KernelEvents::RESPONSE.'.'.Buz::class, ], $dispatchedEvents); } public function testOnKernelResponseDoesNothingWhenNoControllerEvent() { $dispatchedEvents = []; $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::RESPONSE.'.'.Buz::class, static function ($event, $name) use (&$dispatchedEvents) { $dispatchedEvents[] = $name; }); $listener = $this->createListener(); $event = new ResponseEvent( $this->createStub(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST, new Response() ); $listener->afterController($event, KernelEvents::RESPONSE, $dispatcher); $this->assertSame([], $dispatchedEvents); } public function testDispatchedEventIsTheSameInstance() { $capturedEvents = []; $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS.'.'.Buz::class, static function (ControllerAttributeEvent $event) use (&$capturedEvents) { $capturedEvents[] = $event; }); $listener = $this->createListener(); $event = $this->createControllerArgumentsEvent('buzAction'); $listener->beforeController($event, KernelEvents::CONTROLLER_ARGUMENTS, $dispatcher); $this->assertCount(2, $capturedEvents); $this->assertSame($event, $capturedEvents[0]->kernelEvent); $this->assertSame($event, $capturedEvents[1]->kernelEvent); } public function testClassLevelAttributesAreIncluded() { $dispatchedEvents = []; $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS.'.'.Buz::class, static function ($event, $name) use (&$dispatchedEvents) { $dispatchedEvents[] = $name; }); $listener = $this->createListener(); $event = $this->createControllerArgumentsEvent('noAttributeAction'); $listener->beforeController($event, KernelEvents::CONTROLLER_ARGUMENTS, $dispatcher); $this->assertSame([KernelEvents::CONTROLLER_ARGUMENTS.'.'.Buz::class], $dispatchedEvents); } public function testBeforeControllerDispatchesParentAttributeListeners() { $dispatchedEvents = []; $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS.'.'.Buz::class, static function ($event, $name) use (&$dispatchedEvents) { $dispatchedEvents[] = $name; }); $listener = $this->createListener([ KernelEvents::CONTROLLER_ARGUMENTS => [ Buz::class => true, ], ]); $event = $this->createControllerArgumentsEvent('subBuzAction'); $listener->beforeController($event, KernelEvents::CONTROLLER_ARGUMENTS, $dispatcher); $this->assertSame([ KernelEvents::CONTROLLER_ARGUMENTS.'.'.Buz::class, KernelEvents::CONTROLLER_ARGUMENTS.'.'.Buz::class, ], $dispatchedEvents); } public function testBeforeControllerSkipsWhenNoAttributeListenersAreRegistered() { $dispatchedEvents = []; $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS.'.'.Buz::class, static function () use (&$dispatchedEvents) { $dispatchedEvents[] = true; }); $listener = $this->createListener([]); $event = $this->createControllerArgumentsEvent('buzAction'); $listener->beforeController($event, KernelEvents::CONTROLLER_ARGUMENTS, $dispatcher); $this->assertEmpty($dispatchedEvents); } public function testOnKernelControllerHandlesControllerChanges() { $dispatchedEvents = []; $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::CONTROLLER.'.'.Buz::class, static function ($event, $name) use (&$dispatchedEvents) { $dispatchedEvents[] = $name; }); $dispatcher->addListener(KernelEvents::CONTROLLER.'.'.Qux::class, static function ($event, $name) use (&$dispatchedEvents) { $dispatchedEvents[] = $name; }); $originalControllerSet = false; $dispatcher->addListener(KernelEvents::CONTROLLER.'.'.Buz::class, static function (ControllerAttributeEvent $event) use (&$originalControllerSet, &$dispatchedEvents) { if (!$originalControllerSet) { $event->kernelEvent->setController([new ControllerAttributesController(), 'buzQuxAction']); $originalControllerSet = true; } }, -1); $listener = $this->createListener(); $event = new ControllerEvent( $this->createStub(HttpKernelInterface::class), [new ControllerAttributesController(), 'buzAction'], new Request(), HttpKernelInterface::MAIN_REQUEST ); $listener->beforeController($event, KernelEvents::CONTROLLER, $dispatcher); $this->assertContains(KernelEvents::CONTROLLER.'.'.Qux::class, $dispatchedEvents); } public function testOnKernelControllerArgumentsHandlesControllerChanges() { $dispatchedEvents = []; $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS.'.'.Buz::class, static function ($event, $name) use (&$dispatchedEvents) { $dispatchedEvents[] = $name; }); $dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS.'.'.Qux::class, static function ($event, $name) use (&$dispatchedEvents) { $dispatchedEvents[] = $name; }); $listener = $this->createListener(); $event = $this->createControllerArgumentsEvent('buzAction'); $originalControllerSet = false; $dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS.'.'.Buz::class, static function (ControllerAttributeEvent $event) use (&$originalControllerSet, &$dispatchedEvents) { if (!$originalControllerSet) { $event->kernelEvent->setController([new ControllerAttributesController(), 'buzQuxAction']); $originalControllerSet = true; } }, -1); $listener->beforeController($event, KernelEvents::CONTROLLER_ARGUMENTS, $dispatcher); $this->assertContains(KernelEvents::CONTROLLER_ARGUMENTS.'.'.Qux::class, $dispatchedEvents); } private function createListener(?array $attributesWithListenersByEvent = null): ControllerAttributesListener { return new ControllerAttributesListener($attributesWithListenersByEvent ?? [ KernelEvents::CONTROLLER => [ Buz::class => true, Qux::class => true, ], KernelEvents::CONTROLLER_ARGUMENTS => [ Buz::class => true, Qux::class => true, ], KernelEvents::VIEW => [ Buz::class => true, Qux::class => true, ], KernelEvents::RESPONSE => [ Buz::class => true, Qux::class => true, ], KernelEvents::FINISH_REQUEST => [ Buz::class => true, Qux::class => true, ], ]); } private function createControllerArgumentsEvent(string $method): ControllerArgumentsEvent { return new ControllerArgumentsEvent( $this->createStub(HttpKernelInterface::class), [new ControllerAttributesController(), $method], [], new Request(), HttpKernelInterface::MAIN_REQUEST ); } private function createResponseEvent(string $method): ResponseEvent { $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $controller = [new ControllerAttributesController(), $method]; $controllerEvent = new ControllerEvent($kernel, $controller, $request, HttpKernelInterface::MAIN_REQUEST); $controllerArgumentsEvent = new ControllerArgumentsEvent($kernel, $controllerEvent, [], $request, HttpKernelInterface::MAIN_REQUEST); $controllerMetadata = new ControllerArgumentsMetadata($controllerEvent, $controllerArgumentsEvent); return new ResponseEvent( $kernel, $request, HttpKernelInterface::MAIN_REQUEST, new Response(), $controllerMetadata ); } } ================================================ FILE: Tests/EventListener/DebugHandlersListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\KernelEvent; use Symfony\Component\HttpKernel\EventListener\DebugHandlersListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; /** * @author Nicolas Grekas */ class DebugHandlersListenerTest extends TestCase { public function testConfigure() { $userHandler = static fn () => null; $listener = new DebugHandlersListener($userHandler); $eHandler = new ErrorHandler(); $exception = null; set_error_handler([$eHandler, 'handleError']); set_exception_handler([$eHandler, 'handleException']); try { $listener->configure(); } catch (\Exception $exception) { } finally { restore_exception_handler(); restore_error_handler(); } if (null !== $exception) { throw $exception; } $this->assertSame($userHandler, $eHandler->setExceptionHandler('var_dump')); } public function testConfigureForHttpKernelWithNoTerminateWithException() { $listener = new DebugHandlersListener(null); $eHandler = new ErrorHandler(); $event = new KernelEvent( $this->createStub(HttpKernelInterface::class), Request::create('/'), HttpKernelInterface::MAIN_REQUEST ); $exception = null; $h = set_exception_handler([$eHandler, 'handleException']); try { $listener->configure($event); } catch (\Exception $exception) { } restore_exception_handler(); if (null !== $exception) { throw $exception; } $this->assertNull($h); } public function testConsoleEvent() { $dispatcher = new EventDispatcher(); $listener = new DebugHandlersListener(null); $app = $this->createMock(Application::class); $app->expects($this->once())->method('getHelperSet')->willReturn(new HelperSet()); $command = new Command(__FUNCTION__); $command->setApplication($app); $event = new ConsoleEvent($command, new ArgvInput(), new ConsoleOutput()); $dispatcher->addSubscriber($listener); $xListeners = [ KernelEvents::REQUEST => [[$listener, 'configure']], ConsoleEvents::COMMAND => [[$listener, 'configure']], ]; $this->assertSame($xListeners, $dispatcher->getListeners()); $exception = null; $eHandler = new ErrorHandler(); set_error_handler([$eHandler, 'handleError']); set_exception_handler([$eHandler, 'handleException']); try { $dispatcher->dispatch($event, ConsoleEvents::COMMAND); } catch (\Exception $exception) { } finally { restore_exception_handler(); restore_error_handler(); } if (null !== $exception) { throw $exception; } $xHandler = $eHandler->setExceptionHandler('var_dump'); $this->assertInstanceOf(\Closure::class, $xHandler); $app->expects($this->once()) ->method('renderThrowable'); $xHandler(new \Exception()); } public function testReplaceExistingExceptionHandler() { $userHandler = static function () {}; $listener = new DebugHandlersListener($userHandler); $eHandler = new ErrorHandler(); $eHandler->setExceptionHandler('var_dump'); $exception = null; set_exception_handler([$eHandler, 'handleException']); try { $listener->configure(); } catch (\Exception $exception) { } restore_exception_handler(); if (null !== $exception) { throw $exception; } $this->assertSame($userHandler, $eHandler->setExceptionHandler('var_dump')); } } ================================================ FILE: Tests/EventListener/DisallowRobotsIndexingListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\EventListener\DisallowRobotsIndexingListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelInterface; class DisallowRobotsIndexingListenerTest extends TestCase { #[DataProvider('provideResponses')] public function testInvoke(?string $expected, array $responseArgs) { $response = new Response(...$responseArgs); $listener = new DisallowRobotsIndexingListener(); $event = new ResponseEvent($this->createStub(HttpKernelInterface::class), new Request(), KernelInterface::MAIN_REQUEST, $response); $listener->onResponse($event); $this->assertSame($expected, $response->headers->get('X-Robots-Tag'), 'Header doesn\'t match expectations'); } public static function provideResponses(): iterable { yield 'No header' => ['noindex', []]; yield 'Header already set' => [ 'something else', ['', 204, ['X-Robots-Tag' => 'something else']], ]; } } ================================================ FILE: Tests/EventListener/DumpListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\HttpKernel\EventListener\DumpListener; use Symfony\Component\VarDumper\Cloner\ClonerInterface; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Dumper\DataDumperInterface; use Symfony\Component\VarDumper\VarDumper; /** * DumpListenerTest. * * @author Nicolas Grekas */ class DumpListenerTest extends TestCase { public function testSubscribedEvents() { $this->assertSame( [ConsoleEvents::COMMAND => ['configure', 1024]], DumpListener::getSubscribedEvents() ); } public function testConfigure() { $prevDumper = VarDumper::setHandler('var_dump'); VarDumper::setHandler($prevDumper); $cloner = new MockCloner(); $dumper = new MockDumper(); ob_start(); $exception = null; $listener = new DumpListener($cloner, $dumper); try { $listener->configure(); VarDumper::dump('foo'); VarDumper::dump('bar'); $this->assertSame('+foo-+bar-', ob_get_clean()); } catch (\Exception $exception) { } VarDumper::setHandler($prevDumper); if (null !== $exception) { throw $exception; } } #[TestWith([false, false, '+foo-+bar-', []])] #[TestWith([true, false, '+foo-+bar-', []])] #[TestWith([true, true, '', ['foo-', 'bar-']])] public function testConfigureWithProfilerDumper(bool $hasOption, bool $option, string $expectedOutput, array $expectedData) { $prevDumper = VarDumper::setHandler('var_dump'); VarDumper::setHandler($prevDumper); $cloner = new MockCloner(); $dumper = new MockDumper(); $profilerDumper = new MockProfilerDumper(); $input = $this->createStub(InputInterface::class); $input->method('hasOption')->willReturn($hasOption); $input->method('getOption')->willReturn($option); ob_start(); $exception = null; $listener = new DumpListener($cloner, $dumper, null, $profilerDumper); try { $listener->configure(new ConsoleCommandEvent(null, $input, new BufferedOutput())); VarDumper::dump('foo'); VarDumper::dump('bar'); $this->assertSame($expectedOutput, ob_get_clean()); $this->assertSame($expectedData, $profilerDumper->data); } catch (\Exception $exception) { } VarDumper::setHandler($prevDumper); if (null !== $exception) { throw $exception; } } } class MockCloner implements ClonerInterface { public function cloneVar($var): Data { return new Data([[$var.'-']]); } } class MockDumper implements DataDumperInterface { public function dump(Data $data): ?string { echo '+'.$data->getValue(); return null; } } class MockProfilerDumper implements DataDumperInterface { public array $data = []; public function dump(Data $data): ?string { $this->data[] = $data->getValue(); return null; } } ================================================ FILE: Tests/EventListener/ErrorListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Psr\Log\LogLevel; use Psr\Log\NullLogger; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\WithHttpStatus; use Symfony\Component\HttpKernel\Attribute\WithLogLevel; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\EventListener\ErrorListener; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; use Symfony\Component\HttpKernel\Tests\Logger; /** * @author Robert Schönthal */ #[Group('time-sensitive')] class ErrorListenerTest extends TestCase { public function testConstruct() { $logger = new TestLogger(); $l = new ErrorListener('foo', $logger); $_logger = new \ReflectionProperty($l::class, 'logger'); $_controller = new \ReflectionProperty($l::class, 'controller'); $this->assertSame($logger, $_logger->getValue($l)); $this->assertSame('foo', $_controller->getValue($l)); } #[DataProvider('provider')] public function testHandleWithoutLogger($event, $event2) { $initialErrorLog = ini_set('error_log', file_exists('/dev/null') ? '/dev/null' : 'nul'); try { $l = new ErrorListener('foo'); $l->logKernelException($event); $l->onKernelException($event); $this->assertEquals(new Response('foo'), $event->getResponse()); try { $l->logKernelException($event2); $l->onKernelException($event2); $this->fail('RuntimeException expected'); } catch (\RuntimeException $e) { $this->assertSame('bar', $e->getMessage()); $this->assertSame('foo', $e->getPrevious()->getMessage()); } } finally { ini_set('error_log', $initialErrorLog); } } #[DataProvider('provider')] public function testHandleWithLogger($event, $event2) { $logger = new TestLogger(); $l = new ErrorListener('foo', $logger); $l->logKernelException($event); $l->onKernelException($event); $this->assertEquals(new Response('foo'), $event->getResponse()); try { $l->logKernelException($event2); $l->onKernelException($event2); $this->fail('RuntimeException expected'); } catch (\RuntimeException $e) { $this->assertSame('bar', $e->getMessage()); $this->assertSame('foo', $e->getPrevious()->getMessage()); } $this->assertEquals(3, $logger->countErrors()); $logs = $logger->getLogsForLevel('critical'); $this->assertCount(3, $logs); $this->assertStringStartsWith('Uncaught PHP Exception Exception: "foo" at ErrorListenerTest.php line', $logs[0]); $this->assertStringStartsWith('Uncaught PHP Exception Exception: "foo" at ErrorListenerTest.php line', $logs[1]); $this->assertStringStartsWith('Exception thrown when handling an exception (RuntimeException: bar at ErrorListenerTest.php line', $logs[2]); } public function testHandleWithLoggerAndCustomConfiguration() { $request = new Request(); $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new \RuntimeException('bar')); $logger = new TestLogger(); $l = new ErrorListener('not used', $logger, false, [ \RuntimeException::class => [ 'log_level' => 'warning', 'status_code' => 401, ], ]); $l->logKernelException($event); $l->onKernelException($event); $this->assertEquals(new Response('foo', 401), $event->getResponse()); $this->assertEquals(0, $logger->countErrors()); $this->assertCount(0, $logger->getLogsForLevel('critical')); $this->assertCount(1, $logger->getLogsForLevel('warning')); } public function testHandleWithLogLevelAttribute() { $request = new Request(); $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new ChildOfWarningWithLogLevelAttribute()); $logger = new TestLogger(); $l = new ErrorListener('not used', $logger); $l->logKernelException($event); $l->onKernelException($event); $this->assertEquals(0, $logger->countErrors()); $this->assertCount(0, $logger->getLogsForLevel('critical')); $this->assertCount(1, $logger->getLogsForLevel('warning')); } public function testHandleWithLogChannel() { $request = new Request(); $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new \RuntimeException('bar')); $defaultLogger = new TestLogger(); $channelLoger = new TestLogger(); $l = new ErrorListener('not used', $defaultLogger, false, [ \RuntimeException::class => [ 'log_level' => 'warning', 'status_code' => 401, 'log_channel' => 'channel', ], \Exception::class => [ 'log_level' => 'error', 'status_code' => 402, ], ], ['channel' => $channelLoger]); $l->logKernelException($event); $l->onKernelException($event); $this->assertCount(0, $defaultLogger->getLogsForLevel('error')); $this->assertCount(0, $defaultLogger->getLogsForLevel('warning')); $this->assertCount(0, $channelLoger->getLogsForLevel('error')); $this->assertCount(1, $channelLoger->getLogsForLevel('warning')); } public function testHandleWithLoggerChannelNotUsed() { $request = new Request(); $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new \RuntimeException('bar')); $defaultLogger = new TestLogger(); $channelLoger = new TestLogger(); $l = new ErrorListener('not used', $defaultLogger, false, [ \RuntimeException::class => [ 'log_level' => 'warning', 'status_code' => 401, ], \ErrorException::class => [ 'log_level' => 'error', 'status_code' => 402, 'log_channel' => 'channel', ], ], ['channel' => $channelLoger]); $l->logKernelException($event); $l->onKernelException($event); $this->assertSame(0, $defaultLogger->countErrors()); $this->assertCount(0, $defaultLogger->getLogsForLevel('critical')); $this->assertCount(1, $defaultLogger->getLogsForLevel('warning')); $this->assertCount(0, $channelLoger->getLogsForLevel('warning')); $this->assertCount(0, $channelLoger->getLogsForLevel('error')); $this->assertCount(0, $channelLoger->getLogsForLevel('critical')); } public function testHandleClassImplementingInterfaceWithLogLevelAttribute() { $request = new Request(); $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new ImplementingInterfaceWithLogLevelAttribute()); $logger = new TestLogger(); $l = new ErrorListener('not used', $logger); $l->logKernelException($event); $l->onKernelException($event); $this->assertEquals(0, $logger->countErrors()); $this->assertCount(0, $logger->getLogsForLevel('critical')); $this->assertCount(1, $logger->getLogsForLevel('warning')); } public function testHandleWithLogLevelAttributeAndCustomConfiguration() { $request = new Request(); $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new WarningWithLogLevelAttribute()); $logger = new TestLogger(); $l = new ErrorListener('not used', $logger, false, [ WarningWithLogLevelAttribute::class => [ 'log_level' => 'info', 'status_code' => 401, ], ]); $l->logKernelException($event); $l->onKernelException($event); $this->assertEquals(0, $logger->countErrors()); $this->assertCount(0, $logger->getLogsForLevel('warning')); $this->assertCount(1, $logger->getLogsForLevel('info')); } #[DataProvider('exceptionWithAttributeProvider')] public function testHandleHttpAttribute(\Throwable $exception, int $expectedStatusCode, array $expectedHeaders) { $request = new Request(); $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $exception); $l = new ErrorListener('not used'); $l->logKernelException($event); $l->onKernelException($event); $this->assertEquals(new Response('foo', $expectedStatusCode, $expectedHeaders), $event->getResponse()); } public function testHandleCustomConfigurationAndHttpAttribute() { $request = new Request(); $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new WithGeneralAttribute()); $l = new ErrorListener('not used', null, false, [ WithGeneralAttribute::class => [ 'log_level' => 'warning', 'status_code' => 401, ], ]); $l->logKernelException($event); $l->onKernelException($event); $this->assertEquals(new Response('foo', 401), $event->getResponse()); } public static function provider() { if (!class_exists(Request::class)) { return [[null, null]]; } $request = new Request(); $exception = new \Exception('foo'); $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, $exception); $event2 = new ExceptionEvent(new TestKernelThatThrowsException(), $request, HttpKernelInterface::MAIN_REQUEST, $exception); return [ [$event, $event2], ]; } public function testSubRequestFormat() { $listener = new ErrorListener('foo', new NullLogger()); $kernel = $this->createMock(HttpKernelInterface::class); $kernel->expects($this->once())->method('handle')->willReturnCallback(static fn (Request $request) => new Response($request->getRequestFormat())); $request = Request::create('/'); $request->setRequestFormat('xml'); $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new \Exception('foo')); $listener->onKernelException($event); $response = $event->getResponse(); $this->assertEquals('xml', $response->getContent()); } public function testCSPHeaderIsRemoved() { $dispatcher = new EventDispatcher(); $kernel = $this->createMock(HttpKernelInterface::class); $kernel->expects($this->once())->method('handle')->willReturnCallback(static fn (Request $request) => new Response($request->getRequestFormat())); $listener = new ErrorListener('foo', new NullLogger(), true); $dispatcher->addSubscriber($listener); $request = Request::create('/'); $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new \Exception('foo')); $dispatcher->dispatch($event, KernelEvents::EXCEPTION); $response = new Response('', 200, ['content-security-policy' => "style-src 'self'"]); $this->assertTrue($response->headers->has('content-security-policy')); $event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertFalse($response->headers->has('content-security-policy'), 'CSP header has been removed'); } public function testTerminating() { $listener = new ErrorListener('foo', new NullLogger()); $kernel = $this->createMock(HttpKernelInterface::class); $kernel->expects($this->never())->method('handle'); $request = Request::create('/'); $event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new \Exception('foo'), true); $listener->onKernelException($event); } #[DataProvider('controllerProvider')] public function testOnControllerArguments(callable $controller) { $listener = new ErrorListener($controller, new NullLogger(), true); $kernel = $this->createStub(HttpKernelInterface::class); $kernel->method('handle')->willReturnCallback(function (Request $request) use ($listener, $controller, $kernel) { $this->assertSame($controller, $request->attributes->get('_controller')); $arguments = (new ArgumentResolver())->getArguments($request, $controller); $event = new ControllerArgumentsEvent($kernel, $controller, $arguments, $request, HttpKernelInterface::SUB_REQUEST); $listener->onControllerArguments($event); return $controller(...$event->getArguments()); }); $event = new ExceptionEvent($kernel, Request::create('/'), HttpKernelInterface::MAIN_REQUEST, new \Exception('foo')); $listener->onKernelException($event); $this->assertSame('OK: foo', $event->getResponse()->getContent()); } public static function controllerProvider() { yield [static fn (FlattenException $exception) => new Response('OK: '.$exception->getMessage())]; yield [static function ($exception) { static::assertInstanceOf(FlattenException::class, $exception); return new Response('OK: '.$exception->getMessage()); }]; yield [static fn (\Throwable $exception) => new Response('OK: '.$exception->getMessage())]; } public static function exceptionWithAttributeProvider() { yield [new WithCustomUserProvidedAttribute(), 208, ['name' => 'value']]; yield [new WithGeneralAttribute(), 412, ['some' => 'thing']]; yield [new ChildOfWithGeneralAttribute(), 412, ['some' => 'thing']]; yield [new ImplementingInterfaceWithGeneralAttribute(), 412, ['some' => 'thing']]; } } class TestLogger extends Logger implements DebugLoggerInterface { public function countErrors(?Request $request = null): int { return \count($this->logs['critical']); } public function getLogs(?Request $request = null): array { return []; } } class TestKernel implements HttpKernelInterface { public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response { $e = $request->attributes->get('exception'); if ($e instanceof HttpExceptionInterface) { return new Response('foo', $e->getStatusCode(), $e->getHeaders()); } return new Response('foo'); } } class TestKernelThatThrowsException implements HttpKernelInterface { public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response { throw new \RuntimeException('bar'); } } #[\Attribute(\Attribute::TARGET_CLASS)] class UserProvidedHttpStatusCodeAttribute extends WithHttpStatus { public function __construct(array $headers = []) { parent::__construct( Response::HTTP_ALREADY_REPORTED, $headers ); } } #[UserProvidedHttpStatusCodeAttribute(headers: [ 'name' => 'value', ])] class WithCustomUserProvidedAttribute extends \Exception { } #[WithHttpStatus( statusCode: Response::HTTP_PRECONDITION_FAILED, headers: [ 'some' => 'thing', ] )] class WithGeneralAttribute extends \Exception { } #[WithHttpStatus( statusCode: Response::HTTP_PRECONDITION_FAILED, headers: [ 'some' => 'thing', ] )] interface InterfaceWithGeneralAttribute { } class ImplementingInterfaceWithGeneralAttribute extends \Exception implements InterfaceWithGeneralAttribute { } class ChildOfWithGeneralAttribute extends WithGeneralAttribute { } #[WithLogLevel(LogLevel::WARNING)] class WarningWithLogLevelAttribute extends \Exception { } class ChildOfWarningWithLogLevelAttribute extends WarningWithLogLevelAttribute { } #[WithLogLevel(LogLevel::WARNING)] interface InterfaceWithLogLevelAttribute { } class ImplementingInterfaceWithLogLevelAttribute extends \Exception implements InterfaceWithLogLevelAttribute { } ================================================ FILE: Tests/EventListener/FragmentListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\EventListener\FragmentListener; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; class FragmentListenerTest extends TestCase { public function testOnlyTriggeredOnFragmentRoute() { $request = Request::create('http://example.com/foo?_path=foo%3Dbar%26_controller%3Dfoo'); $listener = new FragmentListener(new UriSigner('foo')); $event = $this->createRequestEvent($request); $expected = $request->attributes->all(); $listener->onKernelRequest($event); $this->assertEquals($expected, $request->attributes->all()); $this->assertTrue($request->query->has('_path')); } public function testOnlyTriggeredIfControllerWasNotDefinedYet() { $request = Request::create('http://example.com/_fragment?_path=foo%3Dbar%26_controller%3Dfoo'); $request->attributes->set('_controller', 'bar'); $listener = new FragmentListener(new UriSigner('foo')); $event = $this->createRequestEvent($request, HttpKernelInterface::SUB_REQUEST); $expected = $request->attributes->all(); $listener->onKernelRequest($event); $this->assertEquals($expected, $request->attributes->all()); } public function testAccessDeniedWithNonSafeMethods() { $this->expectException(AccessDeniedHttpException::class); $request = Request::create('http://example.com/_fragment', 'POST'); $listener = new FragmentListener(new UriSigner('foo')); $event = $this->createRequestEvent($request); $listener->onKernelRequest($event); } public function testAccessDeniedWithWrongSignature() { $this->expectException(AccessDeniedHttpException::class); $request = Request::create('http://example.com/_fragment', 'GET', [], [], [], ['REMOTE_ADDR' => '10.0.0.1']); $listener = new FragmentListener(new UriSigner('foo')); $event = $this->createRequestEvent($request); $listener->onKernelRequest($event); } public function testWithSignature() { $signer = new UriSigner('foo'); $request = Request::create($signer->sign('http://example.com/_fragment?_path=foo%3Dbar%26_controller%3Dfoo'), 'GET', [], [], [], ['REMOTE_ADDR' => '10.0.0.1']); $listener = new FragmentListener($signer); $event = $this->createRequestEvent($request); $listener->onKernelRequest($event); $this->assertEquals(['foo' => 'bar', '_controller' => 'foo', '_check_controller_is_allowed' => -1], $request->attributes->get('_route_params')); $this->assertFalse($request->query->has('_path')); } public function testRemovesPathWithControllerDefined() { $request = Request::create('http://example.com/_fragment?_path=foo%3Dbar%26_controller%3Dfoo'); $listener = new FragmentListener(new UriSigner('foo')); $event = $this->createRequestEvent($request, HttpKernelInterface::SUB_REQUEST); $listener->onKernelRequest($event); $this->assertFalse($request->query->has('_path')); } public function testRemovesPathWithControllerNotDefined() { $signer = new UriSigner('foo'); $request = Request::create($signer->sign('http://example.com/_fragment?_path=foo%3Dbar'), 'GET', [], [], [], ['REMOTE_ADDR' => '10.0.0.1']); $listener = new FragmentListener($signer); $event = $this->createRequestEvent($request); $listener->onKernelRequest($event); $this->assertFalse($request->query->has('_path')); } private function createRequestEvent(Request $request, int $requestType = HttpKernelInterface::MAIN_REQUEST): RequestEvent { return new RequestEvent($this->createStub(HttpKernelInterface::class), $request, $requestType); } } ================================================ FILE: Tests/EventListener/IsSignatureValidAttributeListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Exception\UnsignedUriException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\HttpKernel\Attribute\IsSignatureValid; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerAttributeEvent; use Symfony\Component\HttpKernel\EventListener\IsSignatureValidAttributeListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Tests\Fixtures\IsSignatureValidAttributeController; use Symfony\Component\HttpKernel\Tests\Fixtures\IsSignatureValidAttributeMethodsController; class IsSignatureValidAttributeListenerTest extends TestCase { public function testSubscribedEvents() { $events = IsSignatureValidAttributeListener::getSubscribedEvents(); $eventName = KernelEvents::CONTROLLER_ARGUMENTS.'.'.IsSignatureValid::class; $this->assertArrayHasKey($eventName, $events); $this->assertSame('onKernelControllerAttribute', $events[$eventName]); } public function testControllerAttributeEventValidatesSignature() { $request = new Request(); $kernel = $this->createStub(HttpKernelInterface::class); $controllerEvent = new ControllerArgumentsEvent( $kernel, [new IsSignatureValidAttributeController(), '__invoke'], [], $request, HttpKernelInterface::MAIN_REQUEST ); $signer = $this->createMock(UriSigner::class); $signer->expects($this->once())->method('verify')->with($request); $listener = new IsSignatureValidAttributeListener($signer); $listener->onKernelControllerAttribute(new ControllerAttributeEvent(new IsSignatureValid(), $controllerEvent)); } public function testInvokableControllerWithValidSignature() { $request = new Request(); $signer = $this->createMock(UriSigner::class); $signer->expects($this->once())->method('verify')->with($request); $kernel = $this->createStub(HttpKernelInterface::class); $event = new ControllerArgumentsEvent( $kernel, new IsSignatureValidAttributeController(), [], $request, null ); $listener = new IsSignatureValidAttributeListener($signer); $listener->onKernelControllerArguments($event); } public function testNoAttributeSkipsValidation() { $kernel = $this->createStub(HttpKernelInterface::class); $signer = $this->createMock(UriSigner::class); $signer->expects($this->never())->method('verify'); $event = new ControllerArgumentsEvent( $kernel, [new IsSignatureValidAttributeMethodsController(), 'noAttribute'], [], new Request(), null ); $listener = new IsSignatureValidAttributeListener($signer); $listener->onKernelControllerArguments($event); } public function testDefaultCheckRequestSucceeds() { $request = new Request(); $signer = $this->createMock(UriSigner::class); $signer->expects($this->once())->method('verify')->with($request); $kernel = $this->createStub(HttpKernelInterface::class); $event = new ControllerArgumentsEvent( $kernel, [new IsSignatureValidAttributeMethodsController(), 'withDefaultBehavior'], [], $request, null ); $listener = new IsSignatureValidAttributeListener($signer); $listener->onKernelControllerArguments($event); } public function testCheckRequestFailsThrowsHttpException() { $request = new Request(); $signer = $this->createMock(UriSigner::class); $signer->expects($this->once())->method('verify')->willThrowException(new UnsignedUriException()); $kernel = $this->createStub(HttpKernelInterface::class); $event = new ControllerArgumentsEvent( $kernel, [new IsSignatureValidAttributeMethodsController(), 'withDefaultBehavior'], [], $request, null ); $listener = new IsSignatureValidAttributeListener($signer); $this->expectException(UnsignedUriException::class); $listener->onKernelControllerArguments($event); } public function testMultipleAttributesAllValid() { $request = new Request(); $signer = $this->createMock(UriSigner::class); $signer->expects($this->exactly(2))->method('verify')->with($request); $kernel = $this->createStub(HttpKernelInterface::class); $event = new ControllerArgumentsEvent( $kernel, [new IsSignatureValidAttributeMethodsController(), 'withMultiple'], [], $request, null ); $listener = new IsSignatureValidAttributeListener($signer); $listener->onKernelControllerArguments($event); } public function testValidationWithStringMethod() { $request = new Request([], [], [], [], [], ['REQUEST_METHOD' => 'POST']); $signer = $this->createMock(UriSigner::class); $signer->expects($this->once())->method('verify')->with($request); $kernel = $this->createStub(HttpKernelInterface::class); $event = new ControllerArgumentsEvent( $kernel, [new IsSignatureValidAttributeMethodsController(), 'withPostOnly'], [], $request, null ); $listener = new IsSignatureValidAttributeListener($signer); $listener->onKernelControllerArguments($event); } public function testValidationWithArrayMethods() { $request = new Request([], [], [], [], [], ['REQUEST_METHOD' => 'POST']); $signer = $this->createMock(UriSigner::class); $signer->expects($this->once())->method('verify')->with($request); $kernel = $this->createStub(HttpKernelInterface::class); $event = new ControllerArgumentsEvent( $kernel, [new IsSignatureValidAttributeMethodsController(), 'withGetAndPost'], [], $request, null ); $listener = new IsSignatureValidAttributeListener($signer); $listener->onKernelControllerArguments($event); } public function testValidationSkippedForNonMatchingMethod() { $request = new Request([], [], [], [], [], ['REQUEST_METHOD' => 'GET']); $kernel = $this->createStub(HttpKernelInterface::class); $signer = $this->createMock(UriSigner::class); $signer->expects($this->never())->method('verify'); $event = new ControllerArgumentsEvent( $kernel, [new IsSignatureValidAttributeMethodsController(), 'withPostOnly'], [], $request, null ); $listener = new IsSignatureValidAttributeListener($signer); $listener->onKernelControllerArguments($event); } } ================================================ FILE: Tests/EventListener/LocaleAwareListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\EventListener\LocaleAwareListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Contracts\Translation\LocaleAwareInterface; class LocaleAwareListenerTest extends TestCase { private LocaleAwareListener $listener; private MockObject&LocaleAwareInterface $localeAwareService; private RequestStack $requestStack; protected function setUp(): void { $this->localeAwareService = $this->createMock(LocaleAwareInterface::class); $this->requestStack = new RequestStack(); $this->listener = new LocaleAwareListener(new \ArrayIterator([$this->localeAwareService]), $this->requestStack); } public function testLocaleIsSetInOnKernelRequest() { $this->localeAwareService ->expects($this->once()) ->method('setLocale') ->with($this->equalTo('fr')); $event = new RequestEvent($this->createStub(HttpKernelInterface::class), $this->createRequest('fr'), HttpKernelInterface::MAIN_REQUEST); $this->listener->onKernelRequest($event); } public function testDefaultLocaleIsUsedOnExceptionsInOnKernelRequest() { $this->localeAwareService ->expects($this->exactly(2)) ->method('setLocale') ->willReturnCallback(function (string $locale): void { static $counter = 0; if (1 === ++$counter) { throw new \InvalidArgumentException(); } $this->assertSame('en', $locale); }) ; $event = new RequestEvent($this->createStub(HttpKernelInterface::class), $this->createRequest('fr'), HttpKernelInterface::MAIN_REQUEST); $this->listener->onKernelRequest($event); } public function testLocaleIsSetInOnKernelFinishRequestWhenParentRequestExists() { $this->localeAwareService ->expects($this->once()) ->method('setLocale') ->with($this->equalTo('fr')); $this->requestStack->push($this->createRequest('fr')); $this->requestStack->push($subRequest = $this->createRequest('de')); $event = new FinishRequestEvent($this->createStub(HttpKernelInterface::class), $subRequest, HttpKernelInterface::SUB_REQUEST); $this->listener->onKernelFinishRequest($event); } public function testLocaleIsSetToDefaultOnKernelFinishRequestWhenParentRequestDoesNotExist() { $this->localeAwareService ->expects($this->once()) ->method('setLocale') ->with($this->equalTo('en')); $this->requestStack->push($subRequest = $this->createRequest('de')); $event = new FinishRequestEvent($this->createStub(HttpKernelInterface::class), $subRequest, HttpKernelInterface::SUB_REQUEST); $this->listener->onKernelFinishRequest($event); } public function testDefaultLocaleIsUsedOnExceptionsInOnKernelFinishRequest() { $this->localeAwareService ->expects($this->exactly(2)) ->method('setLocale') ->willReturnCallback(function (string $locale): void { static $counter = 0; if (1 === ++$counter) { throw new \InvalidArgumentException(); } $this->assertSame('en', $locale); }) ; $this->requestStack->push($this->createRequest('fr')); $this->requestStack->push($subRequest = $this->createRequest('de')); $event = new FinishRequestEvent($this->createStub(HttpKernelInterface::class), $subRequest, HttpKernelInterface::SUB_REQUEST); $this->listener->onKernelFinishRequest($event); } private function createRequest(string $locale): Request { $request = new Request(); $request->setLocale($locale); return $request; } } ================================================ FILE: Tests/EventListener/LocaleListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\EventListener\LocaleListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RequestContextAwareInterface; use Symfony\Component\Routing\Router; class LocaleListenerTest extends TestCase { public function testIsAnEventSubscriber() { $this->assertInstanceOf(EventSubscriberInterface::class, new LocaleListener(new RequestStack())); } public function testRegisteredEvent() { $this->assertEquals( [ KernelEvents::REQUEST => [['setDefaultLocale', 100], ['onKernelRequest', 16]], KernelEvents::FINISH_REQUEST => [['onKernelFinishRequest', 0]], ], LocaleListener::getSubscribedEvents() ); } public function testDefaultLocale() { $listener = new LocaleListener(new RequestStack(), 'fr'); $event = $this->getEvent($request = Request::create('/')); $listener->setDefaultLocale($event); $this->assertEquals('fr', $request->getLocale()); } public function testLocaleFromRequestAttribute() { $request = Request::create('/'); $request->cookies->set(session_name(), 'value'); $request->attributes->set('_locale', 'es'); $listener = new LocaleListener(new RequestStack(), 'fr'); $event = $this->getEvent($request); $listener->onKernelRequest($event); $this->assertEquals('es', $request->getLocale()); } public function testLocaleSetForRoutingContext() { // the request context is updated $context = $this->createMock(RequestContext::class); $context->expects($this->once())->method('setParameter')->with('_locale', 'es'); $router = $this->getMockBuilder(Router::class)->onlyMethods(['getContext'])->disableOriginalConstructor()->getMock(); $router->expects($this->once())->method('getContext')->willReturn($context); $request = Request::create('/'); $request->attributes->set('_locale', 'es'); $listener = new LocaleListener(new RequestStack(), 'fr', $router); $listener->onKernelRequest($this->getEvent($request)); } public function testRouterResetWithParentRequestOnKernelFinishRequest() { // the request context is updated $context = $this->createMock(RequestContext::class); $context->expects($this->once())->method('setParameter')->with('_locale', 'es'); $router = $this->getMockBuilder(Router::class)->onlyMethods(['getContext'])->disableOriginalConstructor()->getMock(); $router->expects($this->once())->method('getContext')->willReturn($context); $parentRequest = Request::create('/'); $parentRequest->setLocale('es'); $requestStack = new RequestStack(); $requestStack->push($parentRequest); $subRequest = new Request(); $requestStack->push($subRequest); $event = new FinishRequestEvent($this->createStub(HttpKernelInterface::class), new Request(), HttpKernelInterface::MAIN_REQUEST); $listener = new LocaleListener($requestStack, 'fr', $router); $listener->onKernelFinishRequest($event); } public function testRequestLocaleIsNotOverridden() { $request = Request::create('/'); $request->setLocale('de'); $listener = new LocaleListener(new RequestStack(), 'fr'); $event = $this->getEvent($request); $listener->onKernelRequest($event); $this->assertEquals('de', $request->getLocale()); } public function testRequestPreferredLocaleFromAcceptLanguageHeader() { $request = Request::create('/'); $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['de', 'fr']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); $listener->onKernelRequest($event); $this->assertEquals('fr', $request->getLocale()); } public function testRequestDefaultLocaleIfNoAcceptLanguageHeaderIsPresent() { $request = new Request(); $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['lt', 'de']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); $listener->onKernelRequest($event); $this->assertEquals('de', $request->getLocale()); } public function testRequestVaryByLanguageAttributeIsSetIfUsingAcceptLanguageHeader() { $request = new Request(); $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['lt', 'de']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); $listener->onKernelRequest($event); $this->assertTrue($request->attributes->get('_vary_by_language')); } public function testRequestSecondPreferredLocaleFromAcceptLanguageHeader() { $request = Request::create('/'); $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['de', 'en']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); $listener->onKernelRequest($event); $this->assertEquals('en', $request->getLocale()); } public function testDontUseAcceptLanguageHeaderIfNotEnabled() { $request = Request::create('/'); $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); $listener = new LocaleListener(new RequestStack(), 'de', null, false, ['de', 'en']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); $listener->onKernelRequest($event); $this->assertEquals('de', $request->getLocale()); } public function testRequestUnavailablePreferredLocaleFromAcceptLanguageHeader() { $request = Request::create('/'); $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['de', 'it']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); $listener->onKernelRequest($event); $this->assertEquals('de', $request->getLocale()); } public function testDefaultLocaleReturnedWhenNoAcceptLanguageMatchAndDefaultLocaleIsNotFirstEnabledLocale() { $request = Request::create('/'); $request->headers->set('Accept-Language', 'es;q=0.9,ja;q=0.8'); // 'de' is the default locale but is NOT first in enabled_locales $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['it', 'fr', 'de']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); $listener->onKernelRequest($event); $this->assertEquals('de', $request->getLocale()); } public function testRequestNoLocaleFromAcceptLanguageHeader() { $request = Request::create('/'); $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); $listener = new LocaleListener(new RequestStack(), 'de', null, true); $event = $this->getEvent($request); $listener->setDefaultLocale($event); $listener->onKernelRequest($event); $this->assertEquals('fr_FR', $request->getLocale()); } public function testRequestAttributeLocaleNotOverriddenFromAcceptLanguageHeader() { $request = Request::create('/'); $request->attributes->set('_locale', 'it'); $request->headers->set('Accept-Language', 'fr-FR,fr;q=0.9,en-GB;q=0.8,en;q=0.7,en-US;q=0.6,es;q=0.5'); $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['fr', 'en']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); $listener->onKernelRequest($event); $this->assertEquals('it', $request->getLocale()); } public function testEnabledLocalesFiltersEmptyValues() { $request = Request::create('/'); $request->headers->set('Accept-Language', 'es,fr;q=0.8,en;q=0.5'); $listener = new LocaleListener(new RequestStack(), 'de', null, true, ['', null, 'en', 'fr']); $event = $this->getEvent($request); $listener->setDefaultLocale($event); $listener->onKernelRequest($event); $this->assertSame('fr', $request->getLocale()); } public function testFinishRequestWithNoParentResetsRouterContextToDefault() { $context = new RequestContext(); $router = $this->createStub(RequestContextAwareInterface::class); $router->method('getContext')->willReturn($context); $request = Request::create('/'); $request->attributes->set('_locale', 'en'); $requestStack = new RequestStack(); $listener = new LocaleListener($requestStack, 'fr', $router); $listener->onKernelRequest($this->getEvent($request)); $this->assertSame('en', $context->getParameter('_locale')); $event = new FinishRequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); $listener->onKernelFinishRequest($event); $this->assertSame('fr', $context->getParameter('_locale')); } public function testSetDefaultLocaleSetsRouterContext() { $context = new RequestContext(); $router = $this->createStub(RequestContextAwareInterface::class); $router->method('getContext')->willReturn($context); $listener = new LocaleListener(new RequestStack(), 'fr', $router); $request = Request::create('/'); $event = $this->getEvent($request); $listener->setDefaultLocale($event); $this->assertSame('fr', $context->getParameter('_locale')); } private function getEvent(Request $request): RequestEvent { return new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); } } ================================================ FILE: Tests/EventListener/ProfilerListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\HttpKernel\EventListener\ProfilerListener; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\Profiler\Profile; use Symfony\Component\HttpKernel\Profiler\Profiler; class ProfilerListenerTest extends TestCase { /** * Test a main and sub request with an exception and `onlyException` profiler option enabled. */ public function testKernelTerminate() { $profile = new Profile('token'); $profiler = $this->createMock(Profiler::class); $profiler->expects($this->once()) ->method('collect') ->willReturn($profile); $kernel = $this->createStub(HttpKernelInterface::class); $mainRequest = new Request(); $subRequest = new Request(); $response = new Response(); $requestStack = new RequestStack(); $requestStack->push($mainRequest); $onlyException = true; $listener = new ProfilerListener($profiler, $requestStack, null, $onlyException); // main request $listener->onKernelResponse(new ResponseEvent($kernel, $mainRequest, Kernel::MAIN_REQUEST, $response)); // sub request $listener->onKernelException(new ExceptionEvent($kernel, $subRequest, Kernel::SUB_REQUEST, new HttpException(404))); $listener->onKernelResponse(new ResponseEvent($kernel, $subRequest, Kernel::SUB_REQUEST, $response)); $listener->onKernelTerminate(new TerminateEvent($kernel, $mainRequest, $response)); } #[DataProvider('collectRequestProvider')] public function testCollectParameter(Request $request, ?bool $enable) { $profile = new Profile('token'); $profiler = $this->createMock(Profiler::class); $profiler->expects($this->once()) ->method('collect') ->willReturn($profile); $profiler ->expects(null === $enable ? $this->never() : $this->once()) ->method($enable ? 'enable' : 'disable'); $kernel = $this->createStub(HttpKernelInterface::class); $response = new Response(); $requestStack = new RequestStack(); $requestStack->push($request); $listener = new ProfilerListener($profiler, $requestStack, null, false, false, 'profile'); $listener->onKernelResponse(new ResponseEvent($kernel, $request, Kernel::MAIN_REQUEST, $response)); } public static function collectRequestProvider(): iterable { yield [Request::create('/'), null]; yield [Request::create('/', 'GET', ['profile' => '1']), true]; yield [Request::create('/', 'GET', ['profile' => '0']), false]; $request = Request::create('/'); $request->attributes->set('profile', true); yield [$request, true]; } } ================================================ FILE: Tests/EventListener/ResponseListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\EventListener\ResponseListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; class ResponseListenerTest extends TestCase { private EventDispatcher $dispatcher; private HttpKernelInterface $kernel; protected function setUp(): void { $this->dispatcher = new EventDispatcher(); $listener = new ResponseListener('UTF-8'); $this->dispatcher->addListener(KernelEvents::RESPONSE, $listener->onKernelResponse(...)); $this->kernel = $this->createStub(HttpKernelInterface::class); } public function testFilterDoesNothingForSubRequests() { $response = new Response('foo'); $event = new ResponseEvent($this->kernel, new Request(), HttpKernelInterface::SUB_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('', $event->getResponse()->headers->get('content-type')); } public function testFilterSetsNonDefaultCharsetIfNotOverridden() { $listener = new ResponseListener('ISO-8859-15'); $this->dispatcher->addListener(KernelEvents::RESPONSE, $listener->onKernelResponse(...), 1); $response = new Response('foo'); $event = new ResponseEvent($this->kernel, Request::create('/'), HttpKernelInterface::MAIN_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('ISO-8859-15', $response->getCharset()); } public function testFilterDoesNothingIfCharsetIsOverridden() { $listener = new ResponseListener('ISO-8859-15'); $this->dispatcher->addListener(KernelEvents::RESPONSE, $listener->onKernelResponse(...), 1); $response = new Response('foo'); $response->setCharset('ISO-8859-1'); $event = new ResponseEvent($this->kernel, Request::create('/'), HttpKernelInterface::MAIN_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('ISO-8859-1', $response->getCharset()); } public function testFiltersSetsNonDefaultCharsetIfNotOverriddenOnNonTextContentType() { $listener = new ResponseListener('ISO-8859-15'); $this->dispatcher->addListener(KernelEvents::RESPONSE, $listener->onKernelResponse(...), 1); $response = new Response('foo'); $request = Request::create('/'); $request->setRequestFormat('application/json'); $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('ISO-8859-15', $response->getCharset()); } public function testSetContentLanguageHeaderWhenEmptyAndAtLeast2EnabledLocalesAreConfigured() { $listener = new ResponseListener('ISO-8859-15', true, ['fr', 'en']); $this->dispatcher->addListener(KernelEvents::RESPONSE, $listener->onKernelResponse(...), 1); $response = new Response('content'); $request = Request::create('/'); $request->setLocale('fr'); $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('fr', $response->headers->get('Content-Language')); } public function testNotOverrideContentLanguageHeaderWhenNotEmpty() { $listener = new ResponseListener('ISO-8859-15', true, ['de']); $this->dispatcher->addListener(KernelEvents::RESPONSE, $listener->onKernelResponse(...), 1); $response = new Response('content'); $response->headers->set('Content-Language', 'mi, en'); $request = Request::create('/'); $request->setLocale('de'); $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('mi, en', $response->headers->get('Content-Language')); } public function testNotSetContentLanguageHeaderWhenDisabled() { $listener = new ResponseListener('ISO-8859-15', false); $this->dispatcher->addListener(KernelEvents::RESPONSE, $listener->onKernelResponse(...), 1); $response = new Response('content'); $request = Request::create('/'); $request->setLocale('fr'); $event = new ResponseEvent($this->kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response); $this->dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertNull($response->headers->get('Content-Language')); } } ================================================ FILE: Tests/EventListener/RouterListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Controller\ControllerResolver; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\EventListener\ErrorListener; use Symfony\Component\HttpKernel\EventListener\RouterListener; use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\NoConfigurationException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Matcher\RequestMatcherInterface; use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Routing\RequestContext; class RouterListenerTest extends TestCase { #[DataProvider('getPortData')] public function testPort($defaultHttpPort, $defaultHttpsPort, $uri, $expectedHttpPort, $expectedHttpsPort) { $urlMatcher = $this->createStub(UrlMatcherInterface::class); $context = new RequestContext(); $context->setHttpPort($defaultHttpPort); $context->setHttpsPort($defaultHttpsPort); $urlMatcher ->method('getContext') ->willReturn($context); $listener = new RouterListener($urlMatcher, new RequestStack()); $event = $this->createRequestEventForUri($uri); $listener->onKernelRequest($event); $this->assertEquals($expectedHttpPort, $context->getHttpPort()); $this->assertEquals($expectedHttpsPort, $context->getHttpsPort()); $this->assertEquals(str_starts_with($uri, 'https') ? 'https' : 'http', $context->getScheme()); } public static function getPortData() { return [ [80, 443, 'http://localhost/', 80, 443], [80, 443, 'http://localhost:90/', 90, 443], [80, 443, 'https://localhost/', 80, 443], [80, 443, 'https://localhost:90/', 80, 90], ]; } private function createRequestEventForUri(string $uri): RequestEvent { $kernel = $this->createStub(HttpKernelInterface::class); $request = Request::create($uri); $request->attributes->set('_controller', null); // Prevents going in to routing process return new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); } public function testRequestMatcher() { $kernel = $this->createStub(HttpKernelInterface::class); $request = Request::create('http://localhost/'); $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $requestMatcher = $this->createMock(RequestMatcherInterface::class); $requestMatcher->expects($this->once()) ->method('matchRequest') ->with($this->isInstanceOf(Request::class)) ->willReturn([]); $listener = new RouterListener($requestMatcher, new RequestStack(), new RequestContext()); $listener->onKernelRequest($event); } public function testSubRequestWithDifferentMethod() { $kernel = $this->createStub(HttpKernelInterface::class); $request = Request::create('http://localhost/', 'post'); $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $requestMatcher = $this->createStub(RequestMatcherInterface::class); $requestMatcher ->method('matchRequest') ->willReturn([]); $context = new RequestContext(); $listener = new RouterListener($requestMatcher, new RequestStack(), new RequestContext()); $listener->onKernelRequest($event); // sub-request with another HTTP method $kernel = $this->createStub(HttpKernelInterface::class); $request = Request::create('http://localhost/', 'get'); $event = new RequestEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST); $listener->onKernelRequest($event); $this->assertEquals('GET', $context->getMethod()); } #[DataProvider('getLoggingParameterData')] public function testLoggingParameter($parameter, $log, $parameters) { $requestMatcher = $this->createMock(RequestMatcherInterface::class); $requestMatcher->expects($this->once()) ->method('matchRequest') ->willReturn($parameter); $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->once()) ->method('info') ->with($this->equalTo($log), $this->equalTo($parameters)); $kernel = $this->createStub(HttpKernelInterface::class); $request = Request::create('http://localhost/'); $listener = new RouterListener($requestMatcher, new RequestStack(), new RequestContext(), $logger); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); } public static function getLoggingParameterData() { return [ [['_route' => 'foo'], 'Matched route "{route}".', ['route' => 'foo', 'route_parameters' => ['_route' => 'foo'], 'request_uri' => 'http://localhost/', 'method' => 'GET']], [[], 'Matched route "{route}".', ['route' => 'n/a', 'route_parameters' => [], 'request_uri' => 'http://localhost/', 'method' => 'GET']], ]; } public function testWithBadRequest() { $requestStack = new RequestStack(); $requestMatcher = $this->createMock(RequestMatcherInterface::class); $requestMatcher->expects($this->never())->method('matchRequest'); $dispatcher = new EventDispatcher(); $dispatcher->addSubscriber(new ValidateRequestListener()); $dispatcher->addSubscriber(new RouterListener($requestMatcher, $requestStack, new RequestContext())); $dispatcher->addSubscriber(new ErrorListener(static fn () => new Response('Exception handled', 400))); $kernel = new HttpKernel($dispatcher, new ControllerResolver(), $requestStack, new ArgumentResolver()); $request = Request::create('http://localhost/'); $request->headers->set('host', '###'); $response = $kernel->handle($request); $this->assertSame(400, $response->getStatusCode()); } public function testNoRoutingConfigurationResponse() { $requestStack = new RequestStack(); $requestMatcher = $this->createMock(RequestMatcherInterface::class); $requestMatcher ->expects($this->exactly(2)) ->method('matchRequest') ->willThrowException(new NoConfigurationException()) ; $dispatcher = new EventDispatcher(); $dispatcher->addSubscriber(new RouterListener($requestMatcher, $requestStack, new RequestContext())); $kernel = new HttpKernel($dispatcher, new ControllerResolver(), $requestStack, new ArgumentResolver()); $request = Request::create('http://localhost/'); $response = $kernel->handle($request); $this->assertSame(404, $response->getStatusCode()); $this->assertStringContainsString('Welcome', $response->getContent()); $response = $kernel->handle($request); $this->assertSame(404, $response->getStatusCode()); $this->assertStringContainsString('Welcome', $response->getContent()); } public function testRequestWithBadHost() { $kernel = $this->createStub(HttpKernelInterface::class); $request = Request::create('/'); $request->headers->set('host', 'bad host %22'); $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $requestMatcher = $this->createStub(RequestMatcherInterface::class); $listener = new RouterListener($requestMatcher, new RequestStack(), new RequestContext()); try { $listener->onKernelRequest($event); self::fail(\sprintf('Expected "%s" or "%s" to be thrown.', BadRequestHttpException::class, BadRequestException::class)); } catch (\Throwable $e) { $this->assertTrue($e instanceof BadRequestHttpException || $e instanceof BadRequestException); } } public function testResourceNotFoundException() { $this->expectException(NotFoundHttpException::class); $this->expectExceptionMessage('No route found for "GET https://www.symfony.com/path" (from "https://www.google.com")'); $context = new RequestContext(); $urlMatcher = $this->createStub(UrlMatcherInterface::class); $urlMatcher ->method('getContext') ->willReturn($context); $urlMatcher ->method('match') ->willThrowException(new ResourceNotFoundException()); $kernel = $this->createStub(HttpKernelInterface::class); $request = Request::create('https://www.symfony.com/path'); $request->headers->set('referer', 'https://www.google.com'); $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $listener = new RouterListener($urlMatcher, new RequestStack()); $listener->onKernelRequest($event); } public function testMethodNotAllowedException() { $this->expectException(MethodNotAllowedHttpException::class); $this->expectExceptionMessage('No route found for "GET https://www.symfony.com/path": Method Not Allowed (Allow: POST)'); $context = new RequestContext(); $urlMatcher = $this->createStub(UrlMatcherInterface::class); $urlMatcher ->method('getContext') ->willReturn($context); $urlMatcher ->method('match') ->willThrowException(new MethodNotAllowedException(['POST'])); $kernel = $this->createStub(HttpKernelInterface::class); $request = Request::create('https://www.symfony.com/path'); $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $listener = new RouterListener($urlMatcher, new RequestStack()); $listener->onKernelRequest($event); } #[DataProvider('provideRouteMapping')] public function testRouteMapping(array $expected, array $parameters) { $kernel = $this->createStub(HttpKernelInterface::class); $request = Request::create('http://localhost/'); $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $requestMatcher = $this->createStub(RequestMatcherInterface::class); $requestMatcher ->method('matchRequest') ->willReturn($parameters); $listener = new RouterListener($requestMatcher, new RequestStack(), new RequestContext()); $listener->onKernelRequest($event); $expected['_route_mapping'] = $parameters['_route_mapping']; unset($parameters['_route_mapping']); $expected['_route_params'] = $parameters; $this->assertEquals($expected, $request->attributes->all()); } public static function provideRouteMapping(): iterable { yield [ [ 'conference' => 'vienna-2024', ], [ 'slug' => 'vienna-2024', '_route_mapping' => [ 'slug' => 'conference', ], ], ]; yield [ [ 'article' => [ 'id' => 'abc123', 'date' => '2024-04-24', 'slug' => 'symfony-rocks', ], ], [ 'id' => 'abc123', 'date' => '2024-04-24', 'slug' => 'symfony-rocks', '_route_mapping' => [ 'id' => 'article', 'date' => 'article', 'slug' => 'article', ], ], ]; yield [ [ 'conference' => ['slug' => 'vienna-2024'], ], [ 'slug' => 'vienna-2024', '_route_mapping' => [ 'slug' => [ 'conference', 'slug', ], ], ], ]; yield [ [ 'article' => [ 'id' => 'abc123', 'date' => '2024-04-24', 'slug' => 'symfony-rocks', ], ], [ 'id' => 'abc123', 'date' => '2024-04-24', 'slug' => 'symfony-rocks', '_route_mapping' => [ 'id' => [ 'article', 'id', ], 'date' => [ 'article', 'date', ], 'slug' => [ 'article', 'slug', ], ], ], ]; } } ================================================ FILE: Tests/EventListener/SerializeControllerResultListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Attribute\Serialize; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; use Symfony\Component\HttpKernel\Event\ControllerArgumentsMetadata; use Symfony\Component\HttpKernel\Event\ControllerAttributeEvent; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\ViewEvent; use Symfony\Component\HttpKernel\EventListener\SerializeControllerResultAttributeListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Serializer\SerializerInterface; class SerializeControllerResultListenerTest extends TestCase { public function testSerializeAttribute() { $controllerResult = new ProductCreated(10); $responseBody = '{"productId": 10}'; $serializer = $this->createMock(SerializerInterface::class); $serializer->expects($this->once()) ->method('serialize') ->with($controllerResult, 'json', ['foo' => 'bar']) ->willReturn($responseBody); $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $controller = new GetApiController(); $requestType = HttpKernelInterface::MAIN_REQUEST; $viewEvent = new ViewEvent( $kernel, $request, $requestType, $controllerResult, new ControllerArgumentsMetadata( new ControllerEvent($kernel, $controller, $request, $requestType), new ControllerArgumentsEvent( $kernel, $controller, [], $request, $requestType, ), ), ); $listener = new SerializeControllerResultAttributeListener($serializer); $listener->onView(new ControllerAttributeEvent(new Serialize(201, ['X-Test-Header' => 'abc'], ['foo' => 'bar']), $viewEvent)); $response = $viewEvent->getResponse(); self::assertSame(201, $response->getStatusCode()); self::assertSame($responseBody, $response->getContent()); self::assertSame('abc', $response->headers->get('X-Test-Header')); } } class ProductCreated { public function __construct(public readonly int $productId) { } } class GetApiController { #[Serialize(201, ['X-Test-Header' => 'abc'], ['foo' => 'bar'])] public function __invoke(): ProductCreated { return new ProductCreated(10); } } ================================================ FILE: Tests/EventListener/SessionListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\SessionFactory; use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorageFactory; use Symfony\Component\HttpFoundation\Session\Storage\PhpBridgeSessionStorageFactory; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface; use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\EventListener\AbstractSessionListener; use Symfony\Component\HttpKernel\EventListener\SessionListener; use Symfony\Component\HttpKernel\Exception\UnexpectedSessionUsageException; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelInterface; class SessionListenerTest extends TestCase { #[DataProvider('provideSessionOptions')] #[RunInSeparateProcess] public function testSessionCookieOptions(array $phpSessionOptions, array $sessionOptions, array $expectedSessionOptions) { $session = $this->createStub(Session::class); $session->method('getUsageIndex')->willReturn(0, 1); $session->method('getId')->willReturn('123456'); $session->method('getName')->willReturn('PHPSESSID'); $session->method('save'); $session->method('isStarted')->willReturn(true); if (isset($phpSessionOptions['samesite'])) { ini_set('session.cookie_samesite', $phpSessionOptions['samesite']); } session_set_cookie_params(0, $phpSessionOptions['path'] ?? null, $phpSessionOptions['domain'] ?? null, $phpSessionOptions['secure'] ?? null, $phpSessionOptions['httponly'] ?? null); $listener = new SessionListener(new Container(), false, $sessionOptions); $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $request->setSession($session); $response = new Response(); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $cookies = $response->headers->getCookies(); if ($sessionOptions['use_cookies'] ?? true) { $this->assertCount(1, $cookies); $this->assertSame('PHPSESSID', $cookies[0]->getName()); $this->assertSame('123456', $cookies[0]->getValue()); $this->assertSame($expectedSessionOptions['cookie_path'], $cookies[0]->getPath()); $this->assertSame($expectedSessionOptions['cookie_domain'], $cookies[0]->getDomain()); $this->assertSame($expectedSessionOptions['cookie_secure'], $cookies[0]->isSecure()); $this->assertSame($expectedSessionOptions['cookie_httponly'], $cookies[0]->isHttpOnly()); $this->assertSame($expectedSessionOptions['cookie_samesite'], $cookies[0]->getSameSite()); } else { $this->assertCount(0, $cookies); } } public static function provideSessionOptions(): \Generator { yield 'set_samesite_by_php' => [ 'phpSessionOptions' => ['samesite' => Cookie::SAMESITE_STRICT], 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true], 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_STRICT], ]; yield 'set_cookie_path_by_php' => [ 'phpSessionOptions' => ['path' => '/prod/'], 'sessionOptions' => ['cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], 'expectedSessionOptions' => ['cookie_path' => '/prod/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], ]; yield 'set_cookie_secure_by_php' => [ 'phpSessionOptions' => ['secure' => true], 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], ]; yield 'set_cookiesecure_auto_by_symfony_false_by_php' => [ 'phpSessionOptions' => ['secure' => false], 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => true, 'cookie_secure' => 'auto', 'cookie_samesite' => Cookie::SAMESITE_LAX], 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => false, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], ]; yield 'set_cookiesecure_auto_by_symfony_true_by_php' => [ 'phpSessionOptions' => ['secure' => true], 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => true, 'cookie_secure' => 'auto', 'cookie_samesite' => Cookie::SAMESITE_LAX], 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], ]; yield 'set_cookie_httponly_by_php' => [ 'phpSessionOptions' => ['httponly' => true], 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], ]; yield 'set_cookie_domain_by_php' => [ 'phpSessionOptions' => ['domain' => 'test.symfony'], 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => true, 'cookie_secure' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => 'test.symfony', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], ]; yield 'set_samesite_by_symfony' => [ 'phpSessionOptions' => ['samesite' => Cookie::SAMESITE_STRICT], 'sessionOptions' => ['cookie_path' => '/test/', 'cookie_httponly' => true, 'cookie_secure' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], 'expectedSessionOptions' => ['cookie_path' => '/test/', 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], ]; yield 'set_use_cookies_false_by_symfony' => [ 'phpSessionOptions' => [], 'sessionOptions' => ['use_cookies' => false, 'cookie_domain' => '', 'cookie_secure' => true, 'cookie_httponly' => true, 'cookie_samesite' => Cookie::SAMESITE_LAX], 'expectedSessionOptions' => [], ]; } #[RunInSeparateProcess] public function testPhpBridgeAlreadyStartedSession() { session_start(); $sessionId = session_id(); $request = new Request(); $listener = $this->createListener($request, new PhpBridgeSessionStorageFactory()); $event = new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); $listener->onKernelRequest($event); $this->assertTrue($request->hasSession()); $this->assertSame($sessionId, $request->getSession()->getId()); } #[RunInSeparateProcess] public function testSessionCookieWrittenNoCookieGiven() { $request = new Request(); $listener = $this->createListener($request, new NativeSessionStorageFactory()); $kernel = $this->createStub(HttpKernelInterface::class); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $session = $request->getSession(); $session->set('hello', 'world'); $response = new Response(); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $cookies = $response->headers->getCookies(); $this->assertCount(1, $cookies); $sessionCookie = $cookies[0]; $this->assertSame('PHPSESSID', $sessionCookie->getName()); $this->assertNotEmpty($sessionCookie->getValue()); $this->assertFalse($sessionCookie->isCleared()); } #[RunInSeparateProcess] public function testSessionCookieNotWrittenCookieGiven() { $sessionId = $this->createValidSessionId(); $this->assertNotEmpty($sessionId); $request = new Request(); $request->cookies->set('PHPSESSID', $sessionId); $listener = $this->createListener($request, new NativeSessionStorageFactory()); $kernel = $this->createStub(HttpKernelInterface::class); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $session = $request->getSession(); $this->assertSame($sessionId, $session->getId()); $session->set('hello', 'world'); $response = new Response(); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertSame($sessionId, $session->getId()); $cookies = $response->headers->getCookies(); $this->assertCount(0, $cookies); } #[RunInSeparateProcess] public function testNewSessionIdIsNotOverwritten() { $newSessionId = $this->createValidSessionId(); $this->assertNotEmpty($newSessionId); $request = new Request(); $request->cookies->set('PHPSESSID', 'OLD-SESSION-ID'); $listener = $this->createListener($request, new NativeSessionStorageFactory()); $kernel = $this->createStub(HttpKernelInterface::class); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $session = $request->getSession(); $this->assertSame($newSessionId, $session->getId()); $session->set('hello', 'world'); $response = new Response(); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertSame($newSessionId, $session->getId()); $cookies = $response->headers->getCookies(); $this->assertCount(1, $cookies); $sessionCookie = $cookies[0]; $this->assertSame('PHPSESSID', $sessionCookie->getName()); $this->assertSame($newSessionId, $sessionCookie->getValue()); } #[RunInSeparateProcess] public function testSessionCookieClearedWhenInvalidated() { $sessionId = $this->createValidSessionId(); $request = new Request(); $request->cookies->set('PHPSESSID', $sessionId); $listener = $this->createListener($request, new NativeSessionStorageFactory()); $kernel = $this->createStub(HttpKernelInterface::class); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $session = $request->getSession(); $session->start(); $sessionId = $session->getId(); $this->assertNotEmpty($sessionId); $_SESSION['hello'] = 'world'; // check compatibility to php session bridge $session->invalidate(); $response = new Response(); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $cookies = $response->headers->getCookies(); $this->assertCount(1, $cookies); $sessionCookie = $cookies[0]; $this->assertSame('PHPSESSID', $sessionCookie->getName()); $this->assertTrue($sessionCookie->isCleared()); } #[RunInSeparateProcess] public function testSessionCookieNotClearedWhenOtherVariablesSet() { $sessionId = $this->createValidSessionId(); $request = new Request(); $request->cookies->set('PHPSESSID', $sessionId); $listener = $this->createListener($request, new NativeSessionStorageFactory()); $kernel = $this->createStub(HttpKernelInterface::class); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $session = $request->getSession(); $session->start(); $sessionId = $session->getId(); $this->assertNotEmpty($sessionId); $_SESSION['hello'] = 'world'; $response = new Response(); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $cookies = $response->headers->getCookies(); $this->assertCount(0, $cookies); } #[RunInSeparateProcess] public function testSessionCookieSetWhenOtherNativeVariablesSet() { $request = new Request(); $listener = $this->createListener($request, new NativeSessionStorageFactory()); $kernel = $this->createStub(HttpKernelInterface::class); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $session = $request->getSession(); $session->start(); $sessionId = $session->getId(); $this->assertNotEmpty($sessionId); $_SESSION['hello'] = 'world'; $response = new Response(); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $cookies = $response->headers->getCookies(); $this->assertCount(1, $cookies); $sessionCookie = $cookies[0]; $this->assertSame('PHPSESSID', $sessionCookie->getName()); $this->assertNotEmpty($sessionCookie->getValue()); $this->assertFalse($sessionCookie->isCleared()); } public function testOnlyTriggeredOnMainRequest() { $listener = new class extends AbstractSessionListener { protected function getSession(): ?SessionInterface { return null; } }; $event = $this->createMock(RequestEvent::class); $event->expects($this->once())->method('isMainRequest')->willReturn(false); $event->expects($this->never())->method('getRequest'); // sub request $listener->onKernelRequest($event); } public function testSessionIsSet() { $session = $this->createMock(Session::class); $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); $sessionStorage = $this->createMock(NativeSessionStorage::class); $sessionStorage->expects($this->never())->method('setOptions')->with(['cookie_secure' => true]); $container = new Container(); $container->set('session_factory', $sessionFactory); $container->set('request_stack', new RequestStack()); $request = new Request(); $listener = new SessionListener($container); $event = new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); $listener->onKernelRequest($event); $this->assertTrue($request->hasSession()); $this->assertSame($session, $request->getSession()); } public function testSessionUsesFactory() { $session = $this->createMock(Session::class); $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); $container = new Container(); $container->set('session_factory', $sessionFactory); $request = new Request(); $listener = new SessionListener($container); $event = new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); $listener->onKernelRequest($event); $this->assertTrue($request->hasSession()); $this->assertSame($session, $request->getSession()); } public function testUsesFactoryWhenNeeded() { $session = $this->createMock(Session::class); $session->expects($this->once())->method('getName')->willReturn('foo'); $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); $container = new Container(); $container->set('session_factory', $sessionFactory); $request = new Request(); $listener = new SessionListener($container); $event = new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); $listener->onKernelRequest($event); $request->getSession(); $event = new ResponseEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST, new Response()); $listener->onKernelResponse($event); } public function testDontUsesFactoryWhenSessionIsNotUsed() { $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->never())->method('createSession'); $container = new Container(); $container->set('session_factory', $sessionFactory); $request = new Request(); $listener = new SessionListener($container); $event = new RequestEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); $listener->onKernelRequest($event); $event = new ResponseEvent($this->createStub(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST, new Response()); $listener->onKernelResponse($event); } public function testResponseIsPrivateIfSessionStarted() { $session = $this->createMock(Session::class); $session->expects($this->once())->method('getUsageIndex')->willReturn(1); $session->expects($this->once())->method('getName')->willReturn('foo'); $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); $container = new Container(); $container->set('session_factory', $sessionFactory); $listener = new SessionListener($container); $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $request->getSession(); $response = new Response(); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertTrue($response->headers->has('Expires')); $this->assertTrue($response->headers->hasCacheControlDirective('private')); $this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate')); $this->assertSame('0', $response->headers->getCacheControlDirective('max-age')); $this->assertLessThanOrEqual(new \DateTimeImmutable('now', new \DateTimeZone('UTC')), new \DateTimeImmutable($response->headers->get('Expires'))); $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); } public function testResponseIsStillPublicIfSessionStartedAndHeaderPresent() { $session = $this->createMock(Session::class); $session->expects($this->once())->method('getUsageIndex')->willReturn(1); $container = new Container(); $listener = new SessionListener($container); $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $request->setSession($session); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $response = new Response(); $response->setSharedMaxAge(60); $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true'); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertTrue($response->headers->hasCacheControlDirective('public')); $this->assertFalse($response->headers->has('Expires')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); $this->assertFalse($response->headers->hasCacheControlDirective('must-revalidate')); $this->assertSame('60', $response->headers->getCacheControlDirective('s-maxage')); $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); } public function testSessionSaveAndResponseHasSessionCookie() { $session = $this->getMockBuilder(Session::class)->disableOriginalConstructor()->getMock(); $session->expects($this->exactly(1))->method('getUsageIndex')->willReturn(0); $session->expects($this->exactly(1))->method('getId')->willReturn('123456'); $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); $session->expects($this->exactly(1))->method('save'); $session->expects($this->exactly(1))->method('isStarted')->willReturn(true); $container = new Container(); $listener = new SessionListener($container); $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $request->setSession($session); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $response = new Response(); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $cookies = $response->headers->getCookies(); $this->assertCount(1, $cookies); $this->assertSame('PHPSESSID', $cookies[0]->getName()); $this->assertSame('123456', $cookies[0]->getValue()); } public function testUninitializedSessionUsingSessionFromRequest() { $kernel = $this->createStub(HttpKernelInterface::class); $response = new Response(); $response->setSharedMaxAge(60); $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true'); $request = new Request(); $request->setSession(new Session()); $listener = new SessionListener(new Container()); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertFalse($response->headers->has('Expires')); $this->assertTrue($response->headers->hasCacheControlDirective('public')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); $this->assertFalse($response->headers->hasCacheControlDirective('must-revalidate')); $this->assertSame('60', $response->headers->getCacheControlDirective('s-maxage')); $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); } public function testUninitializedSessionWithoutInitializedSession() { $kernel = $this->createStub(HttpKernelInterface::class); $response = new Response(); $response->setSharedMaxAge(60); $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true'); $container = new ServiceLocator([]); $listener = new SessionListener($container); $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertFalse($response->headers->has('Expires')); $this->assertTrue($response->headers->hasCacheControlDirective('public')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); $this->assertFalse($response->headers->hasCacheControlDirective('must-revalidate')); $this->assertSame('60', $response->headers->getCacheControlDirective('s-maxage')); } public function testResponseHeadersMaxAgeAndExpiresNotBeOverriddenIfSessionStarted() { $session = $this->createMock(Session::class); $session->expects($this->once())->method('getUsageIndex')->willReturn(1); $session->expects($this->once())->method('getName')->willReturn('foo'); $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); $container = new Container(); $container->set('session_factory', $sessionFactory); $listener = new SessionListener($container); $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $request->getSession(); $response = new Response(); $response->setPrivate(); $expiresHeader = gmdate('D, d M Y H:i:s', time() + 600).' GMT'; $response->setMaxAge(600); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertTrue($response->headers->has('expires')); $this->assertSame($expiresHeader, $response->headers->get('expires')); $this->assertFalse($response->headers->has('max-age')); $this->assertSame('600', $response->headers->getCacheControlDirective('max-age')); $this->assertFalse($response->headers->hasCacheControlDirective('public')); $this->assertTrue($response->headers->hasCacheControlDirective('private')); $this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate')); $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); } public function testResponseHeadersMaxAgeAndExpiresDefaultValuesIfSessionStarted() { $session = $this->createMock(Session::class); $session->expects($this->once())->method('getUsageIndex')->willReturn(1); $container = new Container(); $listener = new SessionListener($container); $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $request->setSession($session); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $response = new Response(); $expiresHeader = gmdate('D, d M Y H:i:s', time()).' GMT'; $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertTrue($response->headers->has('expires')); $this->assertSame($expiresHeader, $response->headers->get('expires')); $this->assertFalse($response->headers->has('max-age')); $this->assertSame('0', $response->headers->getCacheControlDirective('max-age')); $this->assertFalse($response->headers->hasCacheControlDirective('public')); $this->assertTrue($response->headers->hasCacheControlDirective('private')); $this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate')); $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); } public function testPrivateResponseMaxAgeIsRespectedIfSessionStarted() { $kernel = $this->createStub(HttpKernelInterface::class); $session = $this->createMock(Session::class); $session->expects($this->once())->method('getUsageIndex')->willReturn(1); $request = new Request([], [], [], [], [], ['SERVER_PROTOCOL' => 'HTTP/1.0']); $request->setSession($session); $response = new Response(); $response->headers->set('Cache-Control', 'no-cache'); $response->prepare($request); $listener = new SessionListener(new Container()); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertSame(0, $response->getMaxAge()); $this->assertFalse($response->headers->hasCacheControlDirective('public')); $this->assertTrue($response->headers->hasCacheControlDirective('private')); $this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate')); $this->assertLessThanOrEqual(new \DateTimeImmutable('now', new \DateTimeZone('UTC')), new \DateTimeImmutable($response->headers->get('Expires'))); $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); } public function testSurrogateMainRequestIsPublic() { $session = $this->createMock(Session::class); $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); $session->expects($this->exactly(2))->method('getUsageIndex')->willReturn(0, 1); $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); $container = new Container(); $container->set('session_factory', $sessionFactory); $listener = new SessionListener($container); $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $response = new Response(); $response->setCache(['public' => true, 'max_age' => '30']); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $this->assertTrue($request->hasSession()); $subRequest = clone $request; $this->assertSame($request->getSession(), $subRequest->getSession()); $listener->onKernelRequest(new RequestEvent($kernel, $subRequest, HttpKernelInterface::MAIN_REQUEST)); $listener->onKernelResponse(new ResponseEvent($kernel, $subRequest, HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertFalse($response->headers->has('Expires')); $this->assertFalse($response->headers->hasCacheControlDirective('private')); $this->assertFalse($response->headers->hasCacheControlDirective('must-revalidate')); $this->assertSame('30', $response->headers->getCacheControlDirective('max-age')); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); $this->assertTrue($response->headers->hasCacheControlDirective('private')); $this->assertTrue($response->headers->hasCacheControlDirective('must-revalidate')); $this->assertSame('0', $response->headers->getCacheControlDirective('max-age')); $this->assertTrue($response->headers->has('Expires')); $this->assertLessThanOrEqual(new \DateTimeImmutable('now', new \DateTimeZone('UTC')), new \DateTimeImmutable($response->headers->get('Expires'))); } public function testGetSessionIsCalledOnce() { $session = $this->createMock(Session::class); $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); $kernel = $this->createStub(KernelInterface::class); $requestStack = new RequestStack(); $requestStack->push($mainRequest = new Request([], [], [], [], [], ['HTTPS' => 'on'])); $container = new Container(); $container->set('session_factory', $sessionFactory); $container->set('request_stack', $requestStack); $event = new RequestEvent($kernel, $mainRequest, HttpKernelInterface::MAIN_REQUEST); $listener = new SessionListener($container); $listener->onKernelRequest($event); // storage->setOptions() should have been called already $subRequest = $mainRequest->duplicate(); // at this point both main and subrequest have a closure to build the session $mainRequest->getSession(); // calling the factory on the subRequest should not trigger a second call to storage->setOptions() $subRequest->getSession(); } public function testGetSessionSetsSessionOnMainRequest() { $mainRequest = new Request(); $listener = $this->createListener($mainRequest, new NativeSessionStorageFactory()); $event = new RequestEvent($this->createStub(HttpKernelInterface::class), $mainRequest, HttpKernelInterface::MAIN_REQUEST); $listener->onKernelRequest($event); $this->assertFalse($mainRequest->hasSession(true)); $subRequest = $mainRequest->duplicate(); $event = new RequestEvent($this->createStub(HttpKernelInterface::class), $subRequest, HttpKernelInterface::SUB_REQUEST); $listener->onKernelRequest($event); $session = $subRequest->getSession(); $this->assertTrue($mainRequest->hasSession(true)); $this->assertSame($session, $mainRequest->getSession()); } public function testSessionUsageExceptionIfStatelessAndSessionUsed() { $session = $this->createMock(Session::class); $session->expects($this->once())->method('getUsageIndex')->willReturn(1); $session->expects($this->once())->method('getName')->willReturn('foo'); $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); $container = new Container(); $container->set('session_factory', $sessionFactory); $listener = new SessionListener($container, true); $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $request->attributes->set('_stateless', true); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $request->getSession(); $this->expectException(UnexpectedSessionUsageException::class); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new Response())); } public function testSessionUsageLogIfStatelessAndSessionUsed() { $session = $this->createMock(Session::class); $session->expects($this->once())->method('getName')->willReturn('foo'); $session->expects($this->once())->method('getUsageIndex')->willReturn(1); $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); $logger = $this->createMock(LoggerInterface::class); $logger->expects($this->exactly(1))->method('warning'); $container = new Container(); $container->set('session_factory', $sessionFactory); $container->set('logger', $logger); $listener = new SessionListener($container, false); $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $request->attributes->set('_stateless', true); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $request->getSession(); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new Response())); } public function testSessionIsSavedWhenUnexpectedSessionExceptionThrown() { $session = $this->createMock(Session::class); $session->expects($this->exactly(1))->method('getId')->willReturn('123456'); $session->expects($this->exactly(1))->method('getName')->willReturn('PHPSESSID'); $session->method('isStarted')->willReturn(true); $session->expects($this->once())->method('getUsageIndex')->willReturn(1); $session->expects($this->exactly(1))->method('save'); $sessionFactory = $this->createMock(SessionFactory::class); $sessionFactory->expects($this->once())->method('createSession')->willReturn($session); $container = new Container(); $container->set('session_factory', $sessionFactory); $listener = new SessionListener($container, true); $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $request->attributes->set('_stateless', true); $listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); $request->getSession(); $response = new Response(); $this->expectException(UnexpectedSessionUsageException::class); $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); } public function testSessionUsageCallbackWhenDebugAndStateless() { $session = $this->createMock(Session::class); $session->method('isStarted')->willReturn(true); $session->expects($this->exactly(1))->method('save'); $requestStack = new RequestStack(); $request = new Request(); $request->attributes->set('_stateless', true); $requestStack->push(new Request()); $requestStack->push($request); $requestStack->push($subRequest = new Request()); $subRequest->setSession($session); $collector = $this->createMock(RequestDataCollector::class); $collector->expects($this->once())->method('collectSessionUsage'); $container = new Container(); $container->set('request_stack', $requestStack); $container->set('session_collector', $collector->collectSessionUsage(...)); $this->expectException(UnexpectedSessionUsageException::class); (new SessionListener($container, true))->onSessionUsage(); } public function testSessionUsageCallbackWhenNoDebug() { $session = $this->createMock(Session::class); $session->method('isStarted')->willReturn(true); $session->expects($this->exactly(0))->method('save'); $request = new Request(); $request->attributes->set('_stateless', true); $requestStack = new RequestStack(); $requestStack->push($request); $collector = $this->createMock(RequestDataCollector::class); $collector->expects($this->never())->method('collectSessionUsage'); $container = new Container(); $container->set('request_stack', $requestStack); $container->set('session_collector', $collector); (new SessionListener($container))->onSessionUsage(); } public function testSessionUsageCallbackWhenNoStateless() { $session = $this->createMock(Session::class); $session->method('isStarted')->willReturn(true); $session->expects($this->never())->method('save'); $requestStack = new RequestStack(); $requestStack->push(new Request()); $requestStack->push(new Request()); $container = new Container(); $container->set('request_stack', $requestStack); (new SessionListener($container, true))->onSessionUsage(); } #[RunInSeparateProcess] public function testReset() { session_start(); $_SESSION['test'] = ['test']; session_write_close(); $this->assertNotEmpty($_SESSION); $this->assertNotEmpty(session_id()); $container = new Container(); (new SessionListener($container, true))->reset(); $this->assertSame([], $_SESSION); $this->assertSame('', session_id()); $this->assertSame(\PHP_SESSION_NONE, session_status()); } #[RunInSeparateProcess] public function testResetUnclosedSession() { session_start(); $_SESSION['test'] = ['test']; $this->assertNotEmpty($_SESSION); $this->assertNotEmpty(session_id()); $this->assertSame(\PHP_SESSION_ACTIVE, session_status()); $container = new Container(); (new SessionListener($container, true))->reset(); $this->assertSame([], $_SESSION); $this->assertSame('', session_id()); $this->assertSame(\PHP_SESSION_NONE, session_status()); } private function createListener(Request $request, SessionStorageFactoryInterface $sessionFactory) { $requestStack = new RequestStack(); $request = new Request(); $requestStack->push($request); $sessionFactory = new SessionFactory($requestStack, $sessionFactory); $container = new Container(); $container->set('request_stack', $requestStack); $container->set('session_factory', $sessionFactory); $listener = new SessionListener($container); return new SessionListener($container); } private function createValidSessionId(): string { session_start(); $sessionId = session_id(); $_SESSION['some'] = 'value'; session_write_close(); $_SESSION = []; session_abort(); return $sessionId; } } ================================================ FILE: Tests/EventListener/SurrogateListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\EventListener\SurrogateListener; use Symfony\Component\HttpKernel\HttpCache\Esi; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; class SurrogateListenerTest extends TestCase { public function testFilterDoesNothingForSubRequests() { $dispatcher = new EventDispatcher(); $kernel = $this->createStub(HttpKernelInterface::class); $response = new Response('foo '); $listener = new SurrogateListener(new Esi()); $dispatcher->addListener(KernelEvents::RESPONSE, $listener->onKernelResponse(...)); $event = new ResponseEvent($kernel, new Request(), HttpKernelInterface::SUB_REQUEST, $response); $dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('', $event->getResponse()->headers->get('Surrogate-Control')); } public function testFilterWhenThereIsSomeEsiIncludes() { $dispatcher = new EventDispatcher(); $kernel = $this->createStub(HttpKernelInterface::class); $response = new Response('foo '); $listener = new SurrogateListener(new Esi()); $dispatcher->addListener(KernelEvents::RESPONSE, $listener->onKernelResponse(...)); $event = new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response); $dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('content="ESI/1.0"', $event->getResponse()->headers->get('Surrogate-Control')); } public function testFilterWhenThereIsNoEsiIncludes() { $dispatcher = new EventDispatcher(); $kernel = $this->createStub(HttpKernelInterface::class); $response = new Response('foo'); $listener = new SurrogateListener(new Esi()); $dispatcher->addListener(KernelEvents::RESPONSE, $listener->onKernelResponse(...)); $event = new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response); $dispatcher->dispatch($event, KernelEvents::RESPONSE); $this->assertEquals('', $event->getResponse()->headers->get('Surrogate-Control')); } } ================================================ FILE: Tests/EventListener/ValidateRequestListenerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; class ValidateRequestListenerTest extends TestCase { protected function tearDown(): void { Request::setTrustedProxies([], -1); } public function testListenerThrowsWhenMainRequestHasInconsistentClientIps() { $this->expectException(ConflictingHeadersException::class); $dispatcher = new EventDispatcher(); $kernel = $this->createStub(HttpKernelInterface::class); $request = new Request(); $request->setTrustedProxies(['1.1.1.1'], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_FORWARDED); $request->server->set('REMOTE_ADDR', '1.1.1.1'); $request->headers->set('FORWARDED', 'for=2.2.2.2'); $request->headers->set('X_FORWARDED_FOR', '3.3.3.3'); $dispatcher->addListener(KernelEvents::REQUEST, [new ValidateRequestListener(), 'onKernelRequest']); $event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST); $dispatcher->dispatch($event, KernelEvents::REQUEST); } } ================================================ FILE: Tests/Exception/AccessDeniedHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; class AccessDeniedHttpExceptionTest extends HttpExceptionTest { protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new AccessDeniedHttpException($message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/BadRequestHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; class BadRequestHttpExceptionTest extends HttpExceptionTest { protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new BadRequestHttpException($message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/ConflictHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; class ConflictHttpExceptionTest extends HttpExceptionTest { protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new ConflictHttpException($message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/GoneHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\GoneHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; class GoneHttpExceptionTest extends HttpExceptionTest { protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new GoneHttpException($message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/HttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Exception\HttpException; class HttpExceptionTest extends TestCase { public static function headerDataProvider() { return [ [['X-Test' => 'Test']], [['X-Test' => 1]], [ [ ['X-Test' => 'Test'], ['X-Test-2' => 'Test-2'], ], ], ]; } public function testHeadersDefault() { $exception = $this->createException(); $this->assertSame([], $exception->getHeaders()); } #[DataProvider('headerDataProvider')] public function testHeadersConstructor($headers) { $exception = new HttpException(200, '', null, $headers); $this->assertSame($headers, $exception->getHeaders()); } #[DataProvider('headerDataProvider')] public function testHeadersSetter($headers) { $exception = $this->createException(); $exception->setHeaders($headers); $this->assertSame($headers, $exception->getHeaders()); } public function testThrowableIsAllowedForPrevious() { $previous = new class('Error of PHP 7+') extends \Error { }; $exception = $this->createException('', $previous); $this->assertSame($previous, $exception->getPrevious()); } #[DataProvider('provideStatusCode')] public function testFromStatusCode(int $statusCode) { $exception = HttpException::fromStatusCode($statusCode); $this->assertInstanceOf(HttpException::class, $exception); $this->assertSame($statusCode, $exception->getStatusCode()); } public static function provideStatusCode() { return [ [400], [401], [403], [404], [406], [409], [410], [411], [412], [418], [423], [415], [422], [428], [429], [503], ]; } protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new HttpException(200, $message, $previous, $headers, $code); } } ================================================ FILE: Tests/Exception/LengthRequiredHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\LengthRequiredHttpException; class LengthRequiredHttpExceptionTest extends HttpExceptionTest { protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new LengthRequiredHttpException($message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/LockedHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\LockedHttpException; class LockedHttpExceptionTest extends HttpExceptionTest { protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new LockedHttpException($message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/MethodNotAllowedHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; class MethodNotAllowedHttpExceptionTest extends HttpExceptionTest { public function testHeadersDefault() { $exception = new MethodNotAllowedHttpException(['GET', 'PUT']); $this->assertSame(['Allow' => 'GET, PUT'], $exception->getHeaders()); } public function testWithHeaderConstruct() { $headers = [ 'Cache-Control' => 'public, s-maxage=1200', ]; $exception = new MethodNotAllowedHttpException(['get'], '', null, 0, $headers); $headers['Allow'] = 'GET'; $this->assertSame($headers, $exception->getHeaders()); } #[DataProvider('headerDataProvider')] public function testHeadersSetter($headers) { $exception = new MethodNotAllowedHttpException(['GET']); $exception->setHeaders($headers); $this->assertSame($headers, $exception->getHeaders()); } protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new MethodNotAllowedHttpException(['get'], $message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/NotAcceptableHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; class NotAcceptableHttpExceptionTest extends HttpExceptionTest { protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new NotAcceptableHttpException($message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/NotFoundHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class NotFoundHttpExceptionTest extends HttpExceptionTest { protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new NotFoundHttpException($message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/PreconditionFailedHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException; class PreconditionFailedHttpExceptionTest extends HttpExceptionTest { protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new PreconditionFailedHttpException($message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/PreconditionRequiredHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException; class PreconditionRequiredHttpExceptionTest extends HttpExceptionTest { protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new PreconditionRequiredHttpException($message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/ServiceUnavailableHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; class ServiceUnavailableHttpExceptionTest extends HttpExceptionTest { public function testHeadersDefaultRetryAfter() { $exception = new ServiceUnavailableHttpException(10); $this->assertSame(['Retry-After' => 10], $exception->getHeaders()); } public function testWithHeaderConstruct() { $headers = [ 'Cache-Control' => 'public, s-maxage=1337', ]; $exception = new ServiceUnavailableHttpException(1337, '', null, 0, $headers); $headers['Retry-After'] = 1337; $this->assertSame($headers, $exception->getHeaders()); } #[DataProvider('headerDataProvider')] public function testHeadersSetter($headers) { $exception = new ServiceUnavailableHttpException(10); $exception->setHeaders($headers); $this->assertSame($headers, $exception->getHeaders()); } protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new ServiceUnavailableHttpException(null, $message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/TooManyRequestsHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; class TooManyRequestsHttpExceptionTest extends HttpExceptionTest { public function testHeadersDefaultRertyAfter() { $exception = new TooManyRequestsHttpException(10); $this->assertSame(['Retry-After' => 10], $exception->getHeaders()); } public function testWithHeaderConstruct() { $headers = [ 'Cache-Control' => 'public, s-maxage=69', ]; $exception = new TooManyRequestsHttpException(69, '', null, 0, $headers); $headers['Retry-After'] = 69; $this->assertSame($headers, $exception->getHeaders()); } #[DataProvider('headerDataProvider')] public function testHeadersSetter($headers) { $exception = new TooManyRequestsHttpException(10); $exception->setHeaders($headers); $this->assertSame($headers, $exception->getHeaders()); } protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new TooManyRequestsHttpException(null, $message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/UnauthorizedHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; class UnauthorizedHttpExceptionTest extends HttpExceptionTest { public function testHeadersDefault() { $exception = new UnauthorizedHttpException('Challenge'); $this->assertSame(['WWW-Authenticate' => 'Challenge'], $exception->getHeaders()); } public function testWithHeaderConstruct() { $headers = [ 'Cache-Control' => 'public, s-maxage=1200', ]; $exception = new UnauthorizedHttpException('Challenge', '', null, 0, $headers); $headers['WWW-Authenticate'] = 'Challenge'; $this->assertSame($headers, $exception->getHeaders()); } #[DataProvider('headerDataProvider')] public function testHeadersSetter($headers) { $exception = new UnauthorizedHttpException('Challenge'); $exception->setHeaders($headers); $this->assertSame($headers, $exception->getHeaders()); } protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new UnauthorizedHttpException('Challenge', $message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/UnprocessableEntityHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; class UnprocessableEntityHttpExceptionTest extends HttpExceptionTest { protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new UnprocessableEntityHttpException($message, $previous, $code, $headers); } } ================================================ FILE: Tests/Exception/UnsupportedMediaTypeHttpExceptionTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Exception; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; class UnsupportedMediaTypeHttpExceptionTest extends HttpExceptionTest { protected function createException(string $message = '', ?\Throwable $previous = null, int $code = 0, array $headers = []): HttpException { return new UnsupportedMediaTypeHttpException($message, $previous, $code, $headers); } } ================================================ FILE: Tests/Fixtures/AcmeFooBundle/AcmeFooBundle.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\AcmeFooBundle; use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; class AcmeFooBundle extends AbstractBundle { public function configure(DefinitionConfigurator $definition): void { $definition->rootNode() ->children() ->scalarNode('foo')->defaultValue('bar')->end() ->end() ; $definition->import('Resources/config/definition.php'); } public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void { $builder->prependExtensionConfig('loaded', ['bar' => 'baz']); } public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { $container->parameters() ->set('acme_foo.config', $config) ; $container->services() ->set('acme_foo.foo', \stdClass::class) ; $container->import('Resources/config/services.php'); } } ================================================ FILE: Tests/Fixtures/AcmeFooBundle/Resources/config/definition.php ================================================ rootNode() ->children() ->scalarNode('ping')->defaultValue('pong')->end() ->end() ; }; ================================================ FILE: Tests/Fixtures/AcmeFooBundle/Resources/config/services.php ================================================ services() ->set('acme_foo.bar', \stdClass::class) ; }; ================================================ FILE: Tests/Fixtures/Attribute/Bar.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Attribute; #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::IS_REPEATABLE)] class Bar { public function __construct( public mixed $foo, ) { } } ================================================ FILE: Tests/Fixtures/Attribute/Baz.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Attribute; #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::IS_REPEATABLE)] class Baz { } ================================================ FILE: Tests/Fixtures/Attribute/Buz.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Attribute; #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class Buz { } ================================================ FILE: Tests/Fixtures/Attribute/Foo.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Attribute; #[\Attribute(\Attribute::TARGET_PARAMETER)] class Foo { private $foo; public function __construct($foo) { $this->foo = $foo; } } ================================================ FILE: Tests/Fixtures/Attribute/Qux.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Attribute; #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class Qux { } ================================================ FILE: Tests/Fixtures/Attribute/SubBuz.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Attribute; #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class SubBuz extends Buz { } ================================================ FILE: Tests/Fixtures/Bundle1Bundle/Resources/.gitkeep ================================================ ================================================ FILE: Tests/Fixtures/Bundle1Bundle/foo.txt ================================================ ================================================ FILE: Tests/Fixtures/BundleCompilerPass/BundleAsCompilerPassBundle.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\BundleCompilerPass; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\AbstractBundle; class BundleAsCompilerPassBundle extends AbstractBundle implements CompilerPassInterface { public function process(ContainerBuilder $container): void { $container->register('foo', \stdClass::class) ->setPublic(true); } } ================================================ FILE: Tests/Fixtures/ClearableService.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Bar; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Baz; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo; #[Bar('class'), Undefined('class')] class AttributeController { #[Bar('method'), Baz, Undefined('method')] public function __invoke() { } public function action(#[Foo('bar')] string $baz) { } public function multiAttributeArg(#[Foo('bar'), Undefined('bar')] string $baz) { } public function issue41478(#[Foo('bar')] string $baz, string $bat) { } } ================================================ FILE: Tests/Fixtures/Controller/BasicTypesController.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; class BasicTypesController { public function action(string $foo, int $bar, float $baz) { } } ================================================ FILE: Tests/Fixtures/Controller/CacheAttributeController.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; use Symfony\Component\HttpKernel\Attribute\Cache; #[Cache(smaxage: 20)] class CacheAttributeController { public const CLASS_SMAXAGE = 20; public const METHOD_SMAXAGE = 25; #[Cache(smaxage: 25)] public function foo() { } public function bar() { } } ================================================ FILE: Tests/Fixtures/Controller/ControllerAttributesController.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Buz; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Qux; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\SubBuz; #[Buz] class ControllerAttributesController { #[Buz] #[Qux] public function buzQuxAction() { } #[Buz] public function buzAction() { } #[SubBuz] public function subBuzAction() { } public function noAttributeAction() { } } ================================================ FILE: Tests/Fixtures/Controller/ExtendingRequest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; use Symfony\Component\HttpFoundation\Request; class ExtendingRequest extends Request { } ================================================ FILE: Tests/Fixtures/Controller/ExtendingSession.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; use Symfony\Component\HttpFoundation\Session\Session; class ExtendingSession extends Session { } ================================================ FILE: Tests/Fixtures/Controller/NullableController.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; class NullableController { public function action(?string $foo, ?\stdClass $bar, ?string $baz = 'value', string $last = '') { } } ================================================ FILE: Tests/Fixtures/Controller/VariadicController.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller; class VariadicController { public function action($foo, ...$bar) { } } ================================================ FILE: Tests/Fixtures/DataCollector/CloneVarDataCollector.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\DataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\VarDumper\Cloner\Data; final class CloneVarDataCollector extends DataCollector { private $varToClone; public function __construct($varToClone) { $this->varToClone = $varToClone; } public function collect(Request $request, Response $response, ?\Throwable $exception = null): void { $this->data = $this->cloneVar($this->varToClone); } public function getData(): Data { return $this->data; } public function getName(): string { return 'clone_var'; } } ================================================ FILE: Tests/Fixtures/DataCollector/DummyController.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\DataCollector; class DummyController { /** * Dummy method used as controller callable. */ public static function staticControllerMethod() { throw new \LogicException('Unexpected method call'); } /** * Magic method to allow non existing methods to be called and delegated. */ public function __call(string $method, array $args) { throw new \LogicException('Unexpected method call'); } /** * Magic method to allow non existing methods to be called and delegated. */ public static function __callStatic(string $method, array $args) { throw new \LogicException('Unexpected method call'); } public function __invoke() { throw new \LogicException('Unexpected method call'); } public function regularCallable() { throw new \LogicException('Unexpected method call'); } } ================================================ FILE: Tests/Fixtures/ExtensionNotValidBundle/DependencyInjection/ExtensionNotValidExtension.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionNotValidBundle\DependencyInjection; class ExtensionNotValidExtension { public function getAlias() { return 'extension_not_valid'; } } ================================================ FILE: Tests/Fixtures/ExtensionNotValidBundle/ExtensionNotValidBundle.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionNotValidBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; class ExtensionNotValidBundle extends Bundle { } ================================================ FILE: Tests/Fixtures/ExtensionPresentBundle/DependencyInjection/ExtensionPresentExtension.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; class ExtensionPresentExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { } } ================================================ FILE: Tests/Fixtures/ExtensionPresentBundle/ExtensionPresentBundle.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionPresentBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; class ExtensionPresentBundle extends Bundle { } ================================================ FILE: Tests/Fixtures/IntEnum.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures; enum IntEnum: int { case One = 1; case Two = 2; } ================================================ FILE: Tests/Fixtures/IsSignatureValidAttributeController.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures; use Symfony\Component\HttpKernel\Attribute\IsSignatureValid; #[IsSignatureValid] class IsSignatureValidAttributeController { public function __invoke() { } } ================================================ FILE: Tests/Fixtures/IsSignatureValidAttributeMethodsController.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures; use Symfony\Component\HttpKernel\Attribute\IsSignatureValid; class IsSignatureValidAttributeMethodsController { public function noAttribute() { } #[IsSignatureValid] public function withDefaultBehavior() { } #[IsSignatureValid] #[IsSignatureValid] public function withMultiple() { } #[IsSignatureValid(methods: 'POST')] public function withPostOnly() { } #[IsSignatureValid(methods: ['GET', 'POST'])] public function withGetAndPost() { } } ================================================ FILE: Tests/Fixtures/KernelWithoutBundles.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel; class KernelWithoutBundles extends Kernel { public function registerBundles(): iterable { return []; } public function registerContainerConfiguration(LoaderInterface $loader): void { } public function getProjectDir(): string { return __DIR__; } protected function build(ContainerBuilder $container): void { $container->setParameter('test_executed', true); } } ================================================ FILE: Tests/Fixtures/LazyResettableService.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures; class LazyResettableService extends \stdClass { public static $counter = 0; public function foo(): bool { return true; } public function reset(): void { ++self::$counter; } } ================================================ FILE: Tests/Fixtures/MockableUploadFileWithClientSize.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures; use Symfony\Component\HttpFoundation\File\UploadedFile; class MockableUploadFileWithClientSize extends UploadedFile { public function getClientSize(): int { return 0; } } ================================================ FILE: Tests/Fixtures/MultiResettableService.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures; enum Suit: string { case Hearts = 'H'; case Diamonds = 'D'; case Clubs = 'C'; case Spades = 'S'; } ================================================ FILE: Tests/Fixtures/TestClient.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fixtures; use Symfony\Component\HttpKernel\HttpKernelBrowser; class TestClient extends HttpKernelBrowser { protected function getScript($request): string { $script = parent::getScript($request); $autoload = file_exists(__DIR__.'/../../vendor/autoload.php') ? __DIR__.'/../../vendor/autoload.php' : __DIR__.'/../../../../../../vendor/autoload.php' ; $script = preg_replace('/(\->register\(\);)/', "$0\nrequire_once '$autoload';\n", $script); return $script; } } ================================================ FILE: Tests/Fixtures/UsePropertyInDestruct.php ================================================ parent !== null) { $this->parent->name = ''; } } } ================================================ FILE: Tests/Fixtures/WithPublicObjectProperty.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fragment; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Fragment\EsiFragmentRenderer; use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer; use Symfony\Component\HttpKernel\HttpCache\Esi; class EsiFragmentRendererTest extends TestCase { public function testRenderFallbackToInlineStrategyIfEsiNotSupported() { $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy(true)); $strategy->render('/', Request::create('/')); } public function testRenderFallbackWithScalar() { $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy(true), new UriSigner('foo')); $request = Request::create('/'); $reference = new ControllerReference('main_controller', ['foo' => [true]], []); $strategy->render($reference, $request); } public function testRender() { $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy()); $request = Request::create('/'); $request->setLocale('fr'); $request->headers->set('Surrogate-Capability', 'ESI/1.0'); $this->assertEquals('', $strategy->render('/', $request)->getContent()); $this->assertEquals("\n", $strategy->render('/', $request, ['comment' => 'This is a comment'])->getContent()); $this->assertEquals('', $strategy->render('/', $request, ['alt' => 'foo'])->getContent()); } public function testRenderControllerReference() { $signer = new UriSigner('foo'); $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy(), $signer); $request = Request::create('/'); $request->setLocale('fr'); $request->headers->set('Surrogate-Capability', 'ESI/1.0'); $reference = new ControllerReference('main_controller', [], []); $altReference = new ControllerReference('alt_controller', [], []); $this->assertMatchesRegularExpression( '#^$#', $strategy->render($reference, $request, ['alt' => $altReference])->getContent() ); } public function testRenderControllerReferenceWithAbsoluteUri() { $signer = new UriSigner('foo'); $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy(), $signer); $request = Request::create('http://localhost/'); $request->setLocale('fr'); $request->headers->set('Surrogate-Capability', 'ESI/1.0'); $reference = new ControllerReference('main_controller', [], []); $altReference = new ControllerReference('alt_controller', [], []); $this->assertMatchesRegularExpression( '#^$#', $strategy->render($reference, $request, ['alt' => $altReference, 'absolute_uri' => true])->getContent() ); } public function testRenderControllerReferenceWithoutSignerThrowsException() { $this->expectException(\LogicException::class); $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy()); $request = Request::create('/'); $request->setLocale('fr'); $request->headers->set('Surrogate-Capability', 'ESI/1.0'); $strategy->render(new ControllerReference('main_controller'), $request); } public function testRenderAltControllerReferenceWithoutSignerThrowsException() { $this->expectException(\LogicException::class); $strategy = new EsiFragmentRenderer(new Esi(), $this->getInlineStrategy()); $request = Request::create('/'); $request->setLocale('fr'); $request->headers->set('Surrogate-Capability', 'ESI/1.0'); $strategy->render('/', $request, ['alt' => new ControllerReference('alt_controller')]); } private function getInlineStrategy($called = false) { if (!$called) { return $this->createStub(InlineFragmentRenderer::class); } $inline = $this->createMock(InlineFragmentRenderer::class); $inline->expects($this->once())->method('render'); return $inline; } } ================================================ FILE: Tests/Fragment/FragmentHandlerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fragment; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Fragment\FragmentHandler; use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface; class FragmentHandlerTest extends TestCase { private RequestStack $requestStack; protected function setUp(): void { $this->requestStack = new RequestStack(); $this->requestStack->push(Request::create('/')); } public function testRenderWhenRendererDoesNotExist() { $this->expectException(\InvalidArgumentException::class); $handler = new FragmentHandler($this->requestStack); $handler->render('/', 'foo'); } public function testRenderWithUnknownRenderer() { $this->expectException(\InvalidArgumentException::class); $handler = $this->getHandler(new Response('foo')); $handler->render('/', 'bar'); } public function testDeliverWithUnsuccessfulResponse() { $handler = $this->getHandler(new Response('foo', 404)); try { $handler->render('/', 'foo'); $this->fail('->render() throws a \RuntimeException exception if response is not successful'); } catch (\Exception $e) { $this->assertInstanceOf(\RuntimeException::class, $e); $this->assertEquals(0, $e->getCode()); $this->assertEquals('Error when rendering "http://localhost/" (Status code is 404).', $e->getMessage()); $previousException = $e->getPrevious(); $this->assertInstanceOf(HttpException::class, $previousException); $this->assertEquals(404, $previousException->getStatusCode()); $this->assertEquals(0, $previousException->getCode()); } } public function testRender() { $expectedRequest = Request::create('/'); $handler = $this->getHandler( new Response('foo'), [ '/', $this->callback(static function (Request $request) use ($expectedRequest) { $expectedRequest->server->remove('REQUEST_TIME_FLOAT'); $request->server->remove('REQUEST_TIME_FLOAT'); return $expectedRequest == $request; }), ['foo' => 'foo', 'ignore_errors' => true], ] ); $this->assertEquals('foo', $handler->render('/', 'foo', ['foo' => 'foo'])); } protected function getHandler($returnValue, $arguments = []) { if ($arguments) { $renderer = $this->createMock(FragmentRendererInterface::class); $renderer->method('getName')->willReturn('foo'); $renderer ->expects($this->once()) ->method('render') ->with(...$arguments) ->willReturn($returnValue); } else { $renderer = $this->createStub(FragmentRendererInterface::class); $renderer->method('getName')->willReturn('foo'); $renderer ->method('render') ->willReturn($returnValue); } $handler = new FragmentHandler($this->requestStack); $handler->addRenderer($renderer); return $handler; } } ================================================ FILE: Tests/Fragment/HIncludeFragmentRendererTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fragment; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Fragment\HIncludeFragmentRenderer; use Twig\Environment; use Twig\Loader\ArrayLoader; class HIncludeFragmentRendererTest extends TestCase { public function testRenderExceptionWhenControllerAndNoSigner() { $this->expectException(\LogicException::class); $strategy = new HIncludeFragmentRenderer(); $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/')); } public function testRenderWithControllerAndSigner() { $strategy = new HIncludeFragmentRenderer(null, new UriSigner('foo')); $this->assertMatchesRegularExpression('#^$#', $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/'))->getContent()); } public function testRenderWithUri() { $strategy = new HIncludeFragmentRenderer(); $this->assertEquals('', $strategy->render('/foo', Request::create('/'))->getContent()); $strategy = new HIncludeFragmentRenderer(null, new UriSigner('foo')); $this->assertEquals('', $strategy->render('/foo', Request::create('/'))->getContent()); } public function testRenderWithDefault() { // only default $strategy = new HIncludeFragmentRenderer(); $this->assertEquals('default', $strategy->render('/foo', Request::create('/'), ['default' => 'default'])->getContent()); // only global default $strategy = new HIncludeFragmentRenderer(null, null, 'global_default'); $this->assertEquals('global_default', $strategy->render('/foo', Request::create('/'), [])->getContent()); // global default and default $strategy = new HIncludeFragmentRenderer(null, null, 'global_default'); $this->assertEquals('default', $strategy->render('/foo', Request::create('/'), ['default' => 'default'])->getContent()); } public function testRenderWithAttributesOptions() { // with id $strategy = new HIncludeFragmentRenderer(); $this->assertEquals('default', $strategy->render('/foo', Request::create('/'), ['default' => 'default', 'id' => 'bar'])->getContent()); // with attributes $strategy = new HIncludeFragmentRenderer(); $this->assertEquals('default', $strategy->render('/foo', Request::create('/'), ['default' => 'default', 'attributes' => ['p1' => 'v1', 'p2' => 'v2']])->getContent()); // with id & attributes $strategy = new HIncludeFragmentRenderer(); $this->assertEquals('default', $strategy->render('/foo', Request::create('/'), ['default' => 'default', 'id' => 'bar', 'attributes' => ['p1' => 'v1', 'p2' => 'v2']])->getContent()); } public function testRenderWithTwigAndDefaultText() { $twig = new Environment($loader = new ArrayLoader()); $strategy = new HIncludeFragmentRenderer($twig); $this->assertEquals('loading...', $strategy->render('/foo', Request::create('/'), ['default' => 'loading...'])->getContent()); } } ================================================ FILE: Tests/Fragment/InlineFragmentRendererTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fragment; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; #[Group('time-sensitive')] class InlineFragmentRendererTest extends TestCase { public function testRender() { $strategy = new InlineFragmentRenderer($this->getKernel(new Response('foo'))); $this->assertEquals('foo', $strategy->render('/', Request::create('/'))->getContent()); } public function testRenderWithControllerReference() { $strategy = new InlineFragmentRenderer($this->getKernel(new Response('foo'))); $this->assertEquals('foo', $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/'))->getContent()); } public function testRenderWithObjectsAsAttributes() { $object = new \stdClass(); $subRequest = Request::create('/_fragment?_path=_format%3Dhtml%26_locale%3Den%26_controller%3Dmain_controller'); $subRequest->attributes->replace(['object' => $object, '_format' => 'html', '_controller' => 'main_controller', '_locale' => 'en']); $subRequest->headers->set('x-forwarded-for', ['127.0.0.1']); $subRequest->headers->set('forwarded', ['for="127.0.0.1";host="localhost";proto=http']); $subRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1'); $subRequest->server->set('HTTP_FORWARDED', 'for="127.0.0.1";host="localhost";proto=http'); $strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($subRequest)); $this->assertSame('foo', $strategy->render(new ControllerReference('main_controller', ['object' => $object], []), Request::create('/'))->getContent()); } public function testRenderWithTrustedHeaderDisabled() { Request::setTrustedProxies([], 0); $expectedSubRequest = Request::create('/'); $expectedSubRequest->headers->set('x-forwarded-for', ['127.0.0.1']); $expectedSubRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1'); $strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest)); $this->assertSame('foo', $strategy->render('/', Request::create('/'))->getContent()); Request::setTrustedProxies([], -1); } public function testRenderExceptionNoIgnoreErrors() { $this->expectException(\RuntimeException::class); $dispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher->expects($this->never())->method('dispatch'); $strategy = new InlineFragmentRenderer($this->getKernel(new \RuntimeException('foo')), $dispatcher); $this->assertEquals('foo', $strategy->render('/', Request::create('/'))->getContent()); } public function testRenderExceptionIgnoreErrors() { $exception = new \RuntimeException('foo'); $kernel = $this->getKernel($exception); $request = Request::create('/'); $expectedEvent = new ExceptionEvent($kernel, $request, $kernel::SUB_REQUEST, $exception); $dispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher->expects($this->once())->method('dispatch')->with($expectedEvent, KernelEvents::EXCEPTION); $strategy = new InlineFragmentRenderer($kernel, $dispatcher); $this->assertSame('', $strategy->render('/', $request, ['ignore_errors' => true])->getContent()); } public function testRenderExceptionIgnoreErrorsWithAlt() { $strategy = new InlineFragmentRenderer($this->getKernel(static function () { static $firstCall = true; if ($firstCall) { $firstCall = false; throw new \RuntimeException('foo'); } return new Response('bar'); })); $this->assertEquals('bar', $strategy->render('/', Request::create('/'), ['ignore_errors' => true, 'alt' => '/foo'])->getContent()); } private function getKernel($returnValue) { $kernel = $this->createStub(HttpKernelInterface::class); $mocker = $kernel ->method('handle') ; if ($returnValue instanceof \Exception) { $mocker->willThrowException($returnValue); } elseif ($returnValue instanceof \Closure) { $mocker->willReturnCallback($returnValue); } else { $mocker->willReturn(...(\is_array($returnValue) ? $returnValue : [$returnValue])); } return $kernel; } public function testExceptionInSubRequestsDoesNotMangleOutputBuffers() { $controllerResolver = $this->createMock(ControllerResolverInterface::class); $controllerResolver ->expects($this->once()) ->method('getController') ->willReturn(static function () { ob_start(); echo 'bar'; throw new \RuntimeException(); }) ; $argumentResolver = $this->createMock(ArgumentResolverInterface::class); $argumentResolver ->expects($this->once()) ->method('getArguments') ->willReturn([]) ; $kernel = new HttpKernel(new EventDispatcher(), $controllerResolver, new RequestStack(), $argumentResolver); $renderer = new InlineFragmentRenderer($kernel); // simulate a main request with output buffering ob_start(); echo 'Foo'; // simulate a sub-request with output buffering and an exception $renderer->render('/', Request::create('/'), ['ignore_errors' => true]); $this->assertEquals('Foo', ob_get_clean()); } public function testLocaleAndFormatAreKeptInSubrequest() { $expectedSubRequest = Request::create('/'); $expectedSubRequest->attributes->set('_format', 'foo'); $expectedSubRequest->setLocale('fr'); if (Request::HEADER_X_FORWARDED_FOR & Request::getTrustedHeaderSet()) { $expectedSubRequest->headers->set('x-forwarded-for', ['127.0.0.1']); $expectedSubRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1'); } $expectedSubRequest->headers->set('forwarded', ['for="127.0.0.1";host="localhost";proto=http']); $expectedSubRequest->server->set('HTTP_FORWARDED', 'for="127.0.0.1";host="localhost";proto=http'); $strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest)); $request = Request::create('/'); $request->attributes->set('_format', 'foo'); $request->setLocale('fr'); $strategy->render('/', $request); } public function testESIHeaderIsKeptInSubrequest() { $expectedSubRequest = Request::create('/'); $expectedSubRequest->headers->set('Surrogate-Capability', 'abc="ESI/1.0"'); if (Request::HEADER_X_FORWARDED_FOR & Request::getTrustedHeaderSet()) { $expectedSubRequest->headers->set('x-forwarded-for', ['127.0.0.1']); $expectedSubRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1'); } $expectedSubRequest->headers->set('forwarded', ['for="127.0.0.1";host="localhost";proto=http']); $expectedSubRequest->server->set('HTTP_FORWARDED', 'for="127.0.0.1";host="localhost";proto=http'); $strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest)); $request = Request::create('/'); $request->headers->set('Surrogate-Capability', 'abc="ESI/1.0"'); $strategy->render('/', $request); } public function testESIHeaderIsKeptInSubrequestWithTrustedHeaderDisabled() { Request::setTrustedProxies([], Request::HEADER_FORWARDED); $this->testESIHeaderIsKeptInSubrequest(); Request::setTrustedProxies([], -1); } public function testHeadersPossiblyResultingIn304AreNotAssignedToSubrequest() { $expectedSubRequest = Request::create('/'); $expectedSubRequest->headers->set('x-forwarded-for', ['127.0.0.1']); $expectedSubRequest->headers->set('forwarded', ['for="127.0.0.1";host="localhost";proto=http']); $expectedSubRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1'); $expectedSubRequest->server->set('HTTP_FORWARDED', 'for="127.0.0.1";host="localhost";proto=http'); $strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest)); $request = Request::create('/', 'GET', [], [], [], ['HTTP_IF_MODIFIED_SINCE' => 'Fri, 01 Jan 2016 00:00:00 GMT', 'HTTP_IF_NONE_MATCH' => '*']); $strategy->render('/', $request); } public function testFirstTrustedProxyIsSetAsRemote() { Request::setTrustedProxies(['1.1.1.1'], -1); $expectedSubRequest = Request::create('/'); $expectedSubRequest->headers->set('Surrogate-Capability', 'abc="ESI/1.0"'); $expectedSubRequest->server->set('REMOTE_ADDR', '127.0.0.1'); $expectedSubRequest->headers->set('x-forwarded-for', ['127.0.0.1']); $expectedSubRequest->headers->set('forwarded', ['for="127.0.0.1";host="localhost";proto=http']); $expectedSubRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1'); $expectedSubRequest->server->set('HTTP_FORWARDED', 'for="127.0.0.1";host="localhost";proto=http'); $strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest)); $request = Request::create('/'); $request->headers->set('Surrogate-Capability', 'abc="ESI/1.0"'); $strategy->render('/', $request); Request::setTrustedProxies([], -1); } public function testIpAddressOfRangedTrustedProxyIsSetAsRemote() { $expectedSubRequest = Request::create('/'); $expectedSubRequest->headers->set('Surrogate-Capability', 'abc="ESI/1.0"'); $expectedSubRequest->server->set('REMOTE_ADDR', '127.0.0.1'); $expectedSubRequest->headers->set('x-forwarded-for', ['127.0.0.1']); $expectedSubRequest->headers->set('forwarded', ['for="127.0.0.1";host="localhost";proto=http']); $expectedSubRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1'); $expectedSubRequest->server->set('HTTP_FORWARDED', 'for="127.0.0.1";host="localhost";proto=http'); Request::setTrustedProxies(['1.1.1.1/24'], -1); $strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest)); $request = Request::create('/'); $request->headers->set('Surrogate-Capability', 'abc="ESI/1.0"'); $strategy->render('/', $request); Request::setTrustedProxies([], -1); } public function testStatelessAttributeIsForwardedByDefault() { $request = Request::create('/'); $request->attributes->set('_stateless', true); $kernel = $this->createMock(HttpKernelInterface::class); $kernel ->expects($this->once()) ->method('handle') ->with($this->callback(static fn (Request $subRequest) => $subRequest->attributes->get('_stateless'))) ; $strategy = new InlineFragmentRenderer($kernel); $strategy->render('/', $request); } public function testStatelessAttributeCanBeDisabled() { $request = Request::create('/'); $request->attributes->set('_stateless', true); $kernel = $this->createMock(HttpKernelInterface::class); $kernel ->expects($this->once()) ->method('handle') ->with($this->callback(static fn (Request $subRequest) => !$subRequest->attributes->get('_stateless'))) ; $strategy = new InlineFragmentRenderer($kernel); $strategy->render(new ControllerReference('main_controller', ['_stateless' => false]), $request); } /** * Creates a Kernel expecting a request equals to $request. */ private function getKernelExpectingRequest(Request $expectedRequest) { $kernel = $this->createMock(HttpKernelInterface::class); $kernel ->expects($this->once()) ->method('handle') ->with($this->callback(static function (Request $request) use ($expectedRequest) { $expectedRequest->server->remove('REQUEST_TIME_FLOAT'); $request->server->remove('REQUEST_TIME_FLOAT'); return $expectedRequest == $request; })) ->willReturn(new Response('foo')); return $kernel; } } class Bar { public string $bar = 'bar'; public function getBar() { return $this->bar; } } ================================================ FILE: Tests/Fragment/RoutableFragmentRendererTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fragment; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Fragment\RoutableFragmentRenderer; class RoutableFragmentRendererTest extends TestCase { #[DataProvider('getGenerateFragmentUriData')] public function testGenerateFragmentUri($uri, $controller) { $this->assertEquals($uri, $this->callGenerateFragmentUriMethod($controller, Request::create('/'))); } #[DataProvider('getGenerateFragmentUriData')] public function testGenerateAbsoluteFragmentUri($uri, $controller) { $this->assertEquals('http://localhost'.$uri, $this->callGenerateFragmentUriMethod($controller, Request::create('/'), true)); } public static function getGenerateFragmentUriData() { return [ ['/_fragment?_path=_format%3Dhtml%26_locale%3Den%26_controller%3Dcontroller', new ControllerReference('controller', [], [])], ['/_fragment?_path=_format%3Dxml%26_locale%3Den%26_controller%3Dcontroller', new ControllerReference('controller', ['_format' => 'xml'], [])], ['/_fragment?_path=foo%3Dfoo%26_format%3Djson%26_locale%3Den%26_controller%3Dcontroller', new ControllerReference('controller', ['foo' => 'foo', '_format' => 'json'], [])], ['/_fragment?bar=bar&_path=foo%3Dfoo%26_format%3Dhtml%26_locale%3Den%26_controller%3Dcontroller', new ControllerReference('controller', ['foo' => 'foo'], ['bar' => 'bar'])], ['/_fragment?foo=foo&_path=_format%3Dhtml%26_locale%3Den%26_controller%3Dcontroller', new ControllerReference('controller', [], ['foo' => 'foo'])], ['/_fragment?_path=foo%255B0%255D%3Dfoo%26foo%255B1%255D%3Dbar%26_format%3Dhtml%26_locale%3Den%26_controller%3Dcontroller', new ControllerReference('controller', ['foo' => ['foo', 'bar']], [])], ]; } public function testGenerateFragmentUriWithARequest() { $request = Request::create('/'); $request->attributes->set('_format', 'json'); $request->setLocale('fr'); $controller = new ControllerReference('controller', [], []); $this->assertEquals('/_fragment?_path=_format%3Djson%26_locale%3Dfr%26_controller%3Dcontroller', $this->callGenerateFragmentUriMethod($controller, $request)); } #[DataProvider('getGenerateFragmentUriDataWithNonScalar')] public function testGenerateFragmentUriWithNonScalar($controller) { $this->expectException(\LogicException::class); $this->callGenerateFragmentUriMethod($controller, Request::create('/')); } public static function getGenerateFragmentUriDataWithNonScalar() { return [ [new ControllerReference('controller', ['foo' => new Foo(), 'bar' => 'bar'], [])], [new ControllerReference('controller', ['foo' => ['foo' => 'foo'], 'bar' => ['bar' => new Foo()]], [])], ]; } private function callGenerateFragmentUriMethod(ControllerReference $reference, Request $request, $absolute = false) { $renderer = $this->createStub(RoutableFragmentRenderer::class); $r = new \ReflectionObject($renderer); $m = $r->getMethod('generateFragmentUri'); return $m->invoke($renderer, $reference, $request, $absolute); } } class Foo { public $foo; public function getFoo() { return $this->foo; } } ================================================ FILE: Tests/Fragment/SsiFragmentRendererTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Fragment; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\UriSigner; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer; use Symfony\Component\HttpKernel\Fragment\SsiFragmentRenderer; use Symfony\Component\HttpKernel\HttpCache\Ssi; class SsiFragmentRendererTest extends TestCase { public function testRenderFallbackToInlineStrategyIfSsiNotSupported() { $strategy = new SsiFragmentRenderer(new Ssi(), $this->getInlineStrategy(true)); $strategy->render('/', Request::create('/')); } public function testRender() { $strategy = new SsiFragmentRenderer(new Ssi(), $this->getInlineStrategy()); $request = Request::create('/'); $request->setLocale('fr'); $request->headers->set('Surrogate-Capability', 'SSI/1.0'); $this->assertEquals('', $strategy->render('/', $request)->getContent()); $this->assertEquals('', $strategy->render('/', $request, ['comment' => 'This is a comment'])->getContent(), 'Strategy options should not impact the ssi include tag'); } public function testRenderControllerReference() { $signer = new UriSigner('foo'); $strategy = new SsiFragmentRenderer(new Ssi(), $this->getInlineStrategy(), $signer); $request = Request::create('/'); $request->setLocale('fr'); $request->headers->set('Surrogate-Capability', 'SSI/1.0'); $reference = new ControllerReference('main_controller', [], []); $altReference = new ControllerReference('alt_controller', [], []); $this->assertMatchesRegularExpression( '{^$}', $strategy->render($reference, $request, ['alt' => $altReference])->getContent() ); } public function testRenderControllerReferenceWithAbsoluteUri() { $signer = new UriSigner('foo'); $strategy = new SsiFragmentRenderer(new Ssi(), $this->getInlineStrategy(), $signer); $request = Request::create('http://localhost/'); $request->setLocale('fr'); $request->headers->set('Surrogate-Capability', 'SSI/1.0'); $reference = new ControllerReference('main_controller', [], []); $altReference = new ControllerReference('alt_controller', [], []); $this->assertMatchesRegularExpression( '{^$}', $strategy->render($reference, $request, ['alt' => $altReference, 'absolute_uri' => true])->getContent() ); } public function testRenderControllerReferenceWithoutSignerThrowsException() { $this->expectException(\LogicException::class); $strategy = new SsiFragmentRenderer(new Ssi(), $this->getInlineStrategy()); $request = Request::create('/'); $request->setLocale('fr'); $request->headers->set('Surrogate-Capability', 'SSI/1.0'); $strategy->render(new ControllerReference('main_controller'), $request); } public function testRenderAltControllerReferenceWithoutSignerThrowsException() { $this->expectException(\LogicException::class); $strategy = new SsiFragmentRenderer(new Ssi(), $this->getInlineStrategy()); $request = Request::create('/'); $request->setLocale('fr'); $request->headers->set('Surrogate-Capability', 'SSI/1.0'); $strategy->render('/', $request, ['alt' => new ControllerReference('alt_controller')]); } private function getInlineStrategy($called = false) { if (!$called) { return $this->createStub(InlineFragmentRenderer::class); } $inline = $this->createMock(InlineFragmentRenderer::class); $inline->expects($this->once())->method('render'); return $inline; } } ================================================ FILE: Tests/HttpCache/EsiTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\HttpCache; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpCache\Esi; use Symfony\Component\HttpKernel\HttpCache\HttpCache; class EsiTest extends TestCase { public function testHasSurrogateEsiCapability() { $esi = new Esi(); $request = Request::create('/'); $request->headers->set('Surrogate-Capability', 'abc="ESI/1.0"'); $this->assertTrue($esi->hasSurrogateCapability($request)); $request = Request::create('/'); $request->headers->set('Surrogate-Capability', 'foobar'); $this->assertFalse($esi->hasSurrogateCapability($request)); $request = Request::create('/'); $this->assertFalse($esi->hasSurrogateCapability($request)); } public function testAddSurrogateEsiCapability() { $esi = new Esi(); $request = Request::create('/'); $esi->addSurrogateCapability($request); $this->assertEquals('symfony="ESI/1.0"', $request->headers->get('Surrogate-Capability')); $esi->addSurrogateCapability($request); $this->assertEquals('symfony="ESI/1.0", symfony="ESI/1.0"', $request->headers->get('Surrogate-Capability')); } public function testAddSurrogateControl() { $esi = new Esi(); $response = new Response('foo '); $esi->addSurrogateControl($response); $this->assertEquals('content="ESI/1.0"', $response->headers->get('Surrogate-Control')); $response = new Response('foo'); $esi->addSurrogateControl($response); $this->assertEquals('', $response->headers->get('Surrogate-Control')); } public function testNeedsEsiParsing() { $esi = new Esi(); $response = new Response(); $response->headers->set('Surrogate-Control', 'content="ESI/1.0"'); $this->assertTrue($esi->needsParsing($response)); $response = new Response(); $this->assertFalse($esi->needsParsing($response)); } public function testRenderIncludeTag() { $esi = new Esi(); $this->assertEquals('', $esi->renderIncludeTag('/', '/alt', true)); $this->assertEquals('', $esi->renderIncludeTag('/', '/alt', false)); $this->assertEquals('', $esi->renderIncludeTag('/')); $this->assertEquals(''."\n".'', $esi->renderIncludeTag('/', '/alt', true, 'some comment')); } public function testProcessDoesNothingIfContentTypeIsNotHtml() { $esi = new Esi(); $request = Request::create('/'); $response = new Response(); $response->headers->set('Content-Type', 'text/plain'); $this->assertSame($response, $esi->process($request, $response)); $this->assertFalse($response->headers->has('x-body-eval')); } public function testMultilineEsiRemoveTagsAreRemoved() { $esi = new Esi(); $request = Request::create('/'); $response = new Response(' www.example.com Keep this'."\n www.example.com And this"); $this->assertSame($response, $esi->process($request, $response)); $this->assertEquals(' Keep this And this', substr($response->getContent(), 24, -24)); } public function testCommentTagsAreRemoved() { $esi = new Esi(); $request = Request::create('/'); $response = new Response(' Keep this'); $this->assertSame($response, $esi->process($request, $response)); $this->assertEquals(' Keep this', substr($response->getContent(), 24, -24)); } public function testProcess() { $esi = new Esi(); $request = Request::create('/'); $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); $this->assertSame(['', 'foo ', "...\nalt\n1\n", ''], $content); $this->assertEquals('ESI', $response->headers->get('x-body-eval')); $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); $this->assertSame(['', 'foo ', "foo'\nbar'\n1\n", ''], $content); $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); $this->assertSame(['', 'foo ', "...\n\n\n", ''], $content); $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); $this->assertSame(['', 'foo ', "...\n\n\n", ''], $content); } public function testProcessEscapesPhpTags() { $esi = new Esi(); $request = Request::create('/'); $response = new Response(''); $this->assertSame($response, $esi->process($request, $response)); $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); $this->assertSame(['', '', ''], $content); } public function testProcessWhenNoSrcInAnEsi() { $this->expectException(\RuntimeException::class); $esi = new Esi(); $request = Request::create('/'); $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); } public function testProcessRemoveSurrogateControlHeader() { $esi = new Esi(); $request = Request::create('/'); $response = new Response('foo '); $response->headers->set('Surrogate-Control', 'content="ESI/1.0"'); $this->assertSame($response, $esi->process($request, $response)); $this->assertEquals('ESI', $response->headers->get('x-body-eval')); $response->headers->set('Surrogate-Control', 'no-store, content="ESI/1.0"'); $this->assertSame($response, $esi->process($request, $response)); $this->assertEquals('ESI', $response->headers->get('x-body-eval')); $this->assertEquals('no-store', $response->headers->get('surrogate-control')); $response->headers->set('Surrogate-Control', 'content="ESI/1.0", no-store'); $this->assertSame($response, $esi->process($request, $response)); $this->assertEquals('ESI', $response->headers->get('x-body-eval')); $this->assertEquals('no-store', $response->headers->get('surrogate-control')); } public function testHandle() { $esi = new Esi(); $cache = $this->getCache(Request::create('/'), new Response('foo')); $this->assertEquals('foo', $esi->handle($cache, '/', '/alt', true)); } public function testHandleWhenResponseIsNot200() { $this->expectException(\RuntimeException::class); $esi = new Esi(); $response = new Response('foo'); $response->setStatusCode(404); $cache = $this->getCache(Request::create('/'), $response); $esi->handle($cache, '/', '/alt', false); } public function testHandleWhenResponseIsNot200AndErrorsAreIgnored() { $esi = new Esi(); $response = new Response('foo'); $response->setStatusCode(404); $cache = $this->getCache(Request::create('/'), $response); $this->assertEquals('', $esi->handle($cache, '/', '/alt', true)); } public function testHandleWhenResponseIsNot200AndAltIsPresent() { $esi = new Esi(); $response1 = new Response('foo'); $response1->setStatusCode(404); $response2 = new Response('bar'); $cache = $this->getCache(Request::create('/'), [$response1, $response2]); $this->assertEquals('bar', $esi->handle($cache, '/', '/alt', false)); } public function testHandleWhenResponseIsNotModified() { $esi = new Esi(); $response = new Response(''); $response->setStatusCode(304); $cache = $this->getCache(Request::create('/'), $response); $this->assertEquals('', $esi->handle($cache, '/', '/alt', true)); } protected function getCache($request, $response) { $cache = $this->getMockBuilder(HttpCache::class)->onlyMethods(['getRequest', 'handle'])->disableOriginalConstructor()->getMock(); $cache->expects($this->atLeastOnce()) ->method('getRequest') ->willReturn($request) ; if (\is_array($response)) { $cache ->method('handle') ->willReturn(...$response) ; } else { $cache ->method('handle') ->willReturn($response) ; } return $cache; } } ================================================ FILE: Tests/HttpCache/HttpCacheTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\HttpCache; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\HttpKernel\HttpCache\Esi; use Symfony\Component\HttpKernel\HttpCache\HttpCache; use Symfony\Component\HttpKernel\HttpCache\Store; use Symfony\Component\HttpKernel\HttpCache\StoreInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Kernel; #[Group('time-sensitive')] class HttpCacheTest extends HttpCacheTestCase { public function testTerminateDelegatesTerminationOnlyForTerminableInterface() { $storeMock = $this->createStub(StoreInterface::class); // does not implement TerminableInterface $kernel = new TestKernel(); $httpCache = new HttpCache($kernel, $storeMock); $httpCache->terminate(Request::create('/'), new Response()); $this->assertFalse($kernel->terminateCalled, 'terminate() is never called if the kernel class does not implement TerminableInterface'); // implements TerminableInterface $kernelMock = $this->getMockBuilder(Kernel::class) ->disableOriginalConstructor() ->onlyMethods(['terminate', 'registerBundles', 'registerContainerConfiguration']) ->getMock(); $kernelMock->expects($this->once()) ->method('terminate'); $kernel = new HttpCache($kernelMock, $storeMock); $kernel->terminate(Request::create('/'), new Response()); } public function testDoesNotCallTerminateOnFreshResponse() { $terminateEvents = []; $eventDispatcher = $this->createMock(EventDispatcher::class); $eventDispatcher ->expects($this->atLeastOnce()) ->method('dispatch') ->with($this->callback(static function ($event) use (&$terminateEvents) { if ($event instanceof TerminateEvent) { $terminateEvents[] = $event; } return true; })); $this->setNextResponse( 200, [ 'ETag' => '1234', 'Cache-Control' => 'public, s-maxage=60', ], 'Hello World', null, $eventDispatcher ); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->cache->terminate($this->request, $this->response); sleep(2); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('fresh'); $this->assertEquals(2, $this->response->headers->get('Age')); $this->cache->terminate($this->request, $this->response); $this->assertCount(1, $terminateEvents); } public function testPassesOnNonGetHeadRequests() { $this->setNextResponse(200); $this->request('POST', '/'); $this->assertHttpKernelIsCalled(); $this->assertResponseOk(); $this->assertTraceContains('pass'); $this->assertFalse($this->response->headers->has('Age')); } public function testPassesSuspiciousMethodRequests() { $this->setNextResponse(200); $this->request('POST', '/', ['HTTP_X-HTTP-Method-Override' => '__CONSTRUCT']); $this->assertHttpKernelIsCalled(); $this->assertResponseOk(); $this->assertTraceNotContains('stale'); $this->assertTraceNotContains('invalid'); $this->assertFalse($this->response->headers->has('Age')); } public function testInvalidatesOnPostPutDeleteRequests() { foreach (['post', 'put', 'delete'] as $method) { $this->setNextResponse(200); $this->request($method, '/'); $this->assertHttpKernelIsCalled(); $this->assertResponseOk(); $this->assertTraceContains('invalidate'); $this->assertTraceContains('pass'); } } public function testDoesNotCacheWithAuthorizationRequestHeaderAndNonPublicResponse() { $this->setNextResponse(200, ['ETag' => '"Foo"']); $this->request('GET', '/', ['HTTP_AUTHORIZATION' => 'basic foobarbaz']); $this->assertHttpKernelIsCalled(); $this->assertResponseOk(); $this->assertEquals('private', $this->response->headers->get('Cache-Control')); $this->assertTraceContains('miss'); $this->assertTraceNotContains('store'); $this->assertFalse($this->response->headers->has('Age')); } public function testDoesCacheWithAuthorizationRequestHeaderAndPublicResponse() { $this->setNextResponse(200, ['Cache-Control' => 'public', 'ETag' => '"Foo"']); $this->request('GET', '/', ['HTTP_AUTHORIZATION' => 'basic foobarbaz']); $this->assertHttpKernelIsCalled(); $this->assertResponseOk(); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertTrue($this->response->headers->has('Age')); $this->assertEquals('public', $this->response->headers->get('Cache-Control')); } public function testDoesNotCacheWithCookieHeaderAndNonPublicResponse() { $this->setNextResponse(200, ['ETag' => '"Foo"']); $this->request('GET', '/', [], ['foo' => 'bar']); $this->assertHttpKernelIsCalled(); $this->assertResponseOk(); $this->assertEquals('private', $this->response->headers->get('Cache-Control')); $this->assertTraceContains('miss'); $this->assertTraceNotContains('store'); $this->assertFalse($this->response->headers->has('Age')); } public function testDoesNotCacheRequestsWithACookieHeader() { $this->setNextResponse(200); $this->request('GET', '/', [], ['foo' => 'bar']); $this->assertHttpKernelIsCalled(); $this->assertResponseOk(); $this->assertEquals('private', $this->response->headers->get('Cache-Control')); $this->assertTraceContains('miss'); $this->assertTraceNotContains('store'); $this->assertFalse($this->response->headers->has('Age')); } public function testRespondsWith304WhenIfModifiedSinceMatchesLastModified() { $time = \DateTimeImmutable::createFromFormat('U', time()); $this->setNextResponse(200, ['Cache-Control' => 'public', 'Last-Modified' => $time->format(\DATE_RFC2822), 'Content-Type' => 'text/plain'], 'Hello World'); $this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => $time->format(\DATE_RFC2822)]); $this->assertHttpKernelIsCalled(); $this->assertEquals(304, $this->response->getStatusCode()); $this->assertEquals('', $this->response->headers->get('Content-Type')); $this->assertSame('', $this->response->getContent()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); } public function testRespondsWith304WhenIfNoneMatchMatchesETag() { $this->setNextResponse(200, ['Cache-Control' => 'public', 'ETag' => '12345', 'Content-Type' => 'text/plain'], 'Hello World'); $this->request('GET', '/', ['HTTP_IF_NONE_MATCH' => '12345']); $this->assertHttpKernelIsCalled(); $this->assertEquals(304, $this->response->getStatusCode()); $this->assertEquals('', $this->response->headers->get('Content-Type')); $this->assertTrue($this->response->headers->has('ETag')); $this->assertSame('', $this->response->getContent()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); } public function testRespondsWith304WhenIfNoneMatchAndIfModifiedSinceBothMatch() { $time = \DateTimeImmutable::createFromFormat('U', time()); $this->setNextResponse(200, [], '', static function ($request, $response) use ($time) { $response->setStatusCode(200); $response->headers->set('ETag', '12345'); $response->headers->set('Last-Modified', $time->format(\DATE_RFC2822)); $response->headers->set('Content-Type', 'text/plain'); $response->setContent('Hello World'); }); // only ETag matches $t = \DateTimeImmutable::createFromFormat('U', time() - 3600); $this->request('GET', '/', ['HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $t->format(\DATE_RFC2822)]); $this->assertHttpKernelIsCalled(); $this->assertEquals(304, $this->response->getStatusCode()); // only Last-Modified matches $this->request('GET', '/', ['HTTP_IF_NONE_MATCH' => '1234', 'HTTP_IF_MODIFIED_SINCE' => $time->format(\DATE_RFC2822)]); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); // Both matches $this->request('GET', '/', ['HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $time->format(\DATE_RFC2822)]); $this->assertHttpKernelIsCalled(); $this->assertEquals(304, $this->response->getStatusCode()); } public function testIncrementsMaxAgeWhenNoDateIsSpecifiedEventWhenUsingETag() { $this->setNextResponse( 200, [ 'ETag' => '1234', 'Cache-Control' => 'public, s-maxage=60', ] ); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); sleep(2); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('fresh'); $this->assertEquals(2, $this->response->headers->get('Age')); } public function testValidatesPrivateResponsesCachedOnTheClient() { $this->setNextResponse(200, [], '', static function (Request $request, $response) { $etags = preg_split('/\s*,\s*/', $request->headers->get('IF_NONE_MATCH', '')); if ($request->cookies->has('authenticated')) { $response->headers->set('Cache-Control', 'private, no-store'); $response->setETag('"private tag"'); if (\in_array('"private tag"', $etags, true)) { $response->setStatusCode(304); } else { $response->setStatusCode(200); $response->headers->set('Content-Type', 'text/plain'); $response->setContent('private data'); } } else { $response->headers->set('Cache-Control', 'public'); $response->setETag('"public tag"'); if (\in_array('"public tag"', $etags, true)) { $response->setStatusCode(304); } else { $response->setStatusCode(200); $response->headers->set('Content-Type', 'text/plain'); $response->setContent('public data'); } } }); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('"public tag"', $this->response->headers->get('ETag')); $this->assertEquals('public data', $this->response->getContent()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->request('GET', '/', [], ['authenticated' => '']); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('"private tag"', $this->response->headers->get('ETag')); $this->assertEquals('private data', $this->response->getContent()); $this->assertTraceContains('stale'); $this->assertTraceContains('invalid'); $this->assertTraceNotContains('store'); } public function testStoresResponsesWhenNoCacheRequestDirectivePresent() { $time = \DateTimeImmutable::createFromFormat('U', time() + 5); $this->setNextResponse(200, ['Cache-Control' => 'public', 'Expires' => $time->format(\DATE_RFC2822)]); $this->request('GET', '/', ['HTTP_CACHE_CONTROL' => 'no-cache']); $this->assertHttpKernelIsCalled(); $this->assertTraceContains('store'); $this->assertTrue($this->response->headers->has('Age')); } public function testReloadsResponsesWhenCacheHitsButNoCacheRequestDirectivePresentWhenAllowReloadIsSetTrue() { $count = 0; $this->setNextResponse(200, ['Cache-Control' => 'public, max-age=10000'], '', static function ($request, $response) use (&$count) { ++$count; $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World'); }); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('store'); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('fresh'); $this->cacheConfig['allow_reload'] = true; $this->request('GET', '/', ['HTTP_CACHE_CONTROL' => 'no-cache']); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Goodbye World', $this->response->getContent()); $this->assertTraceContains('reload'); $this->assertTraceContains('store'); } public function testDoesNotReloadResponsesWhenAllowReloadIsSetFalseDefault() { $count = 0; $this->setNextResponse(200, ['Cache-Control' => 'public, max-age=10000'], '', static function ($request, $response) use (&$count) { ++$count; $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World'); }); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('store'); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('fresh'); $this->cacheConfig['allow_reload'] = false; $this->request('GET', '/', ['HTTP_CACHE_CONTROL' => 'no-cache']); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceNotContains('reload'); $this->request('GET', '/', ['HTTP_CACHE_CONTROL' => 'no-cache']); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceNotContains('reload'); } public function testRevalidatesFreshCacheEntryWhenMaxAgeRequestDirectiveIsExceededWhenAllowRevalidateOptionIsSetTrue() { $count = 0; $this->setNextResponse(200, [], '', static function ($request, $response) use (&$count) { ++$count; $response->headers->set('Cache-Control', 'public, max-age=10000'); $response->setETag($count); $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World'); }); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('store'); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('fresh'); $this->cacheConfig['allow_revalidate'] = true; $this->request('GET', '/', ['HTTP_CACHE_CONTROL' => 'max-age=0']); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Goodbye World', $this->response->getContent()); $this->assertTraceContains('stale'); $this->assertTraceContains('invalid'); $this->assertTraceContains('store'); } public function testDoesNotRevalidateFreshCacheEntryWhenEnableRevalidateOptionIsSetFalseDefault() { $count = 0; $this->setNextResponse(200, [], '', static function ($request, $response) use (&$count) { ++$count; $response->headers->set('Cache-Control', 'public, max-age=10000'); $response->setETag($count); $response->setContent(1 == $count ? 'Hello World' : 'Goodbye World'); }); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('store'); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('fresh'); $this->cacheConfig['allow_revalidate'] = false; $this->request('GET', '/', ['HTTP_CACHE_CONTROL' => 'max-age=0']); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceNotContains('stale'); $this->assertTraceNotContains('invalid'); $this->assertTraceContains('fresh'); $this->request('GET', '/', ['HTTP_CACHE_CONTROL' => 'max-age=0']); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceNotContains('stale'); $this->assertTraceNotContains('invalid'); $this->assertTraceContains('fresh'); } public function testFetchesResponseFromBackendWhenCacheMisses() { $time = \DateTimeImmutable::createFromFormat('U', time() + 5); $this->setNextResponse(200, ['Cache-Control' => 'public', 'Expires' => $time->format(\DATE_RFC2822)]); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('miss'); $this->assertTrue($this->response->headers->has('Age')); } public function testDoesNotCacheSomeStatusCodeResponses() { foreach (array_merge(range(201, 202), range(204, 206), range(303, 305), range(400, 403), range(405, 409), range(411, 417), range(500, 505)) as $code) { $time = \DateTimeImmutable::createFromFormat('U', time() + 5); $this->setNextResponse($code, ['Expires' => $time->format(\DATE_RFC2822)]); $this->request('GET', '/'); $this->assertEquals($code, $this->response->getStatusCode()); $this->assertTraceNotContains('store'); $this->assertFalse($this->response->headers->has('Age')); } } public function testDoesNotCacheResponsesWithExplicitNoStoreDirective() { $time = \DateTimeImmutable::createFromFormat('U', time() + 5); $this->setNextResponse(200, ['Expires' => $time->format(\DATE_RFC2822), 'Cache-Control' => 'no-store']); $this->request('GET', '/'); $this->assertTraceNotContains('store'); $this->assertFalse($this->response->headers->has('Age')); } public function testDoesNotCacheResponsesWithoutFreshnessInformationOrAValidator() { $this->setNextResponse(); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceNotContains('store'); } public function testCachesResponsesWithExplicitNoCacheDirective() { $time = \DateTimeImmutable::createFromFormat('U', time() + 5); $this->setNextResponse(200, ['Expires' => $time->format(\DATE_RFC2822), 'Cache-Control' => 'public, no-cache']); $this->request('GET', '/'); $this->assertTraceContains('store'); $this->assertTrue($this->response->headers->has('Age')); } public function testRevalidatesResponsesWithNoCacheDirectiveEvenIfFresh() { $this->setNextResponse(200, ['Cache-Control' => 'public, no-cache, max-age=10', 'ETag' => 'some-etag'], 'OK'); $this->request('GET', '/'); // warm the cache sleep(5); $this->setNextResponse(304, ['Cache-Control' => 'public, no-cache, max-age=10', 'ETag' => 'some-etag']); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); // no-cache -> MUST have revalidated at origin $this->assertTraceContains('valid'); $this->assertEquals('OK', $this->response->getContent()); $this->assertEquals(0, $this->response->getAge()); } public function testCachesResponsesWithAnExpirationHeader() { $time = \DateTimeImmutable::createFromFormat('U', time() + 5); $this->setNextResponse(200, ['Cache-Control' => 'public', 'Expires' => $time->format(\DATE_RFC2822)]); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertNotNull($this->response->headers->get('Date')); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $values = $this->getMetaStorageValues(); $this->assertCount(1, $values); } public function testCachesResponsesWithAMaxAgeDirective() { $this->setNextResponse(200, ['Cache-Control' => 'public, max-age=5']); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertNotNull($this->response->headers->get('Date')); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $values = $this->getMetaStorageValues(); $this->assertCount(1, $values); } public function testCachesResponsesWithASMaxAgeDirective() { $this->setNextResponse(200, ['Cache-Control' => 's-maxage=5']); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertNotNull($this->response->headers->get('Date')); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $values = $this->getMetaStorageValues(); $this->assertCount(1, $values); } public function testCachesResponsesWithALastModifiedValidatorButNoFreshnessInformation() { $time = \DateTimeImmutable::createFromFormat('U', time()); $this->setNextResponse(200, ['Cache-Control' => 'public', 'Last-Modified' => $time->format(\DATE_RFC2822)]); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); } public function testCachesResponsesWithAnETagValidatorButNoFreshnessInformation() { $this->setNextResponse(200, ['Cache-Control' => 'public', 'ETag' => '"123456"']); $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); } public function testHitsCachedResponsesWithExpiresHeader() { $time1 = \DateTimeImmutable::createFromFormat('U', time() - 5); $time2 = \DateTimeImmutable::createFromFormat('U', time() + 5); $this->setNextResponse(200, ['Cache-Control' => 'public', 'Date' => $time1->format(\DATE_RFC2822), 'Expires' => $time2->format(\DATE_RFC2822)]); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertNotNull($this->response->headers->get('Date')); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertLessThan(2, strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date'))); $this->assertGreaterThan(0, $this->response->headers->get('Age')); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); } public function testHitsCachedResponseWithMaxAgeDirective() { $time = \DateTimeImmutable::createFromFormat('U', time() - 5); $this->setNextResponse(200, ['Date' => $time->format(\DATE_RFC2822), 'Cache-Control' => 'public, max-age=10']); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertNotNull($this->response->headers->get('Date')); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertLessThan(2, strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date'))); $this->assertGreaterThan(0, $this->response->headers->get('Age')); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); } public function testDegradationWhenCacheLocked() { if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('Skips on windows to avoid permissions issues.'); } $this->cacheConfig['stale_while_revalidate'] = 10; // The presence of Last-Modified makes this cacheable (because Response::isValidateable() then). $this->setNextResponse(200, ['Cache-Control' => 'public, s-maxage=5', 'Last-Modified' => 'some while ago'], 'Old response'); $this->request('GET', '/'); // warm the cache // Now, lock the cache $concurrentRequest = Request::create('/', 'GET'); $this->store->lock($concurrentRequest); /* * After 10s, the cached response has become stale. Yet, we're still within the "stale_while_revalidate" * timeout so we may serve the stale response. */ sleep(10); $this->store = $this->createStore(); // create another store instance that does not hold the current lock $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('stale-while-revalidate'); $this->assertEquals('Old response', $this->response->getContent()); /* * Another 10s later, stale_while_revalidate is over. Resort to serving the old response, but * do so with a "server unavailable" message. */ sleep(10); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(503, $this->response->getStatusCode()); $this->assertEquals('Old response', $this->response->getContent()); } public function testHitBackendOnlyOnceWhenCacheWasLocked() { // Disable stale-while-revalidate, it circumvents waiting for the lock $this->cacheConfig['stale_while_revalidate'] = 0; $this->setNextResponses([ [ 'status' => 200, 'body' => 'initial response', 'headers' => [ 'Cache-Control' => 'public, no-cache', 'Last-Modified' => 'some while ago', ], ], [ 'status' => 304, 'body' => '', 'headers' => [ 'Cache-Control' => 'public, no-cache', 'Last-Modified' => 'some while ago', ], ], [ 'status' => 500, 'body' => 'The backend should not be called twice during revalidation', 'headers' => [], ], ]); $this->request('GET', '/'); // warm the cache // Use a store that simulates a cache entry being locked upon first attempt $this->store = new class(sys_get_temp_dir().'/http_cache') extends Store { private bool $hasLock = false; public function lock(Request $request): bool { $hasLock = $this->hasLock; $this->hasLock = true; return $hasLock; } public function isLocked(Request $request): bool { return false; } }; $this->request('GET', '/'); // hit the cache with simulated lock/concurrency block $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('initial response', $this->response->getContent()); $traces = $this->cache->getTraces(); $this->assertSame(['waiting', 'stale', 'valid', 'store'], current($traces)); } public function testTraceAddedWhenCacheLocked() { if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('Skips on windows to avoid permissions issues.'); } // Disable stale-while-revalidate, which circumvents blocking access $this->cacheConfig['stale_while_revalidate'] = 0; // The presence of Last-Modified makes this cacheable $this->setNextResponse(200, ['Cache-Control' => 'public, no-cache', 'Last-Modified' => 'some while ago'], 'Old response'); $this->request('GET', '/'); // warm the cache $primedStore = $this->store; $this->store = $this->createStub(Store::class); $this->store->method('lookup')->willReturnCallback(static fn (Request $request) => $primedStore->lookup($request)); // Assume the cache is locked at the first attempt, but immediately treat the lock as released afterwards $this->store->method('lock')->willReturnOnConsecutiveCalls(false, true); $this->store->method('isLocked')->willReturn(false); $this->request('GET', '/'); $this->assertTraceContains('waiting'); } public function testHitsCachedResponseWithSMaxAgeDirective() { $time = \DateTimeImmutable::createFromFormat('U', time() - 5); $this->setNextResponse(200, ['Date' => $time->format(\DATE_RFC2822), 'Cache-Control' => 's-maxage=10, max-age=0']); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertNotNull($this->response->headers->get('Date')); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertLessThan(2, strtotime($this->responses[0]->headers->get('Date')) - strtotime($this->response->headers->get('Date'))); $this->assertGreaterThan(0, $this->response->headers->get('Age')); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); } public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformation() { $this->setNextResponse(); $this->cacheConfig['default_ttl'] = 10; $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertMatchesRegularExpression('/s-maxage=10/', $this->response->headers->get('Cache-Control')); $this->cacheConfig['default_ttl'] = 10; $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertMatchesRegularExpression('/s-maxage=10/', $this->response->headers->get('Cache-Control')); } public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAfterTtlWasExpired() { $this->setNextResponse(); $this->cacheConfig['default_ttl'] = 2; $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); // expires the cache $values = $this->getMetaStorageValues(); $this->assertCount(1, $values); $tmp = unserialize($values[0]); $time = \DateTimeImmutable::createFromFormat('U', time() - 5); $tmp[0][1]['date'] = $time->format(\DATE_RFC2822); $r = new \ReflectionObject($this->store); $m = $r->getMethod('save'); $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp)); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('stale'); $this->assertTraceContains('invalid'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); $this->setNextResponse(); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); } public function testAssignsDefaultTtlWhenResponseHasNoFreshnessInformationAndAfterTtlWasExpiredWithStatus304() { $this->setNextResponse(); $this->cacheConfig['default_ttl'] = 2; $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); // expires the cache $values = $this->getMetaStorageValues(); $this->assertCount(1, $values); $tmp = unserialize($values[0]); $time = \DateTimeImmutable::createFromFormat('U', time() - 5); $tmp[0][1]['date'] = $time->format(\DATE_RFC2822); $r = new \ReflectionObject($this->store); $m = $r->getMethod('save'); $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp)); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('stale'); $this->assertTraceContains('valid'); $this->assertTraceContains('store'); $this->assertTraceNotContains('miss'); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertMatchesRegularExpression('/s-maxage=(2|3)/', $this->response->headers->get('Cache-Control')); } public function testDoesNotAssignDefaultTtlWhenResponseHasMustRevalidateDirective() { $this->setNextResponse(200, ['Cache-Control' => 'must-revalidate']); $this->cacheConfig['default_ttl'] = 10; $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('miss'); $this->assertTraceNotContains('store'); $this->assertDoesNotMatchRegularExpression('/s-maxage/', $this->response->headers->get('Cache-Control')); $this->assertEquals('Hello World', $this->response->getContent()); } public function testFetchesFullResponseWhenCacheStaleAndNoValidatorsPresent() { $time = \DateTimeImmutable::createFromFormat('U', time() + 5); $this->setNextResponse(200, ['Cache-Control' => 'public', 'Expires' => $time->format(\DATE_RFC2822)]); // build initial request $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertNotNull($this->response->headers->get('Date')); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $this->assertNotNull($this->response->headers->get('Age')); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); // go in and play around with the cached metadata directly ... $values = $this->getMetaStorageValues(); $this->assertCount(1, $values); $tmp = unserialize($values[0]); $time = \DateTimeImmutable::createFromFormat('U', time()); $tmp[0][1]['expires'] = $time->format(\DATE_RFC2822); $r = new \ReflectionObject($this->store); $m = $r->getMethod('save'); $m->invoke($this->store, 'md'.hash('sha256', 'http://localhost/'), serialize($tmp)); // build subsequent request; should be found but miss due to freshness $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertLessThanOrEqual(1, $this->response->headers->get('Age')); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $this->assertTraceContains('stale'); $this->assertTraceNotContains('fresh'); $this->assertTraceNotContains('miss'); $this->assertTraceContains('store'); $this->assertEquals('Hello World', $this->response->getContent()); } public function testValidatesCachedResponsesWithLastModifiedAndNoFreshnessInformation() { $time = \DateTimeImmutable::createFromFormat('U', time()); $this->setNextResponse(200, [], 'Hello World', static function ($request, $response) use ($time) { $response->headers->set('Cache-Control', 'public'); $response->headers->set('Last-Modified', $time->format(\DATE_RFC2822)); if ($time->format(\DATE_RFC2822) == $request->headers->get('IF_MODIFIED_SINCE')) { $response->setStatusCode(304); $response->setContent(''); } }); // build initial request $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertNotNull($this->response->headers->get('Last-Modified')); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertTraceNotContains('stale'); // build subsequent request; should be found but miss due to freshness $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertNotNull($this->response->headers->get('Last-Modified')); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $this->assertLessThanOrEqual(1, $this->response->headers->get('Age')); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('stale'); $this->assertTraceContains('valid'); $this->assertTraceContains('store'); $this->assertTraceNotContains('miss'); } public function testValidatesCachedResponsesUseSameHttpMethod() { $this->setNextResponse(200, [], 'Hello World', function ($request, $response) { $this->assertSame('OPTIONS', $request->getMethod()); }); // build initial request $this->request('OPTIONS', '/'); // build subsequent request $this->request('OPTIONS', '/'); } public function testValidatesCachedResponsesWithETagAndNoFreshnessInformation() { $this->setNextResponse(200, [], 'Hello World', function ($request, $response) { $this->assertFalse($request->headers->has('If-Modified-Since')); $response->headers->set('Cache-Control', 'public'); $response->headers->set('ETag', '"12345"'); if ($response->getETag() == $request->headers->get('IF_NONE_MATCH')) { $response->setStatusCode(304); $response->setContent(''); } }); // build initial request $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertNotNull($this->response->headers->get('ETag')); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); // build subsequent request; should be found but miss due to freshness $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertNotNull($this->response->headers->get('ETag')); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $this->assertLessThanOrEqual(1, $this->response->headers->get('Age')); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('stale'); $this->assertTraceContains('valid'); $this->assertTraceContains('store'); $this->assertTraceNotContains('miss'); } public function testServesResponseWhileFreshAndRevalidatesWithLastModifiedInformation() { $time = \DateTimeImmutable::createFromFormat('U', time()); $this->setNextResponse(200, [], 'Hello World', static function (Request $request, Response $response) use ($time) { $response->setSharedMaxAge(10); $response->headers->set('Last-Modified', $time->format(\DATE_RFC2822)); }); // prime the cache $this->request('GET', '/'); // next request before s-maxage has expired: Serve from cache // without hitting the backend $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('fresh'); sleep(15); // expire the cache $this->setNextResponse(304, [], '', function (Request $request, Response $response) use ($time) { $this->assertEquals($time->format(\DATE_RFC2822), $request->headers->get('IF_MODIFIED_SINCE')); }); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('stale'); $this->assertTraceContains('valid'); } public function testReplacesCachedResponsesWhenValidationResultsInNon304Response() { $time = \DateTimeImmutable::createFromFormat('U', time()); $count = 0; $this->setNextResponse(200, [], 'Hello World', static function ($request, $response) use ($time, &$count) { $response->headers->set('Last-Modified', $time->format(\DATE_RFC2822)); $response->headers->set('Cache-Control', 'public'); switch (++$count) { case 1: $response->setContent('first response'); break; case 2: $response->setContent('second response'); break; case 3: $response->setContent(''); $response->setStatusCode(304); break; } }); // first request should fetch from backend and store in cache $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('first response', $this->response->getContent()); // second request is validated, is invalid, and replaces cached entry $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('second response', $this->response->getContent()); // third response is validated, valid, and returns cached entry $this->request('GET', '/'); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('second response', $this->response->getContent()); $this->assertEquals(3, $count); } public function testPassesHeadRequestsThroughDirectlyOnPass() { $this->setNextResponse(200, [], 'Hello World', function ($request, $response) { $response->setContent(''); $response->setStatusCode(200); $this->assertEquals('HEAD', $request->getMethod()); }); $this->request('HEAD', '/', ['HTTP_EXPECT' => 'something ...']); $this->assertHttpKernelIsCalled(); $this->assertEquals('', $this->response->getContent()); } public function testUsesCacheToRespondToHeadRequestsWhenFresh() { $this->setNextResponse(200, [], 'Hello World', function ($request, $response) { $response->headers->set('Cache-Control', 'public, max-age=10'); $response->setContent('Hello World'); $response->setStatusCode(200); $this->assertNotEquals('HEAD', $request->getMethod()); }); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals('Hello World', $this->response->getContent()); $this->request('HEAD', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('', $this->response->getContent()); $this->assertEquals(\strlen('Hello World'), $this->response->headers->get('Content-Length')); } public function testSendsNoContentWhenFresh() { $time = \DateTimeImmutable::createFromFormat('U', time()); $this->setNextResponse(200, [], 'Hello World', static function ($request, $response) use ($time) { $response->headers->set('Cache-Control', 'public, max-age=10'); $response->headers->set('Last-Modified', $time->format(\DATE_RFC2822)); }); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals('Hello World', $this->response->getContent()); $this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => $time->format(\DATE_RFC2822)]); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(304, $this->response->getStatusCode()); $this->assertEquals('', $this->response->getContent()); } public function testInvalidatesCachedResponsesOnPost() { $this->setNextResponse(200, [], 'Hello World', static function ($request, $response) { if ('GET' == $request->getMethod()) { $response->setStatusCode(200); $response->headers->set('Cache-Control', 'public, max-age=500'); $response->setContent('Hello World'); } elseif ('POST' == $request->getMethod()) { $response->setStatusCode(303); $response->headers->set('Location', '/'); $response->headers->remove('Cache-Control'); $response->setContent(''); } }); // build initial request to enter into the cache $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); // make sure it is valid $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('fresh'); // now POST to same URL $this->request('POST', '/helloworld'); $this->assertHttpKernelIsCalled(); $this->assertEquals('/', $this->response->headers->get('Location')); $this->assertTraceContains('invalidate'); $this->assertTraceContains('pass'); $this->assertEquals('', $this->response->getContent()); // now make sure it was actually invalidated $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Hello World', $this->response->getContent()); $this->assertTraceContains('stale'); $this->assertTraceContains('invalid'); $this->assertTraceContains('store'); } public function testServesFromCacheWhenHeadersMatch() { $count = 0; $this->setNextResponse(200, ['Cache-Control' => 'max-age=10000'], '', static function ($request, $response) use (&$count) { $response->headers->set('Vary', 'Accept User-Agent Foo'); $response->headers->set('Cache-Control', 'public, max-age=10'); $response->headers->set('X-Response-Count', ++$count); $response->setContent($request->headers->get('USER_AGENT')); }); $this->request('GET', '/', ['HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0']); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Bob/1.0', $this->response->getContent()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->request('GET', '/', ['HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0']); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Bob/1.0', $this->response->getContent()); $this->assertTraceContains('fresh'); $this->assertTraceNotContains('store'); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); } public function testStoresMultipleResponsesWhenHeadersDiffer() { $count = 0; $this->setNextResponse(200, ['Cache-Control' => 'max-age=10000'], '', static function ($request, $response) use (&$count) { $response->headers->set('Vary', 'Accept User-Agent Foo'); $response->headers->set('Cache-Control', 'public, max-age=10'); $response->headers->set('X-Response-Count', ++$count); $response->setContent($request->headers->get('USER_AGENT')); }); $this->request('GET', '/', ['HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0']); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('Bob/1.0', $this->response->getContent()); $this->assertEquals(1, $this->response->headers->get('X-Response-Count')); $this->request('GET', '/', ['HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/2.0']); $this->assertEquals(200, $this->response->getStatusCode()); $this->assertTraceContains('miss'); $this->assertTraceContains('store'); $this->assertEquals('Bob/2.0', $this->response->getContent()); $this->assertEquals(2, $this->response->headers->get('X-Response-Count')); $this->request('GET', '/', ['HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/1.0']); $this->assertTraceContains('fresh'); $this->assertEquals('Bob/1.0', $this->response->getContent()); $this->assertEquals(1, $this->response->headers->get('X-Response-Count')); $this->request('GET', '/', ['HTTP_ACCEPT' => 'text/html', 'HTTP_USER_AGENT' => 'Bob/2.0']); $this->assertTraceContains('fresh'); $this->assertEquals('Bob/2.0', $this->response->getContent()); $this->assertEquals(2, $this->response->headers->get('X-Response-Count')); $this->request('GET', '/', ['HTTP_USER_AGENT' => 'Bob/2.0']); $this->assertTraceContains('miss'); $this->assertEquals('Bob/2.0', $this->response->getContent()); $this->assertEquals(3, $this->response->headers->get('X-Response-Count')); } public function testShouldCatchExceptions() { $this->catchExceptions(); $this->setNextResponse(); $this->request('GET', '/'); $this->assertExceptionsAreCaught(); } public function testShouldCatchExceptionsWhenReloadingAndNoCacheRequest() { $this->catchExceptions(); $this->setNextResponse(); $this->cacheConfig['allow_reload'] = true; $this->request('GET', '/', [], [], false, ['Pragma' => 'no-cache']); $this->assertExceptionsAreCaught(); } public function testShouldNotCatchExceptions() { $this->catchExceptions(false); $this->setNextResponse(); $this->request('GET', '/'); $this->assertExceptionsAreNotCaught(); } public function testEsiCacheSendsTheLowestTtl() { $responses = [ [ 'status' => 200, 'body' => ' ', 'headers' => [ 'Cache-Control' => 's-maxage=300', 'Surrogate-Control' => 'content="ESI/1.0"', ], ], [ 'status' => 200, 'body' => 'Hello World!', 'headers' => ['Cache-Control' => 's-maxage=200'], ], [ 'status' => 200, 'body' => 'My name is Bobby.', 'headers' => ['Cache-Control' => 's-maxage=100'], ], ]; $this->setNextResponses($responses); $this->request('GET', '/', [], [], true); $this->assertEquals('Hello World! My name is Bobby.', $this->response->getContent()); $this->assertEquals(100, $this->response->getTtl()); } public function testEsiCacheSendsTheLowestTtlForHeadRequests() { $responses = [ [ 'status' => 200, 'body' => 'I am a long-lived main response, but I embed a short-lived resource: ', 'headers' => [ 'Cache-Control' => 's-maxage=300', 'Surrogate-Control' => 'content="ESI/1.0"', ], ], [ 'status' => 200, 'body' => 'I am a short-lived resource', 'headers' => ['Cache-Control' => 's-maxage=100'], ], ]; $this->setNextResponses($responses); $this->request('HEAD', '/', [], [], true); $this->assertSame('', $this->response->getContent()); $this->assertEquals(100, $this->response->getTtl()); } public function testEsiCacheIncludesEmbeddedResponseContentWhenMainResponseFailsRevalidationAndEmbeddedResponseIsFresh() { $this->setNextResponses([ [ 'status' => 200, 'body' => 'main ', 'headers' => [ 'Cache-Control' => 's-maxage=0', // goes stale immediately 'Surrogate-Control' => 'content="ESI/1.0"', 'Last-Modified' => 'Mon, 12 Aug 2024 10:00:00 +0000', ], ], [ 'status' => 200, 'body' => 'embedded', 'headers' => [ 'Cache-Control' => 's-maxage=10', // stays fresh 'Last-Modified' => 'Mon, 12 Aug 2024 10:05:00 +0000', ], ], ]); // prime the cache $this->request('GET', '/', [], [], true); $this->assertSame(200, $this->response->getStatusCode()); $this->assertSame('main embedded', $this->response->getContent()); $this->assertSame('Mon, 12 Aug 2024 10:05:00 +0000', $this->response->getLastModified()->format(\DATE_RFC2822)); // max of both values $this->setNextResponses([ [ // On the next request, the main response has an updated Last-Modified (main page was modified)... 'status' => 200, 'body' => 'main ', 'headers' => [ 'Cache-Control' => 's-maxage=0', 'Surrogate-Control' => 'content="ESI/1.0"', 'Last-Modified' => 'Mon, 12 Aug 2024 10:10:00 +0000', ], ], // no revalidation request happens for the embedded response, since it is still fresh ]); // Re-request with Last-Modified time that we received when the cache was primed $this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => 'Mon, 12 Aug 2024 10:05:00 +0000'], [], true); $this->assertSame(200, $this->response->getStatusCode()); // The cache should use the content ("embedded") from the cached entry $this->assertSame('main embedded', $this->response->getContent()); $traces = $this->cache->getTraces(); $this->assertSame(['stale', 'invalid', 'store'], $traces['GET /']); // The embedded resource was still fresh $this->assertSame(['fresh'], $traces['GET /foo']); } public function testEsiCacheIncludesEmbeddedResponseContentWhenMainResponseFailsRevalidationAndEmbeddedResponseIsValid() { $this->setNextResponses([ [ 'status' => 200, 'body' => 'main ', 'headers' => [ 'Cache-Control' => 's-maxage=0', // goes stale immediately 'Surrogate-Control' => 'content="ESI/1.0"', 'Last-Modified' => 'Mon, 12 Aug 2024 10:00:00 +0000', ], ], [ 'status' => 200, 'body' => 'embedded', 'headers' => [ 'Cache-Control' => 's-maxage=0', // goes stale immediately 'Last-Modified' => 'Mon, 12 Aug 2024 10:05:00 +0000', ], ], ]); // prime the cache $this->request('GET', '/', [], [], true); $this->assertSame(200, $this->response->getStatusCode()); $this->assertSame('main embedded', $this->response->getContent()); $this->assertSame('Mon, 12 Aug 2024 10:05:00 +0000', $this->response->getLastModified()->format(\DATE_RFC2822)); // max of both values $this->setNextResponses([ [ // On the next request, the main response has an updated Last-Modified (main page was modified)... 'status' => 200, 'body' => 'main ', 'headers' => [ 'Cache-Control' => 's-maxage=0', 'Surrogate-Control' => 'content="ESI/1.0"', 'Last-Modified' => 'Mon, 12 Aug 2024 10:10:00 +0000', ], ], [ // We have a stale cache entry for the embedded response which will be revalidated. // Let's assume the resource did not change, so the controller sends a 304 without content body. 'status' => 304, 'body' => '', 'headers' => [ 'Cache-Control' => 's-maxage=0', ], ], ]); // Re-request with Last-Modified time that we received when the cache was primed $this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => 'Mon, 12 Aug 2024 10:05:00 +0000'], [], true); $this->assertSame(200, $this->response->getStatusCode()); // The cache should use the content ("embedded") from the cached entry $this->assertSame('main embedded', $this->response->getContent()); $traces = $this->cache->getTraces(); $this->assertSame(['stale', 'invalid', 'store'], $traces['GET /']); // Check that the embedded resource was successfully revalidated $this->assertSame(['stale', 'valid', 'store'], $traces['GET /foo']); } public function testEsiCacheIncludesEmbeddedResponseContentWhenMainAndEmbeddedResponseAreFresh() { $this->setNextResponses([ [ 'status' => 200, 'body' => 'main ', 'headers' => [ 'Cache-Control' => 's-maxage=10', 'Surrogate-Control' => 'content="ESI/1.0"', 'Last-Modified' => 'Mon, 12 Aug 2024 10:05:00 +0000', ], ], [ 'status' => 200, 'body' => 'embedded', 'headers' => [ 'Cache-Control' => 's-maxage=10', 'Last-Modified' => 'Mon, 12 Aug 2024 10:00:00 +0000', ], ], ]); // prime the cache $this->request('GET', '/', [], [], true); $this->assertSame(200, $this->response->getStatusCode()); $this->assertSame('main embedded', $this->response->getContent()); $this->assertSame('Mon, 12 Aug 2024 10:05:00 +0000', $this->response->getLastModified()->format(\DATE_RFC2822)); // Assume that a client received 'Mon, 12 Aug 2024 10:00:00 +0000' as last-modified information in the past. This may, for example, // be the case when the "main" response at that point had an older Last-Modified time, so the embedded response's Last-Modified time // governed the result for the combined response. In other words, the client received a Last-Modified time that still validates the // embedded response as of now, but no longer matches the Last-Modified time of the "main" resource. // Now this client does a revalidation request. $this->request('GET', '/', ['HTTP_IF_MODIFIED_SINCE' => 'Mon, 12 Aug 2024 10:00:00 +0000'], [], true); $this->assertSame(200, $this->response->getStatusCode()); // The cache should use the content ("embedded") from the cached entry $this->assertSame('main embedded', $this->response->getContent()); $traces = $this->cache->getTraces(); $this->assertSame(['fresh'], $traces['GET /']); // Check that the embedded resource was successfully revalidated $this->assertSame(['fresh'], $traces['GET /foo']); } public function testEsiCacheForceValidation() { $responses = [ [ 'status' => 200, 'body' => ' ', 'headers' => [ 'Cache-Control' => 's-maxage=300', 'Surrogate-Control' => 'content="ESI/1.0"', ], ], [ 'status' => 200, 'body' => 'Hello World!', 'headers' => ['ETag' => 'foobar'], ], [ 'status' => 200, 'body' => 'My name is Bobby.', 'headers' => ['Cache-Control' => 's-maxage=100'], ], ]; $this->setNextResponses($responses); $this->request('GET', '/', [], [], true); $this->assertEquals('Hello World! My name is Bobby.', $this->response->getContent()); $this->assertNull($this->response->getTtl()); $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); $this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache')); } public function testEsiCacheForceValidationForHeadRequests() { $responses = [ [ 'status' => 200, 'body' => 'I am the main response and use expiration caching, but I embed another resource: ', 'headers' => [ 'Cache-Control' => 's-maxage=300', 'Surrogate-Control' => 'content="ESI/1.0"', ], ], [ 'status' => 200, 'body' => 'I am the embedded resource and use validation caching', 'headers' => ['ETag' => 'foobar'], ], ]; $this->setNextResponses($responses); $this->request('HEAD', '/', [], [], true); // The response has been assembled from expiration and validation based resources // This can neither be cached nor revalidated, so it should be private/no cache $this->assertSame('', $this->response->getContent()); $this->assertNull($this->response->getTtl()); $this->assertTrue($this->response->headers->hasCacheControlDirective('private')); $this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache')); } public function testEsiRecalculateContentLengthHeader() { $responses = [ [ 'status' => 200, 'body' => '', 'headers' => [ 'Content-Length' => 26, 'Surrogate-Control' => 'content="ESI/1.0"', ], ], [ 'status' => 200, 'body' => 'Hello World!', 'headers' => [], ], ]; $this->setNextResponses($responses); $this->request('GET', '/', [], [], true); $this->assertEquals('Hello World!', $this->response->getContent()); $this->assertEquals(12, $this->response->headers->get('Content-Length')); } public function testEsiRecalculateContentLengthHeaderForHeadRequest() { $responses = [ [ 'status' => 200, 'body' => '', 'headers' => [ 'Content-Length' => 26, 'Surrogate-Control' => 'content="ESI/1.0"', ], ], [ 'status' => 200, 'body' => 'Hello World!', 'headers' => [], ], ]; $this->setNextResponses($responses); $this->request('HEAD', '/', [], [], true); // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13 // "The Content-Length entity-header field indicates the size of the entity-body, // in decimal number of OCTETs, sent to the recipient or, in the case of the HEAD // method, the size of the entity-body that would have been sent had the request // been a GET." $this->assertSame('', $this->response->getContent()); $this->assertEquals(12, $this->response->headers->get('Content-Length')); } public function testClientIpIsAlwaysLocalhostForForwardedRequests() { $this->setNextResponse(); $this->request('GET', '/', ['REMOTE_ADDR' => '10.0.0.1']); $this->kernel->assert(function ($backendRequest) { $this->assertSame('127.0.0.1', $backendRequest->server->get('REMOTE_ADDR')); }); } #[DataProvider('getTrustedProxyData')] public function testHttpCacheIsSetAsATrustedProxy(array $existing) { Request::setTrustedProxies($existing, Request::HEADER_X_FORWARDED_FOR); $this->setNextResponse(); $this->request('GET', '/', ['REMOTE_ADDR' => '10.0.0.1']); $this->assertSame($existing, Request::getTrustedProxies()); $existing = array_unique(array_merge($existing, ['127.0.0.1'])); $this->kernel->assert(function ($backendRequest) use ($existing) { $this->assertSame($existing, Request::getTrustedProxies()); $this->assertsame('10.0.0.1', $backendRequest->getClientIp()); }); Request::setTrustedProxies([], -1); } public static function getTrustedProxyData() { return [ [[]], [['10.0.0.2']], [['10.0.0.2', '127.0.0.1']], ]; } #[DataProvider('getForwardedData')] public function testForwarderHeaderForForwardedRequests($forwarded, $expected) { $this->setNextResponse(); $server = ['REMOTE_ADDR' => '10.0.0.1']; if (null !== $forwarded) { Request::setTrustedProxies($server, -1); $server['HTTP_FORWARDED'] = $forwarded; } $this->request('GET', '/', $server); $this->kernel->assert(function ($backendRequest) use ($expected) { $this->assertSame($expected, $backendRequest->headers->get('Forwarded')); }); Request::setTrustedProxies([], -1); } public static function getForwardedData() { return [ [null, 'for="10.0.0.1";host="localhost";proto=http'], ['for=10.0.0.2', 'for="10.0.0.2";host="localhost";proto=http, for="10.0.0.1"'], ['for=10.0.0.2, for=10.0.0.3', 'for="10.0.0.2";host="localhost";proto=http, for="10.0.0.3", for="10.0.0.1"'], ]; } public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponses() { $time = \DateTimeImmutable::createFromFormat('U', time()); $responses = [ [ 'status' => 200, 'body' => '', 'headers' => [ 'Surrogate-Control' => 'content="ESI/1.0"', 'ETag' => 'hey', 'Last-Modified' => $time->format(\DATE_RFC2822), ], ], [ 'status' => 200, 'body' => 'Hey!', 'headers' => [], ], ]; $this->setNextResponses($responses); $this->request('GET', '/', [], [], true); $this->assertNull($this->response->getETag()); $this->assertNull($this->response->getLastModified()); } public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponsesAndHeadRequest() { $time = \DateTimeImmutable::createFromFormat('U', time()); $responses = [ [ 'status' => 200, 'body' => '', 'headers' => [ 'Surrogate-Control' => 'content="ESI/1.0"', 'ETag' => 'hey', 'Last-Modified' => $time->format(\DATE_RFC2822), ], ], [ 'status' => 200, 'body' => 'Hey!', 'headers' => [], ], ]; $this->setNextResponses($responses); $this->request('HEAD', '/', [], [], true); $this->assertSame('', $this->response->getContent()); $this->assertNull($this->response->getETag()); $this->assertNull($this->response->getLastModified()); } public function testDoesNotCacheOptionsRequest() { $this->setNextResponse(200, ['Cache-Control' => 'public, s-maxage=60'], 'get'); $this->request('GET', '/'); $this->assertHttpKernelIsCalled(); $this->setNextResponse(200, ['Cache-Control' => 'public, s-maxage=60'], 'options'); $this->request('OPTIONS', '/'); $this->assertHttpKernelIsCalled(); $this->request('GET', '/'); $this->assertHttpKernelIsNotCalled(); $this->assertSame('get', $this->response->getContent()); } public function testUsesOriginalRequestForSurrogate() { $kernel = $this->createMock(HttpKernelInterface::class); $store = $this->createStub(StoreInterface::class); $kernel ->expects($this->exactly(2)) ->method('handle') ->willReturnCallback(function (Request $request) { $this->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR')); return new Response(); }); $cache = new HttpCache($kernel, $store, new Esi() ); $request = Request::create('/'); $request->server->set('REMOTE_ADDR', '10.0.0.1'); // Main request $cache->handle($request, HttpKernelInterface::MAIN_REQUEST); // Main request was now modified by HttpCache // The surrogate will ask for the request using $this->cache->getRequest() // which MUST return the original request so the surrogate // can actually behave like a reverse proxy like e.g. Varnish would. $this->assertSame('10.0.0.1', $cache->getRequest()->getClientIp()); $this->assertSame('10.0.0.1', $cache->getRequest()->server->get('REMOTE_ADDR')); // Surrogate request $cache->handle($request, HttpKernelInterface::SUB_REQUEST); } public function testStaleIfErrorMustNotResetLifetime() { // Make sure we don't accidentally treat the response as fresh (revalidated) again // when stale-if-error handling kicks in. $responses = [ [ 'status' => 200, 'body' => 'OK', // This is cacheable and can be used in stale-if-error cases: 'headers' => ['Cache-Control' => 'public, max-age=10', 'ETag' => 'some-etag'], ], [ 'status' => 500, 'body' => 'FAIL', 'headers' => [], ], [ 'status' => 500, 'body' => 'FAIL', 'headers' => [], ], ]; $this->setNextResponses($responses); $this->cacheConfig['stale_if_error'] = 10; $this->request('GET', '/'); // warm cache sleep(15); // now the entry is stale, but still within the grace period (10s max-age + 10s stale-if-error) $this->request('GET', '/'); // hit backend error $this->assertEquals(200, $this->response->getStatusCode()); // stale-if-error saved the day $this->assertEquals(15, $this->response->getAge()); sleep(10); // now we're outside the grace period $this->request('GET', '/'); // hit backend error $this->assertEquals(500, $this->response->getStatusCode()); // fail } #[DataProvider('getResponseDataThatMayBeServedStaleIfError')] public function testResponsesThatMayBeUsedStaleIfError($responseHeaders, $sleepBetweenRequests = null) { $responses = [ [ 'status' => 200, 'body' => 'OK', 'headers' => $responseHeaders, ], [ 'status' => 500, 'body' => 'FAIL', 'headers' => [], ], ]; $this->setNextResponses($responses); $this->cacheConfig['stale_if_error'] = 10; // after stale, may be served for 10s $this->request('GET', '/'); // warm cache if ($sleepBetweenRequests) { sleep($sleepBetweenRequests); } $this->request('GET', '/'); // hit backend error $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('OK', $this->response->getContent()); $this->assertTraceContains('stale-if-error'); } public static function getResponseDataThatMayBeServedStaleIfError() { // All data sets assume that a 10s stale-if-error grace period has been configured yield 'public, max-age expired' => [['Cache-Control' => 'public, max-age=60'], 65]; yield 'public, validateable with ETag, no TTL' => [['Cache-Control' => 'public', 'ETag' => 'some-etag'], 5]; yield 'public, validateable with Last-Modified, no TTL' => [['Cache-Control' => 'public', 'Last-Modified' => 'yesterday'], 5]; yield 'public, s-maxage will be served stale-if-error, even if the RFC mandates otherwise' => [['Cache-Control' => 'public, s-maxage=20'], 25]; } #[DataProvider('getResponseDataThatMustNotBeServedStaleIfError')] public function testResponsesThatMustNotBeUsedStaleIfError($responseHeaders, $sleepBetweenRequests = null) { $responses = [ [ 'status' => 200, 'body' => 'OK', 'headers' => $responseHeaders, ], [ 'status' => 500, 'body' => 'FAIL', 'headers' => [], ], ]; $this->setNextResponses($responses); $this->cacheConfig['stale_if_error'] = 10; // after stale, may be served for 10s $this->cacheConfig['strict_smaxage'] = true; // full RFC compliance for this feature $this->request('GET', '/'); // warm cache if ($sleepBetweenRequests) { sleep($sleepBetweenRequests); } $this->request('GET', '/'); // hit backend error $this->assertEquals(500, $this->response->getStatusCode()); } public function testSkipsConfiguredResponseHeadersForStore() { $storeMock = $this->createMock(StoreInterface::class); $storeMock ->expects($this->once()) ->method('write') ->with( $this->isInstanceOf(Request::class), $this->callback(function (Response $response) { $this->assertFalse($response->headers->has('Set-Cookie')); $this->assertFalse($response->headers->has('Another-One-To-Skip')); $this->assertTrue($response->headers->has('Cache-Control')); $this->assertTrue($response->headers->has('Another-One-To-Keep')); return true; }) ); $this->setNextResponse(200, [ 'Cache-Control' => 'public, s-maxage=20', 'Set-Cookie' => 'foobar=value; path=/', 'Another-One-To-Skip' => 'foobar', 'Another-One-To-Keep' => 'foobar', ]); $httpCache = new HttpCache($this->kernel, $storeMock, null, [ 'skip_response_headers' => ['Set-Cookie', 'Another-One-To-Skip', 'I-do-Not-Exist'], ]); $response = $httpCache->handle(Request::create('/')); $this->assertSame('foobar=value; path=/', $response->headers->get('Set-Cookie')); $this->assertSame('foobar', $response->headers->get('Another-One-To-Skip')); $this->assertSame('foobar', $response->headers->get('Another-One-To-Keep')); $this->assertFalse($response->headers->has('I-do-Not-Exist')); } public static function getResponseDataThatMustNotBeServedStaleIfError() { // All data sets assume that a 10s stale-if-error grace period has been configured yield 'public, no TTL but beyond grace period' => [['Cache-Control' => 'public'], 15]; yield 'public, validateable with ETag, no TTL but beyond grace period' => [['Cache-Control' => 'public', 'ETag' => 'some-etag'], 15]; yield 'public, validateable with Last-Modified, no TTL but beyond grace period' => [['Cache-Control' => 'public', 'Last-Modified' => 'yesterday'], 15]; yield 'public, stale beyond grace period' => [['Cache-Control' => 'public, max-age=10'], 30]; // Cache-control values that prohibit serving stale responses or responses without positive validation - // see https://tools.ietf.org/html/rfc7234#section-4.2.4 and // https://tools.ietf.org/html/rfc7234#section-5.2.2 yield 'no-cache requires positive validation' => [['Cache-Control' => 'public, no-cache', 'ETag' => 'some-etag']]; yield 'no-cache requires positive validation, even if fresh' => [['Cache-Control' => 'public, no-cache, max-age=10']]; yield 'must-revalidate requires positive validation once stale' => [['Cache-Control' => 'public, max-age=10, must-revalidate'], 15]; yield 'proxy-revalidate requires positive validation once stale' => [['Cache-Control' => 'public, max-age=10, proxy-revalidate'], 15]; } public function testStaleIfErrorWhenStrictSmaxageDisabled() { $responses = [ [ 'status' => 200, 'body' => 'OK', 'headers' => ['Cache-Control' => 'public, s-maxage=20'], ], [ 'status' => 500, 'body' => 'FAIL', 'headers' => [], ], ]; $this->setNextResponses($responses); $this->cacheConfig['stale_if_error'] = 10; $this->cacheConfig['strict_smaxage'] = false; $this->request('GET', '/'); // warm cache sleep(25); $this->request('GET', '/'); // hit backend error $this->assertEquals(200, $this->response->getStatusCode()); $this->assertEquals('OK', $this->response->getContent()); $this->assertTraceContains('stale-if-error'); } public function testTraceHeaderNameCanBeChanged() { $this->cacheConfig['trace_header'] = 'X-My-Header'; $this->setNextResponse(); $this->request('GET', '/'); $this->assertTrue($this->response->headers->has('X-My-Header')); } public function testTraceLevelDefaultsToFullIfDebug() { $this->setNextResponse(); $this->request('GET', '/'); $this->assertTrue($this->response->headers->has('X-Symfony-Cache')); $this->assertEquals('GET /: miss', $this->response->headers->get('X-Symfony-Cache')); } public function testTraceLevelDefaultsToNoneIfNotDebug() { $this->cacheConfig['debug'] = false; $this->setNextResponse(); $this->request('GET', '/'); $this->assertFalse($this->response->headers->has('X-Symfony-Cache')); } public function testTraceLevelShort() { $this->cacheConfig['trace_level'] = 'short'; $this->setNextResponse(); $this->request('GET', '/'); $this->assertTrue($this->response->headers->has('X-Symfony-Cache')); $this->assertEquals('miss', $this->response->headers->get('X-Symfony-Cache')); } public function testQueryMethodIsCacheable() { $this->setNextResponse(200, ['Cache-Control' => 'public, max-age=10000'], 'Query result', function (Request $request) { $this->assertSame('QUERY', $request->getMethod()); return '{"query": "users"}' === $request->getContent(); }); $this->kernel->reset(); $this->store = $this->createStore(); $this->cacheConfig['debug'] = true; $this->cache = new HttpCache($this->kernel, $this->store, null, $this->cacheConfig); $request1 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "users"}'); $this->response = $this->cache->handle($request1, HttpKernelInterface::MAIN_REQUEST, $this->catch); $this->assertSame(200, $this->response->getStatusCode()); $this->assertTraceContains('miss'); $this->assertSame('Query result', $this->response->getContent()); $request2 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "users"}'); $this->response = $this->cache->handle($request2, HttpKernelInterface::MAIN_REQUEST, $this->catch); $this->assertSame(200, $this->response->getStatusCode()); $this->assertTrue($this->response->headers->has('Age')); $this->assertSame('Query result', $this->response->getContent()); } public function testQueryMethodDifferentBodiesCreateDifferentCacheEntries() { $this->setNextResponses([ [ 'status' => 200, 'body' => 'Users result', 'headers' => ['Cache-Control' => 'public, max-age=10000'], ], [ 'status' => 200, 'body' => 'Posts result', 'headers' => ['Cache-Control' => 'public, max-age=10000'], ], ]); $this->store = $this->createStore(); $this->cacheConfig['debug'] = true; $this->cache = new HttpCache($this->kernel, $this->store, null, $this->cacheConfig); $request1 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "users"}'); $this->response = $this->cache->handle($request1, HttpKernelInterface::MAIN_REQUEST, $this->catch); $this->assertSame('Users result', $this->response->getContent()); $this->assertTraceContains('miss'); $request2 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "posts"}'); $this->response = $this->cache->handle($request2, HttpKernelInterface::MAIN_REQUEST, $this->catch); $this->assertSame('Posts result', $this->response->getContent()); $this->assertTraceContains('miss'); $request3 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "users"}'); $this->response = $this->cache->handle($request3, HttpKernelInterface::MAIN_REQUEST, $this->catch); $this->assertSame('Users result', $this->response->getContent()); $this->assertTrue($this->response->headers->has('Age')); } public function testQueryMethodWithEmptyBodyIsCacheable() { $this->setNextResponse(200, ['Cache-Control' => 'public, max-age=10000'], 'Empty query result'); $this->kernel->reset(); $this->store = $this->createStore(); $this->cacheConfig['debug'] = true; $this->cache = new HttpCache($this->kernel, $this->store, null, $this->cacheConfig); $request1 = Request::create('/', 'QUERY', [], [], [], [], ''); $this->response = $this->cache->handle($request1, HttpKernelInterface::MAIN_REQUEST, $this->catch); $this->assertSame(200, $this->response->getStatusCode()); $this->assertTraceContains('miss'); $request2 = Request::create('/', 'QUERY', [], [], [], [], ''); $this->response = $this->cache->handle($request2, HttpKernelInterface::MAIN_REQUEST, $this->catch); $this->assertSame(200, $this->response->getStatusCode()); $this->assertTrue($this->response->headers->has('Age')); } } class TestKernel implements HttpKernelInterface { public bool $terminateCalled = false; public function terminate(Request $request, Response $response) { $this->terminateCalled = true; } public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response { } } ================================================ FILE: Tests/HttpCache/HttpCacheTestCase.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\HttpCache; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpCache\Esi; use Symfony\Component\HttpKernel\HttpCache\HttpCache; use Symfony\Component\HttpKernel\HttpCache\Store; use Symfony\Component\HttpKernel\HttpKernelInterface; abstract class HttpCacheTestCase extends TestCase { protected $kernel; protected $cache; protected $caches; protected $cacheConfig; protected $request; protected $response; protected $responses; protected $catch; protected $esi; protected ?Store $store = null; protected function setUp(): void { $this->kernel = null; $this->cache = null; $this->esi = null; $this->caches = []; $this->cacheConfig = []; $this->request = null; $this->response = null; $this->responses = []; $this->catch = false; $this->clearDirectory(sys_get_temp_dir().'/http_cache'); } protected function tearDown(): void { $this->cache?->getStore()->cleanup(); $this->kernel = null; $this->cache = null; $this->caches = null; $this->request = null; $this->response = null; $this->responses = null; $this->cacheConfig = null; $this->catch = null; $this->esi = null; $this->clearDirectory(sys_get_temp_dir().'/http_cache'); } public function assertHttpKernelIsCalled() { $this->assertTrue($this->kernel->hasBeenCalled()); } public function assertHttpKernelIsNotCalled() { $this->assertFalse($this->kernel->hasBeenCalled()); } public function assertResponseOk() { $this->assertEquals(200, $this->response->getStatusCode()); } public function assertTraceContains($trace) { $traces = $this->cache->getTraces(); $traces = current($traces); $this->assertMatchesRegularExpression('/'.$trace.'/', implode(', ', $traces)); } public function assertTraceNotContains($trace) { $traces = $this->cache->getTraces(); $traces = current($traces); $this->assertDoesNotMatchRegularExpression('/'.$trace.'/', implode(', ', $traces)); } public function assertExceptionsAreCaught() { $this->assertTrue($this->kernel->isCatchingExceptions()); } public function assertExceptionsAreNotCaught() { $this->assertFalse($this->kernel->isCatchingExceptions()); } public function request($method, $uri = '/', $server = [], $cookies = [], $esi = false, $headers = []) { if (null === $this->kernel) { throw new \LogicException('You must call setNextResponse() before calling request().'); } $this->kernel->reset(); if (!$this->store) { $this->store = $this->createStore(); } if (!isset($this->cacheConfig['debug'])) { $this->cacheConfig['debug'] = true; } $this->esi = $esi ? new Esi() : null; $this->cache = new HttpCache($this->kernel, $this->store, $this->esi, $this->cacheConfig); $this->request = Request::create($uri, $method, [], $cookies, [], $server); $this->request->headers->add($headers); $this->response = $this->cache->handle($this->request, HttpKernelInterface::MAIN_REQUEST, $this->catch); $this->responses[] = $this->response; } public function getMetaStorageValues() { $values = []; foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(sys_get_temp_dir().'/http_cache/md', \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { $values[] = file_get_contents($file); } return $values; } // A basic response with 200 status code and a tiny body. public function setNextResponse($statusCode = 200, array $headers = [], $body = 'Hello World', ?\Closure $customizer = null, ?EventDispatcher $eventDispatcher = null) { $this->kernel = new TestHttpKernel($body, $statusCode, $headers, $customizer, $eventDispatcher); } public function setNextResponses($responses) { $this->kernel = new TestMultipleHttpKernel($responses); } public function catchExceptions($catch = true) { $this->catch = $catch; } public static function clearDirectory($directory) { if (!is_dir($directory)) { return; } $fp = opendir($directory); while (false !== $file = readdir($fp)) { if (!\in_array($file, ['.', '..'], true)) { if (is_link($directory.'/'.$file)) { unlink($directory.'/'.$file); } elseif (is_dir($directory.'/'.$file)) { self::clearDirectory($directory.'/'.$file); rmdir($directory.'/'.$file); } else { unlink($directory.'/'.$file); } } } closedir($fp); } protected function createStore(): Store { return new Store(sys_get_temp_dir().'/http_cache'); } } ================================================ FILE: Tests/HttpCache/ResponseCacheStrategyTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\HttpCache; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpCache\ResponseCacheStrategy; class ResponseCacheStrategyTest extends TestCase { public function testMinimumSharedMaxAgeWins() { $cacheStrategy = new ResponseCacheStrategy(); $response1 = new Response(); $response1->setSharedMaxAge(60); $cacheStrategy->add($response1); $response2 = new Response(); $response2->setSharedMaxAge(3600); $cacheStrategy->add($response2); $response = new Response(); $response->setSharedMaxAge(86400); $cacheStrategy->update($response); $this->assertSame('60', $response->headers->getCacheControlDirective('s-maxage')); } public function testSharedMaxAgeNotSetIfNotSetInAnyEmbeddedRequest() { $cacheStrategy = new ResponseCacheStrategy(); $response1 = new Response(); $response1->setSharedMaxAge(60); $cacheStrategy->add($response1); $response2 = new Response(); $cacheStrategy->add($response2); $response = new Response(); $response->setSharedMaxAge(86400); $cacheStrategy->update($response); $this->assertFalse($response->headers->hasCacheControlDirective('s-maxage')); } public function testSharedMaxAgeNotSetIfNotSetInMainRequest() { $cacheStrategy = new ResponseCacheStrategy(); $response1 = new Response(); $response1->setSharedMaxAge(60); $cacheStrategy->add($response1); $response2 = new Response(); $response2->setSharedMaxAge(3600); $cacheStrategy->add($response2); $response = new Response(); $cacheStrategy->update($response); $this->assertFalse($response->headers->hasCacheControlDirective('s-maxage')); } public function testExpiresHeaderUpdatedFromMaxAge() { $cacheStrategy = new ResponseCacheStrategy(); $response1 = new Response(); $response1->setExpires(new \DateTime('+ 1 hour')); $response1->setPublic(); $cacheStrategy->add($response1); $response = new Response(); $response->setMaxAge(0); $response->setSharedMaxAge(86400); $cacheStrategy->update($response); $this->assertSame('0', $response->headers->getCacheControlDirective('max-age')); $this->assertSame('3600', $response->headers->getCacheControlDirective('s-maxage')); // Expires header must be same as Date header because "max-age" is 0. $this->assertSame($response->headers->get('Date'), $response->headers->get('Expires')); } public function testMaxAgeUpdatedFromExpiresHeader() { $cacheStrategy = new ResponseCacheStrategy(); $response1 = new Response(); $response1->setExpires(new \DateTime('+ 1 hour', new \DateTimeZone('UTC'))); $response1->setPublic(); $cacheStrategy->add($response1); $response = new Response(); $response->setMaxAge(86400); $cacheStrategy->update($response); $this->assertSame('3600', $response->headers->getCacheControlDirective('max-age')); $this->assertNull($response->headers->getCacheControlDirective('s-maxage')); $this->assertSame((new \DateTime('+ 1 hour', new \DateTimeZone('UTC')))->format('D, d M Y H:i:s').' GMT', $response->headers->get('Expires')); } public function testMaxAgeAndSharedMaxAgeUpdatedFromExpiresHeader() { $cacheStrategy = new ResponseCacheStrategy(); $response1 = new Response(); $response1->setExpires(new \DateTime('+ 1 day', new \DateTimeZone('UTC'))); $response1->setPublic(); $cacheStrategy->add($response1); $response = new Response(); $response->setMaxAge(3600); $response->setSharedMaxAge(86400); $cacheStrategy->update($response); $this->assertSame('3600', $response->headers->getCacheControlDirective('max-age')); $this->assertSame('86400', $response->headers->getCacheControlDirective('s-maxage')); $this->assertSame((new \DateTime('+ 1 hour', new \DateTimeZone('UTC')))->format('D, d M Y H:i:s').' GMT', $response->headers->get('Expires')); } public function testMainResponseNotCacheableWhenEmbeddedResponseRequiresValidation() { $cacheStrategy = new ResponseCacheStrategy(); $embeddedResponse = new Response(); $embeddedResponse->setLastModified(new \DateTimeImmutable()); $cacheStrategy->add($embeddedResponse); $mainResponse = new Response(); $mainResponse->setSharedMaxAge(3600); $cacheStrategy->update($mainResponse); $this->assertTrue($mainResponse->headers->hasCacheControlDirective('no-cache')); $this->assertTrue($mainResponse->headers->hasCacheControlDirective('must-revalidate')); $this->assertFalse($mainResponse->isFresh()); } public function testValidationOnMainResponseIsNotPossibleWhenItContainsEmbeddedResponses() { $cacheStrategy = new ResponseCacheStrategy(); // This main response uses the "validation" model $mainResponse = new Response(); $mainResponse->setLastModified(new \DateTimeImmutable()); $mainResponse->setEtag('foo'); // Embedded response uses "expiry" model $embeddedResponse = new Response(); $mainResponse->setSharedMaxAge(3600); $cacheStrategy->add($embeddedResponse); $cacheStrategy->update($mainResponse); $this->assertFalse($mainResponse->isValidateable()); $this->assertFalse($mainResponse->headers->has('Last-Modified')); $this->assertFalse($mainResponse->headers->has('ETag')); $this->assertTrue($mainResponse->headers->hasCacheControlDirective('no-cache')); $this->assertTrue($mainResponse->headers->hasCacheControlDirective('must-revalidate')); } public function testMainResponseWithValidationIsUnchangedWhenThereIsNoEmbeddedResponse() { $cacheStrategy = new ResponseCacheStrategy(); $mainResponse = new Response(); $mainResponse->setLastModified(new \DateTimeImmutable()); $cacheStrategy->update($mainResponse); $this->assertTrue($mainResponse->isValidateable()); } public function testMainResponseWithExpirationIsUnchangedWhenThereIsNoEmbeddedResponse() { $cacheStrategy = new ResponseCacheStrategy(); $mainResponse = new Response(); $mainResponse->setSharedMaxAge(3600); $cacheStrategy->update($mainResponse); $this->assertTrue($mainResponse->isFresh()); } public function testLastModifiedIsMergedWithEmbeddedResponse() { $cacheStrategy = new ResponseCacheStrategy(); $mainResponse = new Response(); $mainResponse->setLastModified(new \DateTimeImmutable('-2 hour')); $embeddedDate = new \DateTimeImmutable('-1 hour'); $embeddedResponse = new Response(); $embeddedResponse->setLastModified($embeddedDate); $cacheStrategy->add($embeddedResponse); $cacheStrategy->update($mainResponse); $this->assertTrue($mainResponse->headers->has('Last-Modified')); $this->assertSame($embeddedDate->getTimestamp(), $mainResponse->getLastModified()->getTimestamp()); } public function testLastModifiedIsRemovedWhenEmbeddedResponseHasNoLastModified() { $cacheStrategy = new ResponseCacheStrategy(); $mainResponse = new Response(); $mainResponse->setLastModified(new \DateTimeImmutable('-2 hour')); $embeddedResponse = new Response(); $cacheStrategy->add($embeddedResponse); $cacheStrategy->update($mainResponse); $this->assertFalse($mainResponse->headers->has('Last-Modified')); } public function testLastModifiedIsNotAddedWhenMainResponseHasNoLastModified() { $cacheStrategy = new ResponseCacheStrategy(); $mainResponse = new Response(); $embeddedResponse = new Response(); $embeddedResponse->setLastModified(new \DateTimeImmutable('-2 hour')); $cacheStrategy->add($embeddedResponse); $cacheStrategy->update($mainResponse); $this->assertFalse($mainResponse->headers->has('Last-Modified')); } public function testMainResponseIsNotCacheableWhenEmbeddedResponseIsNotCacheable() { $cacheStrategy = new ResponseCacheStrategy(); $mainResponse = new Response(); $mainResponse->setSharedMaxAge(3600); // Public, cacheable /* This response has no validation or expiration information. That makes it uncacheable, it is always stale. (It does *not* make this private, though.) */ $embeddedResponse = new Response(); $this->assertFalse($embeddedResponse->isFresh()); // not fresh, as no lifetime is provided $cacheStrategy->add($embeddedResponse); $cacheStrategy->update($mainResponse); $this->assertTrue($mainResponse->headers->hasCacheControlDirective('no-cache')); $this->assertTrue($mainResponse->headers->hasCacheControlDirective('must-revalidate')); $this->assertFalse($mainResponse->isFresh()); } public function testEmbeddingPrivateResponseMakesMainResponsePrivate() { $cacheStrategy = new ResponseCacheStrategy(); $mainResponse = new Response(); $mainResponse->setSharedMaxAge(3600); // public, cacheable // The embedded response might for example contain per-user data that remains valid for 60 seconds $embeddedResponse = new Response(); $embeddedResponse->setPrivate(); $embeddedResponse->setMaxAge(60); // this would implicitly set "private" as well, but let's be explicit $cacheStrategy->add($embeddedResponse); $cacheStrategy->update($mainResponse); $this->assertTrue($mainResponse->headers->hasCacheControlDirective('private')); $this->assertFalse($mainResponse->headers->hasCacheControlDirective('public')); } public function testEmbeddingPublicResponseDoesNotMakeMainResponsePublic() { $cacheStrategy = new ResponseCacheStrategy(); $mainResponse = new Response(); $mainResponse->setPrivate(); // this is the default, but let's be explicit $mainResponse->setMaxAge(100); $embeddedResponse = new Response(); $embeddedResponse->setPublic(); $embeddedResponse->setSharedMaxAge(100); $cacheStrategy->add($embeddedResponse); $cacheStrategy->update($mainResponse); $this->assertTrue($mainResponse->headers->hasCacheControlDirective('private')); $this->assertFalse($mainResponse->headers->hasCacheControlDirective('public')); } public function testResponseIsExiprableWhenEmbeddedResponseCombinesExpiryAndValidation() { /* When "expiration wins over validation" (https://symfony.com/doc/current/http_cache/validation.html) * and both the main and embedded response provide s-maxage, then the more restricting value of both * should be fine, regardless of whether the embedded response can be validated later on or must be * completely regenerated. */ $cacheStrategy = new ResponseCacheStrategy(); $mainResponse = new Response(); $mainResponse->setSharedMaxAge(3600); $embeddedResponse = new Response(); $embeddedResponse->setSharedMaxAge(60); $embeddedResponse->setEtag('foo'); $cacheStrategy->add($embeddedResponse); $cacheStrategy->update($mainResponse); $this->assertEqualsWithDelta(60, (int) $mainResponse->headers->getCacheControlDirective('s-maxage'), 1); } public function testResponseIsExpirableButNotValidateableWhenMainResponseCombinesExpirationAndValidation() { $cacheStrategy = new ResponseCacheStrategy(); $mainResponse = new Response(); $mainResponse->setSharedMaxAge(3600); $mainResponse->setEtag('foo'); $mainResponse->setLastModified(new \DateTimeImmutable()); $embeddedResponse = new Response(); $embeddedResponse->setSharedMaxAge(60); $cacheStrategy->add($embeddedResponse); $cacheStrategy->update($mainResponse); $this->assertSame('60', $mainResponse->headers->getCacheControlDirective('s-maxage')); $this->assertFalse($mainResponse->isValidateable()); } #[DataProvider('cacheControlMergingProvider')] #[Group('time-sensitive')] public function testCacheControlMerging(array $expects, array $main, array $surrogates) { $cacheStrategy = new ResponseCacheStrategy(); $buildResponse = static function ($config) { $response = new Response(); foreach ($config as $key => $value) { switch ($key) { case 'age': $response->headers->set('Age', $value); break; case 'expires': $expires = clone $response->getDate(); $expires = $expires->modify('+'.$value.' seconds'); $response->setExpires($expires); break; case 'max-age': $response->setMaxAge($value); break; case 's-maxage': $response->setSharedMaxAge($value); break; case 'private': $response->setPrivate(); break; case 'public': $response->setPublic(); break; default: $response->headers->addCacheControlDirective($key, $value); } } return $response; }; foreach ($surrogates as $config) { $cacheStrategy->add($buildResponse($config)); } $response = $buildResponse($main); $cacheStrategy->update($response); foreach ($expects as $key => $value) { if ('expires' === $key) { $this->assertSame($value, $response->getExpires()->format('U') - $response->getDate()->format('U')); } elseif ('age' === $key) { $this->assertSame($value, $response->getAge()); } elseif (true === $value) { $this->assertTrue($response->headers->hasCacheControlDirective($key), \sprintf('Cache-Control header must have "%s" flag', $key)); } elseif (false === $value) { $this->assertFalse( $response->headers->hasCacheControlDirective($key), \sprintf('Cache-Control header must NOT have "%s" flag', $key) ); } else { $this->assertSame($value, $response->headers->getCacheControlDirective($key), \sprintf('Cache-Control flag "%s" should be "%s"', $key, $value)); } } } public static function cacheControlMergingProvider() { yield 'result is public if all responses are public' => [ ['private' => false, 'public' => true], ['public' => true], [ ['public' => true], ], ]; yield 'result is private by default' => [ ['private' => true, 'public' => false], ['public' => true], [ [], ], ]; yield 'combines public and private responses' => [ ['must-revalidate' => false, 'private' => true, 'public' => false], ['public' => true], [ ['private' => true], ], ]; yield 'inherits no-cache from surrogates' => [ ['no-cache' => true, 'public' => false], ['public' => true], [ ['no-cache' => true], ], ]; yield 'inherits no-store from surrogate' => [ ['no-store' => true, 'public' => false], ['public' => true], [ ['no-store' => true], ], ]; yield 'resolve to lowest possible max-age' => [ ['public' => false, 'private' => true, 's-maxage' => false, 'max-age' => '60'], ['public' => true, 'max-age' => 3600], [ ['private' => true, 'max-age' => 60], ], ]; yield 'resolves multiple max-age' => [ ['public' => false, 'private' => true, 's-maxage' => false, 'max-age' => '60'], ['private' => true, 'max-age' => 100], [ ['private' => true, 'max-age' => 3600], ['public' => true, 'max-age' => 60, 's-maxage' => 60], ['private' => true, 'max-age' => 60], ], ]; yield 'merge max-age and s-maxage' => [ ['public' => true, 'max-age' => null, 's-maxage' => '60'], ['public' => true, 's-maxage' => 3600], [ ['public' => true, 'max-age' => 60], ], ]; yield 's-maxage may be set to 0' => [ ['public' => true, 's-maxage' => '0', 'max-age' => null], ['public' => true, 's-maxage' => '0'], [ ['public' => true, 's-maxage' => '60'], ], ]; yield 's-maxage may be set to 0, and works independently from maxage' => [ ['public' => true, 's-maxage' => '0', 'max-age' => '30'], ['public' => true, 's-maxage' => '0', 'max-age' => '30'], [ ['public' => true, 'max-age' => '60'], ], ]; yield 'public subresponse without lifetime does not remove lifetime for main response' => [ ['public' => true, 's-maxage' => '30', 'max-age' => null], ['public' => true, 's-maxage' => '30'], [ ['public' => true], ], ]; yield 'lifetime for subresponse is kept when main response has no lifetime' => [ ['public' => true, 'max-age' => '30'], ['public' => true], [ ['public' => true, 'max-age' => '30'], ], ]; yield 's-maxage on the subresponse implies public, so the result is public as well' => [ ['public' => true, 'max-age' => '10', 's-maxage' => null], ['public' => true, 'max-age' => '10'], [ ['max-age' => '30', 's-maxage' => '20'], ], ]; yield 'result is private when combining private responses' => [ ['no-cache' => false, 'must-revalidate' => false, 'private' => true], ['s-maxage' => 60, 'private' => true], [ ['s-maxage' => 60, 'private' => true], ], ]; yield 'result can have s-maxage and max-age' => [ ['public' => true, 'private' => false, 's-maxage' => '60', 'max-age' => '30'], ['s-maxage' => 100, 'max-age' => 2000], [ ['s-maxage' => 1000, 'max-age' => 30], ['s-maxage' => 500, 'max-age' => 500], ['s-maxage' => 60, 'max-age' => 1000], ], ]; yield 'does not set headers without value' => [ ['max-age' => null, 's-maxage' => null, 'public' => null], ['private' => true], [ ['private' => true], ], ]; yield 'max-age 0 is sent to the client' => [ ['private' => true, 'max-age' => '0'], ['max-age' => 0, 'private' => true], [ ['max-age' => 60, 'private' => true], ], ]; yield 'max-age is relative to age' => [ ['max-age' => '240', 'age' => 60], ['max-age' => 180], [ ['max-age' => 600, 'age' => 60], ], ]; yield 'retains lowest age of all responses' => [ ['max-age' => '160', 'age' => 60], ['max-age' => 600, 'age' => 60], [ ['max-age' => 120, 'age' => 20], ], ]; yield 'max-age can be less than age, essentially expiring the response' => [ ['age' => 120, 'max-age' => '90'], ['max-age' => 90, 'age' => 120], [ ['max-age' => 120, 'age' => 60], ], ]; yield 'max-age is 0 regardless of age' => [ ['max-age' => '0'], ['max-age' => 60], [ ['max-age' => 0, 'age' => 60], ], ]; yield 'max-age is not negative' => [ ['max-age' => '0'], ['max-age' => 0], [ ['max-age' => 0, 'age' => 60], ], ]; yield 'calculates lowest Expires header' => [ ['expires' => 60], ['expires' => 60], [ ['expires' => 120], ], ]; yield 'calculates Expires header relative to age' => [ ['expires' => 210, 'age' => 120], ['expires' => 90], [ ['expires' => 600, 'age' => '120'], ], ]; } } ================================================ FILE: Tests/HttpCache/SsiTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\HttpCache; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpCache\HttpCache; use Symfony\Component\HttpKernel\HttpCache\Ssi; class SsiTest extends TestCase { public function testHasSurrogateSsiCapability() { $ssi = new Ssi(); $request = Request::create('/'); $request->headers->set('Surrogate-Capability', 'abc="SSI/1.0"'); $this->assertTrue($ssi->hasSurrogateCapability($request)); $request = Request::create('/'); $request->headers->set('Surrogate-Capability', 'foobar'); $this->assertFalse($ssi->hasSurrogateCapability($request)); $request = Request::create('/'); $this->assertFalse($ssi->hasSurrogateCapability($request)); } public function testAddSurrogateSsiCapability() { $ssi = new Ssi(); $request = Request::create('/'); $ssi->addSurrogateCapability($request); $this->assertEquals('symfony="SSI/1.0"', $request->headers->get('Surrogate-Capability')); $ssi->addSurrogateCapability($request); $this->assertEquals('symfony="SSI/1.0", symfony="SSI/1.0"', $request->headers->get('Surrogate-Capability')); } public function testAddSurrogateControl() { $ssi = new Ssi(); $response = new Response('foo '); $ssi->addSurrogateControl($response); $this->assertEquals('content="SSI/1.0"', $response->headers->get('Surrogate-Control')); $response = new Response('foo'); $ssi->addSurrogateControl($response); $this->assertEquals('', $response->headers->get('Surrogate-Control')); } public function testNeedsSsiParsing() { $ssi = new Ssi(); $response = new Response(); $response->headers->set('Surrogate-Control', 'content="SSI/1.0"'); $this->assertTrue($ssi->needsParsing($response)); $response = new Response(); $this->assertFalse($ssi->needsParsing($response)); } public function testRenderIncludeTag() { $ssi = new Ssi(); $this->assertEquals('', $ssi->renderIncludeTag('/', '/alt', true)); $this->assertEquals('', $ssi->renderIncludeTag('/', '/alt', false)); $this->assertEquals('', $ssi->renderIncludeTag('/')); } public function testProcessDoesNothingIfContentTypeIsNotHtml() { $ssi = new Ssi(); $request = Request::create('/'); $response = new Response(); $response->headers->set('Content-Type', 'text/plain'); $ssi->process($request, $response); $this->assertFalse($response->headers->has('x-body-eval')); } public function testProcess() { $ssi = new Ssi(); $request = Request::create('/'); $response = new Response('foo '); $ssi->process($request, $response); $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); $this->assertSame(['', 'foo ', "...\n\n\n", ''], $content); $this->assertEquals('SSI', $response->headers->get('x-body-eval')); $response = new Response('foo '); $ssi->process($request, $response); $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); $this->assertSame(['', 'foo ', "foo'\n\n\n", ''], $content); } public function testProcessEscapesPhpTags() { $ssi = new Ssi(); $request = Request::create('/'); $response = new Response(''); $ssi->process($request, $response); $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); $this->assertSame(['', '', ''], $content); } public function testProcessWhenNoSrcInAnSsi() { $this->expectException(\RuntimeException::class); $ssi = new Ssi(); $request = Request::create('/'); $response = new Response('foo '); $ssi->process($request, $response); } public function testProcessRemoveSurrogateControlHeader() { $ssi = new Ssi(); $request = Request::create('/'); $response = new Response('foo '); $response->headers->set('Surrogate-Control', 'content="SSI/1.0"'); $ssi->process($request, $response); $this->assertEquals('SSI', $response->headers->get('x-body-eval')); $response->headers->set('Surrogate-Control', 'no-store, content="SSI/1.0"'); $ssi->process($request, $response); $this->assertEquals('SSI', $response->headers->get('x-body-eval')); $this->assertEquals('no-store', $response->headers->get('surrogate-control')); $response->headers->set('Surrogate-Control', 'content="SSI/1.0", no-store'); $ssi->process($request, $response); $this->assertEquals('SSI', $response->headers->get('x-body-eval')); $this->assertEquals('no-store', $response->headers->get('surrogate-control')); } public function testHandle() { $ssi = new Ssi(); $cache = $this->getCache(Request::create('/'), new Response('foo')); $this->assertEquals('foo', $ssi->handle($cache, '/', '/alt', true)); } public function testHandleWhenResponseIsNot200() { $this->expectException(\RuntimeException::class); $ssi = new Ssi(); $response = new Response('foo'); $response->setStatusCode(404); $cache = $this->getCache(Request::create('/'), $response); $ssi->handle($cache, '/', '/alt', false); } public function testHandleWhenResponseIsNot200AndErrorsAreIgnored() { $ssi = new Ssi(); $response = new Response('foo'); $response->setStatusCode(404); $cache = $this->getCache(Request::create('/'), $response); $this->assertEquals('', $ssi->handle($cache, '/', '/alt', true)); } public function testHandleWhenResponseIsNot200AndAltIsPresent() { $ssi = new Ssi(); $response1 = new Response('foo'); $response1->setStatusCode(404); $response2 = new Response('bar'); $cache = $this->getCache(Request::create('/'), [$response1, $response2]); $this->assertEquals('bar', $ssi->handle($cache, '/', '/alt', false)); } protected function getCache($request, $response) { $cache = $this->getMockBuilder(HttpCache::class)->onlyMethods(['getRequest', 'handle'])->disableOriginalConstructor()->getMock(); $cache->expects($this->atLeastOnce()) ->method('getRequest') ->willReturn($request) ; if (\is_array($response)) { $cache ->method('handle') ->willReturn(...$response) ; } else { $cache ->method('handle') ->willReturn($response) ; } return $cache; } } ================================================ FILE: Tests/HttpCache/StoreTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\HttpCache; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpCache\Store; class StoreTest extends TestCase { protected Request $request; protected Response $response; protected Store $store; protected function setUp(): void { $this->request = Request::create('/'); $this->response = new Response('hello world', 200, []); HttpCacheTestCase::clearDirectory(sys_get_temp_dir().'/http_cache'); $this->store = new Store(sys_get_temp_dir().'/http_cache'); } protected function tearDown(): void { HttpCacheTestCase::clearDirectory(sys_get_temp_dir().'/http_cache'); } public function testReadsAnEmptyArrayWithReadWhenNothingCachedAtKey() { $this->assertSame([], $this->getStoreMetadata('/nothing')); } public function testUnlockFileThatDoesExist() { $this->storeSimpleEntry(); $this->store->lock($this->request); $this->assertTrue($this->store->unlock($this->request)); } public function testUnlockFileThatDoesNotExist() { $this->assertFalse($this->store->unlock($this->request)); } public function testRemovesEntriesForKeyWithPurge() { $request = Request::create('/foo'); $this->store->write($request, new Response('foo')); $metadata = $this->getStoreMetadata($request); $this->assertNotEmpty($metadata); $this->assertTrue($this->store->purge('/foo')); $this->assertSame([], $this->getStoreMetadata($request)); // cached content should be kept after purging $path = $this->store->getPath($metadata[0][1]['x-content-digest'][0]); $this->assertTrue(is_file($path)); $this->assertFalse($this->store->purge('/bar')); } public function testStoresACacheEntry() { $cacheKey = $this->storeSimpleEntry(); $this->assertNotEmpty($this->getStoreMetadata($cacheKey)); } public function testSetsTheXContentDigestResponseHeaderBeforeStoring() { $cacheKey = $this->storeSimpleEntry(); $entries = $this->getStoreMetadata($cacheKey); [, $res] = $entries[0]; $this->assertEquals('en6c78e0e3bd51d358d01e758642b85fb8', $res['x-content-digest'][0]); } public function testDoesNotTrustXContentDigestFromUpstream() { $response = new Response('test', 200, ['X-Content-Digest' => 'untrusted-from-elsewhere']); $cacheKey = $this->store->write($this->request, $response); $entries = $this->getStoreMetadata($cacheKey); [, $res] = $entries[0]; $this->assertEquals('en6c78e0e3bd51d358d01e758642b85fb8', $res['x-content-digest'][0]); $this->assertEquals('en6c78e0e3bd51d358d01e758642b85fb8', $response->headers->get('X-Content-Digest')); } public function testWritesResponseEvenIfXContentDigestIsPresent() { // Prime the store $this->store->write($this->request, new Response('test', 200, ['X-Content-Digest' => 'untrusted-from-elsewhere'])); $response = $this->store->lookup($this->request); $this->assertNotNull($response); } public function testWritingARestoredResponseDoesNotCorruptCache() { /* * This covers the regression reported in https://github.com/symfony/symfony/issues/37174. * * A restored response does *not* load the body, but only keep the file path in a special X-Body-File * header. For reasons (?), the file path was also used as the restored response body. * It would be up to others (HttpCache...?) to honor this header and actually load the response content * from there. * * When a restored response was stored again, the Store itself would ignore the header. In the first * step, this would compute a new Content Digest based on the file path in the restored response body; * this is covered by "Checkpoint 1" below. But, since the X-Body-File header was left untouched (Checkpoint 2), downstream * code (HttpCache...) would not immediately notice. * * Only upon performing the lookup for a second time, we'd get a Response where the (wrong) Content Digest * is also reflected in the X-Body-File header, this time also producing wrong content when the downstream * evaluates it. */ $this->store->write($this->request, $this->response); $digest = $this->response->headers->get('X-Content-Digest'); $path = $this->getStorePath($digest); $response = $this->store->lookup($this->request); $this->store->write($this->request, $response); $this->assertEquals($digest, $response->headers->get('X-Content-Digest')); // Checkpoint 1 $this->assertEquals($path, $response->headers->get('X-Body-File')); // Checkpoint 2 $response = $this->store->lookup($this->request); $this->assertEquals($digest, $response->headers->get('X-Content-Digest')); $this->assertEquals($path, $response->headers->get('X-Body-File')); } public function testFindsAStoredEntryWithLookup() { $this->storeSimpleEntry(); $response = $this->store->lookup($this->request); $this->assertNotNull($response); $this->assertInstanceOf(Response::class, $response); } public function testDoesNotFindAnEntryWithLookupWhenNoneExists() { $request = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']); $this->assertNull($this->store->lookup($request)); } public function testCanonizesUrlsForCacheKeys() { $this->storeSimpleEntry($path = '/test?x=y&p=q'); $hitsReq = Request::create($path); $missReq = Request::create('/test?p=x'); $this->assertNotNull($this->store->lookup($hitsReq)); $this->assertNull($this->store->lookup($missReq)); } public function testDoesNotFindAnEntryWithLookupWhenTheBodyDoesNotExist() { $this->storeSimpleEntry(); $this->assertNotNull($this->response->headers->get('X-Content-Digest')); $path = $this->getStorePath($this->response->headers->get('X-Content-Digest')); @unlink($path); $this->assertNull($this->store->lookup($this->request)); } public function testRestoresResponseHeadersProperlyWithLookup() { $this->storeSimpleEntry(); $response = $this->store->lookup($this->request); $this->assertEquals($response->headers->all(), array_merge(['content-length' => 4, 'x-body-file' => [$this->getStorePath($response->headers->get('X-Content-Digest'))]], $this->response->headers->all())); } public function testRestoresResponseContentFromEntityStoreWithLookup() { $this->storeSimpleEntry(); $response = $this->store->lookup($this->request); $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test')), $response->headers->get('X-Body-File')); } public function testInvalidatesMetaAndEntityStoreEntriesWithInvalidate() { $this->storeSimpleEntry(); $this->store->invalidate($this->request); $response = $this->store->lookup($this->request); $this->assertInstanceOf(Response::class, $response); $this->assertFalse($response->isFresh()); } public function testSucceedsQuietlyWhenInvalidateCalledWithNoMatchingEntries() { $req = Request::create('/test'); $this->store->invalidate($req); $this->assertNull($this->store->lookup($this->request)); } public function testDoesNotReturnEntriesThatVaryWithLookup() { $req1 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']); $req2 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam']); $res = new Response('test', 200, ['Vary' => 'Foo Bar']); $this->store->write($req1, $res); $this->assertNull($this->store->lookup($req2)); } public function testDoesNotReturnEntriesThatSlightlyVaryWithLookup() { $req1 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']); $req2 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bam']); $res = new Response('test', 200, ['Vary' => ['Foo', 'Bar']]); $this->store->write($req1, $res); $this->assertNull($this->store->lookup($req2)); } public function testStoresMultipleResponsesForEachVaryCombination() { $req1 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']); $res1 = new Response('test 1', 200, ['Vary' => 'Foo Bar']); $key = $this->store->write($req1, $res1); $req2 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam']); $res2 = new Response('test 2', 200, ['Vary' => 'Foo Bar']); $this->store->write($req2, $res2); $req3 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Baz', 'HTTP_BAR' => 'Boom']); $res3 = new Response('test 3', 200, ['Vary' => 'Foo Bar']); $this->store->write($req3, $res3); $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File')); $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File')); $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 1')), $this->store->lookup($req1)->headers->get('X-Body-File')); $this->assertCount(3, $this->getStoreMetadata($key)); } public function testOverwritesNonVaryingResponseWithStore() { $req1 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']); $res1 = new Response('test 1', 200, ['Vary' => 'Foo Bar']); $this->store->write($req1, $res1); $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 1')), $this->store->lookup($req1)->headers->get('X-Body-File')); $req2 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam']); $res2 = new Response('test 2', 200, ['Vary' => 'Foo Bar']); $this->store->write($req2, $res2); $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File')); $req3 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']); $res3 = new Response('test 3', 200, ['Vary' => 'Foo Bar']); $key = $this->store->write($req3, $res3); $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File')); $this->assertCount(2, $this->getStoreMetadata($key)); } public function testLocking() { $req = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']); $this->assertTrue($this->store->lock($req)); $this->store->lock($req); $this->assertTrue($this->store->isLocked($req)); $this->store->unlock($req); $this->assertFalse($this->store->isLocked($req)); } public function testPurgeHttps() { $request = Request::create('https://example.com/foo'); $this->store->write($request, new Response('foo')); $this->assertNotEmpty($this->getStoreMetadata($request)); $this->assertTrue($this->store->purge('https://example.com/foo')); $this->assertSame([], $this->getStoreMetadata($request)); } public function testPurgeHttpAndHttps() { $requestHttp = Request::create('https://example.com/foo'); $this->store->write($requestHttp, new Response('foo')); $requestHttps = Request::create('http://example.com/foo'); $this->store->write($requestHttps, new Response('foo')); $this->assertNotEmpty($this->getStoreMetadata($requestHttp)); $this->assertNotEmpty($this->getStoreMetadata($requestHttps)); $this->assertTrue($this->store->purge('http://example.com/foo')); $this->assertSame([], $this->getStoreMetadata($requestHttp)); $this->assertSame([], $this->getStoreMetadata($requestHttps)); } public function testDoesNotStorePrivateHeaders() { $request = Request::create('https://example.com/foo'); $response = new Response('foo'); $response->headers->setCookie(Cookie::fromString('foo=bar')); $this->store->write($request, $response); $this->assertArrayNotHasKey('set-cookie', $this->getStoreMetadata($request)[0][1]); $this->assertNotEmpty($response->headers->getCookies()); } public function testDiscardsInvalidBodyEval() { $request = Request::create('https://example.com/foo'); $response = new Response('foo', 200, ['X-Body-Eval' => 'SSI']); $this->store->write($request, $response); $this->assertNull($this->store->lookup($request)); $request = Request::create('https://example.com/foo'); $content = str_repeat('a', 24).'b'.str_repeat('a', 24).'b'; $response = new Response($content, 200, ['X-Body-Eval' => 'SSI']); $this->store->write($request, $response); $this->assertNull($this->store->lookup($request)); } public function testLoadsBodyEval() { $request = Request::create('https://example.com/foo'); $content = str_repeat('a', 24).'b'.str_repeat('a', 24); $response = new Response($content, 200, ['X-Body-Eval' => 'SSI']); $this->store->write($request, $response); $response = $this->store->lookup($request); $this->assertSame($content, $response->getContent()); } /** * Basic case when the second header has a different value. * Both responses should be cached. */ public function testWriteWithMultipleVaryAndCachedAllResponse() { $req1 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_BAR' => 'bar']); $content = str_repeat('a', 24).'b'.str_repeat('a', 24); $res1 = new Response($content, 200, ['vary' => ['Foo', 'Bar'], 'X-Body-Eval' => 'SSI']); $this->store->write($req1, $res1); $responseLook = $this->store->lookup($req1); $this->assertSame($content, $responseLook->getContent()); $req2 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_BAR' => 'foobar']); $content2 = str_repeat('b', 24).'a'.str_repeat('b', 24); $res2 = new Response($content2, 200, ['vary' => ['Foo', 'Bar'], 'X-Body-Eval' => 'SSI']); $this->store->write($req2, $res2); $responseLook = $this->store->lookup($req2); $this->assertSame($content2, $responseLook->getContent()); $responseLook = $this->store->lookup($req1); $this->assertSame($content, $responseLook->getContent()); } /** * Basic case when the second header has the same value on both requests. * The last response should be cached. */ public function testWriteWithMultipleVaryAndCachedLastResponse() { $req1 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_BAR' => 'bar']); $content = str_repeat('a', 24).'b'.str_repeat('a', 24); $res1 = new Response($content, 200, ['vary' => ['Foo', 'Bar'], 'X-Body-Eval' => 'SSI']); $this->store->write($req1, $res1); $responseLook = $this->store->lookup($req1); $this->assertSame($content, $responseLook->getContent()); $req2 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_BAR' => 'bar']); $content2 = str_repeat('b', 24).'a'.str_repeat('b', 24); $res2 = new Response($content2, 200, ['vary' => ['Foo', 'Bar'], 'X-Body-Eval' => 'SSI']); $this->store->write($req2, $res2); $responseLook = $this->store->lookup($req2); $this->assertSame($content2, $responseLook->getContent()); $responseLook = $this->store->lookup($req1); $this->assertSame($content2, $responseLook->getContent()); } /** * Case when a vary value has been removed. * Both responses should be cached. */ public function testWriteWithChangingVary() { $req1 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_BAR' => 'bar']); $content = str_repeat('a', 24).'b'.str_repeat('a', 24); $res1 = new Response($content, 200, ['vary' => ['Foo', 'bar', 'foobar'], 'X-Body-Eval' => 'SSI']); $this->store->write($req1, $res1); $req2 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_FOOBAR' => 'bar']); $content2 = str_repeat('b', 24).'a'.str_repeat('b', 24); $res2 = new Response($content2, 200, ['vary' => ['Foo', 'foobar'], 'X-Body-Eval' => 'SSI']); $this->store->write($req2, $res2); $responseLook = $this->store->lookup($req2); $this->assertSame($content2, $responseLook->getContent()); $responseLook = $this->store->lookup($req1); $this->assertSame($content, $responseLook->getContent()); } /** * Case when a vary value has been removed and headers of the new vary list are the same. * The last response should be cached. */ public function testWriteWithRemoveVaryAndAllHeadersOnTheList() { $req1 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_FOOBAR' => 'bar']); $content = str_repeat('a', 24).'b'.str_repeat('a', 24); $res1 = new Response($content, 200, ['vary' => ['Foo', 'bar', 'foobar'], 'X-Body-Eval' => 'SSI']); $this->store->write($req1, $res1); $req2 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_FOOBAR' => 'bar']); $content2 = str_repeat('b', 24).'a'.str_repeat('b', 24); $res2 = new Response($content2, 200, ['vary' => ['Foo', 'foobar'], 'X-Body-Eval' => 'SSI']); $this->store->write($req2, $res2); $responseLook = $this->store->lookup($req2); $this->assertSame($content2, $responseLook->getContent()); $responseLook = $this->store->lookup($req1); $this->assertSame($content2, $responseLook->getContent()); } /** * Case when a vary value has been added and headers of the new vary list are the same. * The last response should be cached. */ public function testWriteWithAddingVaryAndAllHeadersOnTheList() { $req1 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_FOOBAR' => 'bar']); $content = str_repeat('a', 24).'b'.str_repeat('a', 24); $res1 = new Response($content, 200, ['vary' => ['Foo', 'foobar'], 'X-Body-Eval' => 'SSI']); $this->store->write($req1, $res1); $req2 = Request::create('/foo', 'get', [], [], [], ['HTTP_FOO' => 'foo', 'HTTP_BAR' => 'foobar', 'HTTP_FOOBAR' => 'bar']); $content2 = str_repeat('b', 24).'a'.str_repeat('b', 24); $res2 = new Response($content2, 200, ['vary' => ['Foo', 'bar', 'foobar'], 'X-Body-Eval' => 'SSI']); $this->store->write($req2, $res2); $responseLook = $this->store->lookup($req2); $this->assertSame($content2, $responseLook->getContent()); $responseLook = $this->store->lookup($req1); $this->assertSame($content, $responseLook->getContent()); } protected function storeSimpleEntry($path = null, $headers = []) { $path ??= '/test'; $this->request = Request::create($path, 'get', [], [], [], $headers); $this->response = new Response('test', 200, ['Cache-Control' => 'max-age=420']); return $this->store->write($this->request, $this->response); } protected function getStoreMetadata($key) { $r = new \ReflectionObject($this->store); $m = $r->getMethod('getMetadata'); if ($key instanceof Request) { $m1 = $r->getMethod('getCacheKey'); $key = $m1->invoke($this->store, $key); } return $m->invoke($this->store, $key); } protected function getStorePath($key) { $r = new \ReflectionObject($this->store); $m = $r->getMethod('getPath'); return $m->invoke($this->store, $key); } public function testQueryMethodCacheKeyIncludesBody() { $response = new Response('test', 200, ['Cache-Control' => 'max-age=420']); $request1 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "users"}'); $request2 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "posts"}'); $request3 = Request::create('/', 'QUERY', [], [], [], [], '{"query": "users"}'); $key1 = $this->store->write($request1, $response); $key2 = $this->store->write($request2, $response); $key3 = $this->store->write($request3, $response); $this->assertNotSame($key1, $key2); $this->assertSame($key1, $key3); $this->assertNotEmpty($this->getStoreMetadata($key1)); $this->assertNotEmpty($this->getStoreMetadata($key2)); $this->assertNotNull($this->store->lookup($request1)); $this->assertNotNull($this->store->lookup($request2)); $this->assertNotNull($this->store->lookup($request3)); } public function testQueryMethodCacheKeyDiffersFromGet() { $response = new Response('test', 200, ['Cache-Control' => 'max-age=420']); $getRequest = Request::create('/'); $queryRequest = Request::create('/', 'QUERY', [], [], [], [], '{"query": "test"}'); $getKey = $this->store->write($getRequest, $response); $queryKey = $this->store->write($queryRequest, $response); $this->assertNotSame($getKey, $queryKey); $this->assertNotEmpty($this->getStoreMetadata($getKey)); $this->assertNotEmpty($this->getStoreMetadata($queryKey)); $this->assertNotNull($this->store->lookup($getRequest)); $this->assertNotNull($this->store->lookup($queryRequest)); } public function testOtherMethodsCacheKeyIgnoresBody() { $response1 = new Response('test 1', 200, ['Cache-Control' => 'max-age=420']); $response2 = new Response('test 2', 200, ['Cache-Control' => 'max-age=420']); $getRequest1 = Request::create('/', 'GET', [], [], [], [], '{"data": "test"}'); $getRequest2 = Request::create('/', 'GET', [], [], [], [], '{"data": "different"}'); $key1 = $this->store->write($getRequest1, $response1); $key2 = $this->store->write($getRequest2, $response2); $this->assertSame($key1, $key2); $lookup1 = $this->store->lookup($getRequest1); $lookup2 = $this->store->lookup($getRequest2); $this->assertNotNull($lookup1); $this->assertNotNull($lookup2); $this->assertCount(1, $this->getStoreMetadata($key1)); $this->assertSame($lookup1->getContent(), $lookup2->getContent()); } public function testQueryMethodCacheKeyAvoidsBoundaryCollisions() { $response = new Response('test', 200, ['Cache-Control' => 'max-age=420']); $request1 = Request::create('/api/query', 'QUERY', [], [], [], [], 'test'); $request2 = Request::create('/api/que', 'QUERY', [], [], [], [], 'rytest'); $key1 = $this->store->write($request1, $response); $key2 = $this->store->write($request2, $response); $this->assertNotSame($key1, $key2); $this->assertNotEmpty($this->getStoreMetadata($key1)); $this->assertNotEmpty($this->getStoreMetadata($key2)); $this->assertNotNull($this->store->lookup($request1)); $this->assertNotNull($this->store->lookup($request2)); } } ================================================ FILE: Tests/HttpCache/SubRequestHandlerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\HttpCache; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpCache\SubRequestHandler; use Symfony\Component\HttpKernel\HttpKernelInterface; class SubRequestHandlerTest extends TestCase { private static array $globalState; protected function setUp(): void { self::$globalState = $this->getGlobalState(); } protected function tearDown(): void { Request::setTrustedProxies(self::$globalState[0], self::$globalState[1]); } public function testTrustedHeadersAreKept() { Request::setTrustedProxies(['10.0.0.1'], -1); $globalState = $this->getGlobalState(); $request = Request::create('/'); $request->server->set('REMOTE_ADDR', '10.0.0.1'); $request->headers->set('X-Forwarded-For', '10.0.0.2'); $request->headers->set('X-Forwarded-Host', 'Good'); $request->headers->set('X-Forwarded-Port', '1234'); $request->headers->set('X-Forwarded-Proto', 'https'); $request->headers->set('X-Forwarded-Prefix', '/admin'); $kernel = new TestSubRequestHandlerKernel(function ($request, $type, $catch) { $this->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR')); $this->assertSame('10.0.0.2', $request->getClientIp()); $this->assertSame('Good', $request->headers->get('X-Forwarded-Host')); $this->assertSame('1234', $request->headers->get('X-Forwarded-Port')); $this->assertSame('https', $request->headers->get('X-Forwarded-Proto')); $this->assertSame('/admin', $request->headers->get('X-Forwarded-Prefix')); }); SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MAIN_REQUEST, true); $this->assertSame($globalState, $this->getGlobalState()); } public function testUntrustedHeadersAreRemoved() { $request = Request::create('/'); $request->server->set('REMOTE_ADDR', '10.0.0.1'); $request->headers->set('X-Forwarded-For', '10.0.0.2'); $request->headers->set('X-Forwarded-Host', 'Evil'); $request->headers->set('X-Forwarded-Port', '1234'); $request->headers->set('X-Forwarded-Proto', 'http'); $request->headers->set('X-Forwarded-Prefix', '/admin'); $request->headers->set('Forwarded', 'Evil2'); $kernel = new TestSubRequestHandlerKernel(function ($request, $type, $catch) { $this->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR')); $this->assertSame('10.0.0.1', $request->getClientIp()); $this->assertFalse($request->headers->has('X-Forwarded-Host')); $this->assertFalse($request->headers->has('X-Forwarded-Port')); $this->assertFalse($request->headers->has('X-Forwarded-Proto')); $this->assertFalse($request->headers->has('X-Forwarded-Prefix')); $this->assertSame('for="10.0.0.1";host="localhost";proto=http', $request->headers->get('Forwarded')); }); SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MAIN_REQUEST, true); $this->assertSame(self::$globalState, $this->getGlobalState()); } public function testTrustedForwardedHeader() { Request::setTrustedProxies(['10.0.0.1'], -1); $globalState = $this->getGlobalState(); $request = Request::create('/'); $request->server->set('REMOTE_ADDR', '10.0.0.1'); $request->headers->set('Forwarded', 'for="10.0.0.2";host="foo.bar:1234";proto=https'); $kernel = new TestSubRequestHandlerKernel(function ($request, $type, $catch) { $this->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR')); $this->assertSame('10.0.0.2', $request->getClientIp()); $this->assertSame('foo.bar:1234', $request->getHttpHost()); $this->assertSame('https', $request->getScheme()); $this->assertSame(1234, $request->getPort()); }); SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MAIN_REQUEST, true); $this->assertSame($globalState, $this->getGlobalState()); } public function testTrustedXForwardedForHeader() { Request::setTrustedProxies(['10.0.0.1'], -1); $globalState = $this->getGlobalState(); $request = Request::create('/'); $request->server->set('REMOTE_ADDR', '10.0.0.1'); $request->headers->set('X-Forwarded-For', '10.0.0.2'); $request->headers->set('X-Forwarded-Host', 'foo.bar'); $request->headers->set('X-Forwarded-Proto', 'https'); $request->headers->set('X-Forwarded-Prefix', '/admin'); $kernel = new TestSubRequestHandlerKernel(function ($request, $type, $catch) { $this->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR')); $this->assertSame('10.0.0.2', $request->getClientIp()); $this->assertSame('foo.bar', $request->getHttpHost()); $this->assertSame('https', $request->getScheme()); $this->assertSame('/admin', $request->getBaseUrl()); }); SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MAIN_REQUEST, true); $this->assertSame($globalState, $this->getGlobalState()); } private function getGlobalState(): array { return [ Request::getTrustedProxies(), Request::getTrustedHeaderSet(), ]; } } class TestSubRequestHandlerKernel implements HttpKernelInterface { public function __construct( private \Closure $assertCallback, ) { } public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = true): Response { $assertCallback = $this->assertCallback; $assertCallback($request, $type, $catch); return new Response(); } } ================================================ FILE: Tests/HttpCache/TestHttpKernel.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\HttpCache; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; class TestHttpKernel extends HttpKernel implements ControllerResolverInterface, ArgumentResolverInterface { protected ?string $body = null; protected int $status; protected array $headers; protected bool $called = false; protected ?\Closure $customizer; protected bool $catch = false; protected array $backendRequest; public function __construct($body, $status, $headers, ?\Closure $customizer = null, ?EventDispatcher $eventDispatcher = null) { $this->body = $body; $this->status = $status; $this->headers = $headers; $this->customizer = $customizer; parent::__construct($eventDispatcher ?? new EventDispatcher(), $this, null, $this); } public function assert(\Closure $callback) { $trustedConfig = [Request::getTrustedProxies(), Request::getTrustedHeaderSet()]; [$trustedProxies, $trustedHeaderSet, $backendRequest] = $this->backendRequest; Request::setTrustedProxies($trustedProxies, $trustedHeaderSet); try { $callback($backendRequest); } finally { [$trustedProxies, $trustedHeaderSet] = $trustedConfig; Request::setTrustedProxies($trustedProxies, $trustedHeaderSet); } } public function handle(Request $request, $type = HttpKernelInterface::MAIN_REQUEST, $catch = false): Response { $this->catch = $catch; $this->backendRequest = [Request::getTrustedProxies(), Request::getTrustedHeaderSet(), $request]; return parent::handle($request, $type, $catch); } public function isCatchingExceptions() { return $this->catch; } public function getController(Request $request): callable|false { return $this->callController(...); } public function getArguments(Request $request, callable $controller, ?\ReflectionFunctionAbstract $reflector = null): array { return [$request]; } public function callController(Request $request) { $this->called = true; $response = new Response($this->body, $this->status, $this->headers); if (null !== $customizer = $this->customizer) { $customizer($request, $response); } return $response; } public function hasBeenCalled() { return $this->called; } public function reset() { $this->called = false; } } ================================================ FILE: Tests/HttpCache/TestMultipleHttpKernel.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\HttpCache; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; class TestMultipleHttpKernel extends HttpKernel implements ControllerResolverInterface, ArgumentResolverInterface { protected array $bodies = []; protected array $statuses = []; protected array $headers = []; protected bool $called = false; protected Request $backendRequest; public function __construct($responses) { foreach ($responses as $response) { $this->bodies[] = $response['body']; $this->statuses[] = $response['status']; $this->headers[] = $response['headers']; } parent::__construct(new EventDispatcher(), $this, null, $this); } public function getBackendRequest() { return $this->backendRequest; } public function handle(Request $request, $type = HttpKernelInterface::MAIN_REQUEST, $catch = false): Response { $this->backendRequest = $request; return parent::handle($request, $type, $catch); } public function getController(Request $request): callable|false { return $this->callController(...); } public function getArguments(Request $request, callable $controller, ?\ReflectionFunctionAbstract $reflector = null): array { return [$request]; } public function callController(Request $request) { $this->called = true; $response = new Response(array_shift($this->bodies), array_shift($this->statuses), array_shift($this->headers)); return $response; } public function hasBeenCalled() { return $this->called; } public function reset() { $this->called = false; } } ================================================ FILE: Tests/HttpClientKernelTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpClientKernel; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; class HttpClientKernelTest extends TestCase { public function testHandlePassesMaxRedirectsHttpClientOption() { $request = new Request(); $request->attributes->set('http_client_options', ['max_redirects' => 50]); $response = $this->createStub(ResponseInterface::class); $response->method('getStatusCode')->willReturn(200); $client = $this->createMock(HttpClientInterface::class); $client ->expects($this->once()) ->method('request') ->willReturnCallback(function (string $method, string $uri, array $options) use ($request, $response) { $this->assertSame($request->getMethod(), $method); $this->assertSame($request->getUri(), $uri); $this->assertArrayHasKey('max_redirects', $options); $this->assertSame(50, $options['max_redirects']); return $response; }); $kernel = new HttpClientKernel($client); $kernel->handle($request); } } ================================================ FILE: Tests/HttpKernelBrowserTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\HttpKernelBrowser; use Symfony\Component\HttpKernel\Tests\Fixtures\MockableUploadFileWithClientSize; use Symfony\Component\HttpKernel\Tests\Fixtures\TestClient; #[Group('time-sensitive')] class HttpKernelBrowserTest extends TestCase { public function testDoRequest() { $client = new HttpKernelBrowser(new TestHttpKernel()); $client->request('GET', '/'); $this->assertEquals('Request: /', $client->getResponse()->getContent(), '->doRequest() uses the request handler to make the request'); $this->assertInstanceOf(\Symfony\Component\BrowserKit\Request::class, $client->getInternalRequest()); $this->assertInstanceOf(Request::class, $client->getRequest()); $this->assertInstanceOf(\Symfony\Component\BrowserKit\Response::class, $client->getInternalResponse()); $this->assertInstanceOf(Response::class, $client->getResponse()); $client->request('GET', 'http://www.example.com/'); $this->assertEquals('Request: /', $client->getResponse()->getContent(), '->doRequest() uses the request handler to make the request'); $this->assertEquals('www.example.com', $client->getRequest()->getHost(), '->doRequest() uses the request handler to make the request'); $client->request('GET', 'http://www.example.com/?parameter=http://example.com'); $this->assertEquals('http://www.example.com/?parameter='.urlencode('http://example.com'), $client->getRequest()->getUri(), '->doRequest() uses the request handler to make the request'); } public function testGetScript() { $client = new TestClient(new TestHttpKernel()); $client->insulate(); $client->request('GET', '/'); $this->assertEquals('Request: /', $client->getResponse()->getContent(), '->getScript() returns a script that uses the request handler to make the request'); } public function testFilterResponseConvertsCookies() { $client = new HttpKernelBrowser(new TestHttpKernel()); $r = new \ReflectionObject($client); $m = $r->getMethod('filterResponse'); $response = new Response(); $response->headers->setCookie($cookie1 = new Cookie('foo', 'bar', \DateTimeImmutable::createFromFormat('j-M-Y H:i:s T', '15-Feb-2009 20:00:00 GMT')->format('U'), '/foo', 'http://example.com', true, true, false, null)); $domResponse = $m->invoke($client, $response); $this->assertSame((string) $cookie1, $domResponse->getHeader('Set-Cookie')); $response = new Response(); $response->headers->setCookie($cookie1 = new Cookie('foo', 'bar', \DateTimeImmutable::createFromFormat('j-M-Y H:i:s T', '15-Feb-2009 20:00:00 GMT')->format('U'), '/foo', 'http://example.com', true, true, false, null)); $response->headers->setCookie($cookie2 = new Cookie('foo1', 'bar1', \DateTimeImmutable::createFromFormat('j-M-Y H:i:s T', '15-Feb-2009 20:00:00 GMT')->format('U'), '/foo', 'http://example.com', true, true, false, null)); $domResponse = $m->invoke($client, $response); $this->assertSame((string) $cookie1, $domResponse->getHeader('Set-Cookie')); $this->assertSame([(string) $cookie1, (string) $cookie2], $domResponse->getHeader('Set-Cookie', false)); } public function testFilterResponseSupportsStreamedResponses() { $client = new HttpKernelBrowser(new TestHttpKernel()); $r = new \ReflectionObject($client); $m = $r->getMethod('filterResponse'); $response = new StreamedResponse(static function () { echo 'foo'; }); $domResponse = $m->invoke($client, $response); $this->assertEquals('foo', $domResponse->getContent()); } public function testFilterResponseSupportsStreamedResponsesWithChunks() { $client = new HttpKernelBrowser(new TestHttpKernel()); $r = new \ReflectionObject($client); $m = $r->getMethod('filterResponse'); $response = new StreamedResponse(new \ArrayIterator(['foo'])); $domResponse = $m->invoke($client, $response); $this->assertEquals('foo', $domResponse->getContent()); } public function testUploadedFile() { $source = tempnam(sys_get_temp_dir(), 'source'); file_put_contents($source, '1'); $target = sys_get_temp_dir().'/sf.moved.file'; @unlink($target); $kernel = new TestHttpKernel(); $client = new HttpKernelBrowser($kernel); $files = [ ['tmp_name' => $source, 'name' => 'original', 'type' => 'mime/original', 'size' => null, 'error' => \UPLOAD_ERR_OK], new UploadedFile($source, 'original', 'mime/original', \UPLOAD_ERR_OK, true), ]; $file = null; foreach ($files as $file) { $client->request('POST', '/', [], ['foo' => $file]); $files = $client->getRequest()->files->all(); $this->assertCount(1, $files); $file = $files['foo']; $this->assertEquals('original', $file->getClientOriginalName()); $this->assertEquals('mime/original', $file->getClientMimeType()); $this->assertEquals(1, $file->getSize()); } $file->move(\dirname($target), basename($target)); $this->assertFileExists($target); unlink($target); } public function testUploadedFileWhenNoFileSelected() { $kernel = new TestHttpKernel(); $client = new HttpKernelBrowser($kernel); $file = ['tmp_name' => '', 'name' => '', 'type' => '', 'size' => 0, 'error' => \UPLOAD_ERR_NO_FILE]; $client->request('POST', '/', [], ['foo' => $file]); $files = $client->getRequest()->files->all(); $this->assertCount(1, $files); $this->assertNull($files['foo']); } public function testUploadedFileWhenSizeExceedsUploadMaxFileSize() { if (UploadedFile::getMaxFilesize() > \PHP_INT_MAX) { $this->markTestSkipped('Requires PHP_INT_MAX to be greater than "upload_max_filesize" and "post_max_size" ini settings'); } $source = tempnam(sys_get_temp_dir(), 'source'); $kernel = new TestHttpKernel(); $client = new HttpKernelBrowser($kernel); $file = $this ->getMockBuilder(MockableUploadFileWithClientSize::class) ->setConstructorArgs([$source, 'original', 'mime/original', \UPLOAD_ERR_OK, true]) ->onlyMethods(['getSize', 'getClientSize']) ->getMock() ; /* should be modified when the getClientSize will be removed */ $file->expects($this->atLeastOnce()) ->method('getSize') ->willReturn(\PHP_INT_MAX) ; $file ->method('getClientSize') ->willReturn(\PHP_INT_MAX) ; $client->request('POST', '/', [], [$file]); $files = $client->getRequest()->files->all(); $this->assertCount(1, $files); $file = $files[0]; $this->assertFalse($file->isValid()); $this->assertEquals(\UPLOAD_ERR_INI_SIZE, $file->getError()); $this->assertEquals('mime/original', $file->getClientMimeType()); $this->assertEquals('original', $file->getClientOriginalName()); $this->assertEquals(0, $file->getSize()); unlink($source); } public function testAcceptHeaderNotSet() { $client = new HttpKernelBrowser(new TestHttpKernel()); $client->request('GET', '/'); $this->assertFalse($client->getRequest()->headers->has('Accept')); $client->request('GET', '/', [], [], ['HTTP_ACCEPT' => 'application/ld+json']); $this->assertSame('application/ld+json', $client->getRequest()->headers->get('Accept')); } } ================================================ FILE: Tests/HttpKernelTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\ControllerDoesNotReturnResponseException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Bar; class HttpKernelTest extends TestCase { /** * Catch exceptions: true * Throwable type: RuntimeException * Listener: false. */ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrue() { $this->expectException(\RuntimeException::class); $kernel = $this->getHttpKernel(new EventDispatcher(), static fn () => throw new \RuntimeException()); $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); } public function testRequestStackIsNotBrokenWhenControllerThrowsAnExceptionAndCatchIsTrue() { $requestStack = new RequestStack(); $kernel = $this->getHttpKernel(new EventDispatcher(), static fn () => throw new \RuntimeException(), $requestStack); try { $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); } catch (\Throwable $exception) { } self::assertNull($requestStack->getCurrentRequest()); } public function testRequestStackIsNotBrokenWhenControllerThrowsAnExceptionAndCatchIsFalse() { $requestStack = new RequestStack(); $kernel = $this->getHttpKernel(new EventDispatcher(), static fn () => throw new \RuntimeException(), $requestStack); try { $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, false); } catch (\Throwable $exception) { } self::assertNull($requestStack->getCurrentRequest()); } public function testRequestStackIsNotBrokenWhenControllerThrowsAnThrowable() { $requestStack = new RequestStack(); $kernel = $this->getHttpKernel(new EventDispatcher(), static fn () => throw new \Error(), $requestStack); try { $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); } catch (\Throwable $exception) { } self::assertNull($requestStack->getCurrentRequest()); } /** * Catch exceptions: false * Throwable type: RuntimeException * Listener: false. */ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsFalseAndNoListenerIsRegistered() { $this->expectException(\RuntimeException::class); $kernel = $this->getHttpKernel(new EventDispatcher(), static fn () => throw new \RuntimeException()); $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, false); } /** * Catch exceptions: true * Throwable type: RuntimeException * Listener: true. */ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrueWithAHandlingListener() { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::EXCEPTION, static function ($event) { $event->setResponse(new Response($event->getThrowable()->getMessage())); }); $kernel = $this->getHttpKernel($dispatcher, static fn () => throw new \RuntimeException('foo')); $response = $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); $this->assertEquals('500', $response->getStatusCode()); $this->assertEquals('foo', $response->getContent()); } /** * Catch exceptions: true * Throwable type: TypeError * Listener: true. */ public function testHandleWhenControllerThrowsAThrowableAndCatchIsTrueWithAHandlingListener() { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::EXCEPTION, static function ($event) { $event->setResponse(new Response($event->getThrowable()->getMessage())); }); $kernel = $this->getHttpKernel($dispatcher, static fn () => throw new \TypeError('foo'), handleAllThrowables: true); $response = $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); $this->assertEquals('500', $response->getStatusCode()); $this->assertEquals('foo', $response->getContent()); } /** * Catch exceptions: false * Throwable type: TypeError * Listener: true. */ public function testHandleWhenControllerThrowsAThrowableAndCatchIsFalseWithAHandlingListener() { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::EXCEPTION, static function ($event) { $event->setResponse(new Response($event->getThrowable()->getMessage())); }); $kernel = $this->getHttpKernel($dispatcher, static fn () => throw new \TypeError('foo'), handleAllThrowables: true); $this->expectException(\TypeError::class); $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, false); } /** * Catch exceptions: true * Throwable type: TypeError * Listener: true. */ public function testHandleWhenControllerThrowsAThrowableAndCatchIsTrueNotHandlingThrowables() { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::EXCEPTION, static function ($event) { $event->setResponse(new Response($event->getThrowable()->getMessage())); }); $controllerResolver = $this->createStub(ControllerResolverInterface::class); $controllerResolver ->method('getController') ->willReturn(static fn () => throw new \TypeError('foo')); $argumentResolver = $this->createStub(ArgumentResolverInterface::class); $argumentResolver ->method('getArguments') ->willReturn([]); $kernel = new HttpKernel($dispatcher, $controllerResolver, null, $argumentResolver); $this->expectException(\TypeError::class); $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); } /** * Catch exceptions: true * Throwable type: RuntimeException * Listener: true. */ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrueWithANonHandlingListener() { $exception = new \RuntimeException(); $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::EXCEPTION, static function ($event) { // should set a response, but does not }); $kernel = $this->getHttpKernel($dispatcher, static fn () => throw $exception); try { $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); $this->fail('LogicException expected'); } catch (\RuntimeException $e) { $this->assertSame($exception, $e); } } public function testHandleExceptionWithARedirectionResponse() { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::EXCEPTION, static function ($event) { $event->setResponse(new RedirectResponse('/login', 301)); }); $kernel = $this->getHttpKernel($dispatcher, static fn () => throw new AccessDeniedHttpException()); $response = $kernel->handle(new Request()); $this->assertEquals('301', $response->getStatusCode()); $this->assertEquals('/login', $response->headers->get('Location')); } public function testHandleHttpException() { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::EXCEPTION, static function ($event) { $event->setResponse(new Response($event->getThrowable()->getMessage())); }); $kernel = $this->getHttpKernel($dispatcher, static fn () => throw new MethodNotAllowedHttpException(['POST'])); $response = $kernel->handle(new Request()); $this->assertEquals('405', $response->getStatusCode()); $this->assertEquals('POST', $response->headers->get('Allow')); } public function getStatusCodes() { return [ [200, 404], [404, 200], [301, 200], [500, 200], ]; } #[DataProvider('getSpecificStatusCodes')] public function testHandleWhenAnExceptionIsHandledWithASpecificStatusCode($expectedStatusCode) { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::EXCEPTION, static function (ExceptionEvent $event) use ($expectedStatusCode) { $event->allowCustomResponseCode(); $event->setResponse(new Response('', $expectedStatusCode)); }); $kernel = $this->getHttpKernel($dispatcher, static fn () => throw new \RuntimeException()); $response = $kernel->handle(new Request()); $this->assertEquals($expectedStatusCode, $response->getStatusCode()); } public static function getSpecificStatusCodes() { return [ [200], [302], [403], ]; } public function testHandleWhenAListenerReturnsAResponse() { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::REQUEST, static function ($event) { $event->setResponse(new Response('hello')); }); $kernel = $this->getHttpKernel($dispatcher); $this->assertEquals('hello', $kernel->handle(new Request())->getContent()); } public function testHandleWhenNoControllerIsFound() { $this->expectException(NotFoundHttpException::class); $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, false); $kernel->handle(new Request()); } public function testHandleWhenTheControllerIsAClosure() { $response = new Response('foo'); $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, static fn () => $response); $this->assertSame($response, $kernel->handle(new Request())); } public function testHandleWhenTheControllerIsAnObjectWithInvoke() { $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, new TestController()); $this->assertResponseEquals(new Response('foo'), $kernel->handle(new Request())); } public function testHandleWhenTheControllerIsAFunction() { $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, 'Symfony\Component\HttpKernel\Tests\controller_func'); $this->assertResponseEquals(new Response('foo'), $kernel->handle(new Request())); } public function testHandleWhenTheControllerIsAnArray() { $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, [new TestController(), 'controller']); $this->assertResponseEquals(new Response('foo'), $kernel->handle(new Request())); } public function testHandleWhenTheControllerIsAStaticArray() { $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, ['Symfony\Component\HttpKernel\Tests\TestController', 'staticcontroller']); $this->assertResponseEquals(new Response('foo'), $kernel->handle(new Request())); } public function testHandleWhenTheControllerDoesNotReturnAResponse() { $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, static fn () => null); try { $kernel->handle(new Request()); $this->fail('The kernel should throw an exception.'); } catch (ControllerDoesNotReturnResponseException $e) { $first = $e->getTrace()[0]; // `file` index the array starting at 0, and __FILE__ starts at 1 $line = file($first['file'])[$first['line'] - 2]; $this->assertStringContainsString('// call controller', $line); } } public function testHandleWhenTheControllerDoesNotReturnAResponseButAViewIsRegistered() { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::VIEW, static function ($event) { $event->setResponse(new Response($event->getControllerResult())); }); $kernel = $this->getHttpKernel($dispatcher, static fn () => 'foo'); $this->assertEquals('foo', $kernel->handle(new Request())->getContent()); } public function testHandleWithAResponseListener() { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::RESPONSE, static function ($event) { $event->setResponse(new Response('foo')); }); $kernel = $this->getHttpKernel($dispatcher); $this->assertEquals('foo', $kernel->handle(new Request())->getContent()); } public function testHandleAllowChangingControllerArguments() { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS, static function ($event) { $event->setArguments(['foo']); }); $kernel = $this->getHttpKernel($dispatcher, static fn ($content) => new Response($content)); $this->assertResponseEquals(new Response('foo'), $kernel->handle(new Request())); } public function testHandleAllowChangingControllerAndArguments() { $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::CONTROLLER_ARGUMENTS, static function ($event) { $oldController = $event->getController(); $oldArguments = $event->getArguments(); $newController = static function ($id) use ($oldController, $oldArguments) { $response = $oldController(...$oldArguments); $response->headers->set('X-Id', $id); return $response; }; $event->setController($newController); $event->setArguments(['bar']); }); $kernel = $this->getHttpKernel($dispatcher, static fn ($content) => new Response($content), null, ['foo']); $this->assertResponseEquals(new Response('foo', 200, ['X-Id' => 'bar']), $kernel->handle(new Request())); } public function testTerminate() { $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher); $dispatcher->addListener(KernelEvents::TERMINATE, static function ($event) use (&$called, &$capturedKernel, &$capturedRequest, &$capturedResponse) { $called = true; $capturedKernel = $event->getKernel(); $capturedRequest = $event->getRequest(); $capturedResponse = $event->getResponse(); }); $kernel->terminate($request = Request::create('/'), $response = new Response()); $this->assertTrue($called); $this->assertEquals($kernel, $capturedKernel); $this->assertEquals($request, $capturedRequest); $this->assertEquals($response, $capturedResponse); } public function testTerminateWithException() { $dispatcher = new EventDispatcher(); $requestStack = new RequestStack(); $kernel = $this->getHttpKernel($dispatcher, null, $requestStack); $dispatcher->addListener(KernelEvents::EXCEPTION, static function (ExceptionEvent $event) use (&$capturedRequest, $requestStack) { $capturedRequest = $requestStack->getCurrentRequest(); $event->setResponse(new Response()); }); $kernel->terminateWithException(new \Exception('boo'), $request = Request::create('/')); $this->assertSame($request, $capturedRequest); $this->assertNull($requestStack->getCurrentRequest()); } public function testVerifyRequestStackPushPopDuringHandle() { $request = new Request(); $stack = $this->getMockBuilder(RequestStack::class)->onlyMethods(['push', 'pop'])->getMock(); $stack->expects($this->once())->method('push')->with($this->equalTo($request)); $stack->expects($this->once())->method('pop'); $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, null, $stack); $kernel->handle($request, HttpKernelInterface::MAIN_REQUEST); } public function testVerifyRequestStackPushPopWithStreamedResponse() { $request = new Request(); $stack = new RequestStack(); $dispatcher = new EventDispatcher(); $kernel = $this->getHttpKernel($dispatcher, static fn () => new StreamedResponse(static function () use ($stack) { echo $stack->getMainRequest()::class; }), $stack); $response = $kernel->handle($request, HttpKernelInterface::MAIN_REQUEST); self::assertNull($stack->getMainRequest()); ob_start(); $response->send(); self::assertSame(Request::class, ob_get_clean()); self::assertNull($stack->getMainRequest()); } public function testInconsistentClientIpsOnMainRequests() { $this->expectException(BadRequestHttpException::class); $request = new Request(); $request->setTrustedProxies(['1.1.1.1'], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_FORWARDED); $request->server->set('REMOTE_ADDR', '1.1.1.1'); $request->headers->set('FORWARDED', 'for=2.2.2.2'); $request->headers->set('X_FORWARDED_FOR', '3.3.3.3'); $dispatcher = new EventDispatcher(); $dispatcher->addListener(KernelEvents::REQUEST, static function ($event) { $event->getRequest()->getClientIp(); }); $kernel = $this->getHttpKernel($dispatcher); $kernel->handle($request, $kernel::MAIN_REQUEST, false); Request::setTrustedProxies([], -1); } public function testResponseEventCanAccessControllerAttributes() { $dispatcher = new EventDispatcher(); $capturedAttributes = null; $dispatcher->addListener(KernelEvents::CONTROLLER, static function ($event) { $event->setController($event->getController(), [new Bar('test')]); }); $dispatcher->addListener(KernelEvents::RESPONSE, static function ($event) use (&$capturedAttributes) { $capturedAttributes = $event->controllerMetadata->getAttributes('*'); }); $kernel = $this->getHttpKernel($dispatcher); $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, false); $this->assertEquals([new Bar('test')], $capturedAttributes); } public function testViewEventProvidesControllerArgumentsViaMetadata() { $dispatcher = new EventDispatcher(); $capturedArguments = $capturedNamedArguments = null; $dispatcher->addListener(KernelEvents::VIEW, static function ($event) use (&$capturedArguments, &$capturedNamedArguments) { $capturedArguments = $event->controllerMetadata->getArguments(); $capturedNamedArguments = $event->controllerMetadata->getNamedArguments(); $event->setResponse(new Response('ok')); }); $kernel = $this->getHttpKernel($dispatcher, static fn ($value) => $value, arguments: ['resolved']); $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, false); $this->assertSame(['resolved'], $capturedArguments); $this->assertSame(['value' => 'resolved'], $capturedNamedArguments); } public function testExceptionEventProvidesControllerMetadata() { $dispatcher = new EventDispatcher(); $capturedController = $capturedArguments = null; $controller = static fn (string $value) => throw new \RuntimeException('boom'); $dispatcher->addListener(KernelEvents::EXCEPTION, static function ($event) use (&$capturedController, &$capturedArguments) { $capturedController = $event->controllerMetadata->getController(); $capturedArguments = $event->controllerMetadata->getArguments(); $event->setResponse(new Response('handled')); }); $kernel = $this->getHttpKernel($dispatcher, $controller, arguments: ['meta']); $response = $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, true); $this->assertSame('handled', $response->getContent()); $this->assertSame($controller, $capturedController); $this->assertSame(['meta'], $capturedArguments); } public function testFinishRequestEventKeepsControllerMetadata() { $dispatcher = new EventDispatcher(); $capturedArguments = null; $dispatcher->addListener(KernelEvents::FINISH_REQUEST, static function ($event) use (&$capturedArguments) { $capturedArguments = $event->controllerMetadata->getArguments(); }); $kernel = $this->getHttpKernel($dispatcher, static fn ($value) => new Response($value), arguments: ['done']); $response = $kernel->handle(new Request(), HttpKernelInterface::MAIN_REQUEST, false); $this->assertSame('done', $response->getContent()); $this->assertSame(['done'], $capturedArguments); } private function getHttpKernel(EventDispatcherInterface $eventDispatcher, $controller = null, ?RequestStack $requestStack = null, array $arguments = [], bool $handleAllThrowables = false) { $controller ??= static fn () => new Response('Hello'); $controllerResolver = $this->createStub(ControllerResolverInterface::class); $controllerResolver ->method('getController') ->willReturn($controller); $argumentResolver = $this->createStub(ArgumentResolverInterface::class); $argumentResolver ->method('getArguments') ->willReturn($arguments); return new HttpKernel($eventDispatcher, $controllerResolver, $requestStack, $argumentResolver, $handleAllThrowables); } private function assertResponseEquals(Response $expected, Response $actual) { $expected->setDate($actual->getDate()); $this->assertEquals($expected, $actual); } } class TestController { public function __invoke() { return new Response('foo'); } public function controller() { return new Response('foo'); } public static function staticController() { return new Response('foo'); } } function controller_func() { return new Response('foo'); } ================================================ FILE: Tests/KernelTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass; use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\Tests\Fixtures\KernelWithoutBundles; use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService; class KernelTest extends TestCase { protected function tearDown(): void { try { (new Filesystem())->remove(__DIR__.'/Fixtures/var'); } catch (IOException $e) { } } public function testConstructor() { $env = 'test_env'; $debug = true; $kernel = new KernelForTest($env, $debug); $this->assertEquals($env, $kernel->getEnvironment()); $this->assertEquals($debug, $kernel->isDebug()); $this->assertFalse($kernel->isBooted()); $this->assertLessThanOrEqual(microtime(true), $kernel->getStartTime()); } public function testEmptyEnv() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage(\sprintf('Invalid environment provided to "%s": the environment cannot be empty.', KernelForTest::class)); new KernelForTest('', false); } public function testClone() { $env = 'test_env'; $debug = true; $kernel = new KernelForTest($env, $debug); $clone = clone $kernel; $this->assertEquals($env, $clone->getEnvironment()); $this->assertEquals($debug, $clone->isDebug()); $this->assertFalse($clone->isBooted()); $this->assertLessThanOrEqual(microtime(true), $clone->getStartTime()); } public function testClassNameValidityGetter() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('The environment "test.env" contains invalid characters, it can only contain characters allowed in PHP class names.'); // We check the classname that will be generated by using a $env that // contains invalid characters. $env = 'test.env'; $kernel = new KernelForTest($env, false, false); $kernel->boot(); } public function testInitializeContainerClearsOldContainers() { $fs = new Filesystem(); $legacyContainerDir = __DIR__.'/Fixtures/var/cache/custom/ContainerA123456'; $fs->mkdir($legacyContainerDir); touch($legacyContainerDir.'.legacy'); $kernel = new CustomProjectDirKernel(); $kernel->boot(); $containerDir = __DIR__.'/Fixtures/var/cache/custom/'.substr($kernel->getContainer()::class, 0, 16); $this->assertTrue(unlink(__DIR__.'/Fixtures/var/cache/custom/Symfony_Component_HttpKernel_Tests_CustomProjectDirKernelCustomDebugContainer.php.meta')); $this->assertFileExists($containerDir); $this->assertFileDoesNotExist($containerDir.'.legacy'); $kernel = new CustomProjectDirKernel(static function ($container) { $container->register('foo', 'stdClass')->setPublic(true); }); $kernel->boot(); $this->assertFileExists($containerDir); $this->assertFileExists($containerDir.'.legacy'); $this->assertFileDoesNotExist($legacyContainerDir); $this->assertFileDoesNotExist($legacyContainerDir.'.legacy'); } public function testBootInitializesBundlesAndContainer() { $kernel = $this->getKernel(['initializeBundles']); $kernel->expects($this->once()) ->method('initializeBundles'); $kernel->boot(); } public function testBootSetsTheContainerToTheBundles() { $bundle = $this->createMock(Bundle::class); $bundle->expects($this->once()) ->method('setContainer'); $kernel = $this->getKernel(['initializeBundles', 'getBundles']); $kernel->expects($this->once()) ->method('getBundles') ->willReturn([$bundle]); $kernel->boot(); } public function testBootSetsTheBootedFlagToTrue() { // use test kernel to access isBooted() $kernel = new KernelForTest('test', false); $kernel->boot(); $this->assertTrue($kernel->isBooted()); } public function testClassCacheIsNotLoadedByDefault() { $kernel = $this->getKernel(['initializeBundles', 'doLoadClassCache'], [], false, KernelForTestWithLoadClassCache::class); $kernel->expects($this->never()) ->method('doLoadClassCache'); $kernel->boot(); } public function testBootKernelSeveralTimesOnlyInitializesBundlesOnce() { $kernel = $this->getKernel(['initializeBundles']); $kernel->expects($this->once()) ->method('initializeBundles'); $kernel->boot(); $kernel->boot(); } public function testShutdownCallsShutdownOnAllBundles() { $bundle = $this->createMock(Bundle::class); $bundle->expects($this->once()) ->method('shutdown'); $kernel = new KernelForTest('test', false, true, [$bundle]); $kernel->boot(); $kernel->shutdown(); } public function testShutdownGivesNullContainerToAllBundles() { $bundle = $this->createMock(Bundle::class); $bundle->expects($this->exactly(2)) ->method('setContainer') ->willReturnCallback(function ($container) { if (null !== $container) { $this->assertInstanceOf(ContainerInterface::class, $container); } }) ; $kernel = new KernelForTest('test', false, true, [$bundle]); $kernel->boot(); $kernel->shutdown(); } public function testHandleCallsHandleOnHttpKernel() { $type = HttpKernelInterface::MAIN_REQUEST; $catch = true; $request = new Request(); $httpKernelMock = $this->getMockBuilder(HttpKernel::class) ->disableOriginalConstructor() ->getMock(); $httpKernelMock ->expects($this->once()) ->method('handle') ->with($request, $type, $catch); $kernel = $this->getKernel(['getHttpKernel']); $kernel->expects($this->once()) ->method('getHttpKernel') ->willReturn($httpKernelMock); $kernel->handle($request, $type, $catch); } public function testHandleBootsTheKernel() { $type = HttpKernelInterface::MAIN_REQUEST; $catch = true; $request = new Request(); $httpKernelMock = $this->createStub(HttpKernelInterface::class); $kernel = $this->getKernel(['getHttpKernel', 'boot']); $kernel->expects($this->once()) ->method('getHttpKernel') ->willReturn($httpKernelMock); $kernel->expects($this->once()) ->method('boot'); $kernel->handle($request, $type, $catch); } public function testSerialize() { $env = 'test_env'; $debug = true; $kernel = new KernelForTest($env, $debug); $expected = \sprintf('O:48:"%s":2:{s:11:"environment";s:8:"test_env";s:5:"debug";b:1;}', KernelForTest::class); $this->assertEquals($expected, serialize($kernel)); } public function testLocateResourceThrowsExceptionWhenNameIsNotValid() { $this->expectException(\InvalidArgumentException::class); (new KernelForTest('test', false))->locateResource('Foo'); } public function testLocateResourceThrowsExceptionWhenNameIsUnsafe() { $this->expectException(\RuntimeException::class); (new KernelForTest('test', false))->locateResource('@FooBundle/../bar'); } public function testLocateResourceThrowsExceptionWhenBundleDoesNotExist() { $this->expectException(\InvalidArgumentException::class); (new KernelForTest('test', false))->locateResource('@FooBundle/config/routing.xml'); } public function testLocateResourceThrowsExceptionWhenResourceDoesNotExist() { $bundle = $this->createStub(BundleInterface::class); $bundle->method('getPath')->willReturn(__DIR__.'/Fixtures/Bundle1Bundle'); $this->expectException(\InvalidArgumentException::class); $kernel = $this->getKernel(['getBundle']); $kernel ->expects($this->once()) ->method('getBundle') ->willReturn($bundle) ; $kernel->locateResource('@Bundle1Bundle/config/routing.xml'); } public function testLocateResourceReturnsTheFirstThatMatches() { $bundle = $this->createStub(BundleInterface::class); $bundle->method('getPath')->willReturn(__DIR__.'/Fixtures/Bundle1Bundle'); $kernel = $this->getKernel(['getBundle']); $kernel ->expects($this->once()) ->method('getBundle') ->willReturn($bundle) ; $this->assertEquals(__DIR__.'/Fixtures/Bundle1Bundle/foo.txt', $kernel->locateResource('@Bundle1Bundle/foo.txt')); } public function testLocateResourceOnDirectories() { $bundle = $this->createStub(BundleInterface::class); $bundle->method('getName')->willReturn('Bundle1Bundle'); $bundle->method('getPath')->willReturn(__DIR__.'/Fixtures/Bundle1Bundle'); $kernel = $this->getKernel(['getBundle']); $kernel ->expects($this->exactly(2)) ->method('getBundle') ->willReturn($bundle) ; $this->assertEquals( __DIR__.'/Fixtures/Bundle1Bundle/Resources/', $kernel->locateResource('@Bundle1Bundle/Resources/') ); $this->assertEquals( __DIR__.'/Fixtures/Bundle1Bundle/Resources', $kernel->locateResource('@Bundle1Bundle/Resources') ); } public function testInitializeBundleThrowsExceptionWhenRegisteringTwoBundlesWithTheSameName() { $this->expectException(\LogicException::class); $this->expectExceptionMessage('Trying to register two bundles with the same name "DuplicateName"'); $fooBundle = $this->createStub(BundleInterface::class); $fooBundle->method('getName')->willReturn('DuplicateName'); $barBundle = $this->createStub(BundleInterface::class); $barBundle->method('getName')->willReturn('DuplicateName'); $kernel = new KernelForTest('test', false, true, [$fooBundle, $barBundle]); $kernel->boot(); } public function testTerminateReturnsSilentlyIfKernelIsNotBooted() { $kernel = $this->getKernel(['getHttpKernel']); $kernel->expects($this->never()) ->method('getHttpKernel'); $kernel->terminate(Request::create('/'), new Response()); } public function testTerminateDelegatesTerminationOnlyForTerminableInterface() { // does not implement TerminableInterface $httpKernel = new TestKernel(); $kernel = $this->getKernel(['getHttpKernel']); $kernel->expects($this->once()) ->method('getHttpKernel') ->willReturn($httpKernel); $kernel->boot(); $kernel->terminate(Request::create('/'), new Response()); $this->assertFalse($httpKernel->terminateCalled, 'terminate() is never called if the kernel class does not implement TerminableInterface'); // implements TerminableInterface $httpKernelMock = $this->getMockBuilder(HttpKernel::class) ->disableOriginalConstructor() ->onlyMethods(['terminate']) ->getMock(); $httpKernelMock ->expects($this->once()) ->method('terminate'); $kernel = $this->getKernel(['getHttpKernel']); $kernel->expects($this->exactly(2)) ->method('getHttpKernel') ->willReturn($httpKernelMock); $kernel->boot(); $kernel->terminate(Request::create('/'), new Response()); } public function testKernelWithoutBundles() { $kernel = new KernelWithoutBundles('test', true); $kernel->boot(); $this->assertTrue($kernel->getContainer()->getParameter('test_executed')); } public function testProjectDirExtension() { $kernel = new CustomProjectDirKernel(); $kernel->boot(); $this->assertSame(__DIR__.'/Fixtures', $kernel->getProjectDir()); $this->assertSame(__DIR__.\DIRECTORY_SEPARATOR.'Fixtures', $kernel->getContainer()->getParameter('kernel.project_dir')); } public function testKernelReset() { $this->tearDown(); $kernel = new CustomProjectDirKernel(); $kernel->boot(); $containerClass = $kernel->getContainer()::class; $containerFile = (new \ReflectionClass($kernel->getContainer()))->getFileName(); unlink(__DIR__.'/Fixtures/var/cache/custom/Symfony_Component_HttpKernel_Tests_CustomProjectDirKernelCustomDebugContainer.php.meta'); $kernel = new CustomProjectDirKernel(); $kernel->boot(); $this->assertInstanceOf($containerClass, $kernel->getContainer()); $this->assertFileExists($containerFile); unlink(__DIR__.'/Fixtures/var/cache/custom/Symfony_Component_HttpKernel_Tests_CustomProjectDirKernelCustomDebugContainer.php.meta'); $kernel = new CustomProjectDirKernel(static function ($container) { $container->register('foo', 'stdClass')->setPublic(true); }); $kernel->boot(); $this->assertNotInstanceOf($containerClass, $kernel->getContainer()); $this->assertFileExists($containerFile); $this->assertFileExists(\dirname($containerFile).'.legacy'); } public function testKernelExtension() { $kernel = new class extends CustomProjectDirKernel implements ExtensionInterface { public function load(array $configs, ContainerBuilder $container): void { $container->setParameter('test.extension-registered', true); } /** * To be removed when symfony/dependency-injection is bumped to 8.0+. */ public function getNamespace(): string { return ''; } /** * To be removed when symfony/dependency-injection is bumped to 8.0+. */ public function getXsdValidationBasePath(): string|false { return false; } public function getAlias(): string { return 'test-extension'; } }; $kernel->boot(); $this->assertTrue($kernel->getContainer()->getParameter('test.extension-registered')); } public function testKernelPass() { $kernel = new PassKernel(); $kernel->boot(); $this->assertTrue($kernel->getContainer()->getParameter('test.processed')); } public function testWarmup() { $kernel = new CustomProjectDirKernel(); $kernel->boot(); $this->assertTrue($kernel->warmedUp); $this->assertSame(realpath($kernel->getBuildDir()), $kernel->warmedUpBuildDir); } public function testServicesResetter() { $httpKernelMock = $this->getMockBuilder(HttpKernelInterface::class) ->disableOriginalConstructor() ->getMock(); $httpKernelMock ->expects($this->exactly(2)) ->method('handle'); $kernel = new CustomProjectDirKernel(static function ($container) { $container->addCompilerPass(new ResettableServicePass()); $container->register('one', ResettableService::class) ->setPublic(true) ->addTag('kernel.reset', ['method' => 'reset']); $container->register('services_resetter', ServicesResetter::class)->setPublic(true); }, $httpKernelMock, 'resetting'); ResettableService::$counter = 0; $request = new Request(); $kernel->handle($request); $kernel->getContainer()->get('one'); $this->assertEquals(0, ResettableService::$counter); $this->assertFalse($kernel->getContainer()->initialized('services_resetter')); $kernel->handle($request); $this->assertEquals(1, ResettableService::$counter); } public function testServicesAreNotResetBetweenHttpCacheFragments() { ResettableService::$counter = 0; $fragmentKernel = new FragmentHandlingKernel(); $kernel = new CustomProjectDirKernel(static function (ContainerBuilder $container) { $container->addCompilerPass(new ResettableServicePass()); $container->register('kernel', CustomProjectDirKernel::class) ->setSynthetic(true) ->setPublic(true); $container->register('one', ResettableService::class) ->setPublic(true) ->addTag('kernel.reset', ['method' => 'reset']); $container->register('services_resetter', ServicesResetter::class)->setPublic(true); $container->register('http_cache', FragmentRenderingHttpCache::class) ->setPublic(true) ->addArgument(new Reference('kernel')); }, $fragmentKernel, 'http_cache_fragments'); $kernel->handle(new Request()); $this->assertSame([ ['/first-fragment', HttpKernelInterface::MAIN_REQUEST], ['/second-fragment', HttpKernelInterface::MAIN_REQUEST], ], $fragmentKernel->handledPaths); $this->assertSame([0, 0], $fragmentKernel->resetCounters); $this->assertSame(0, ResettableService::$counter); $kernel->boot(); $this->assertSame(1, ResettableService::$counter); } public function testHttpCacheHandlesRequestsAfterKernelBoot() { $kernel = new CustomProjectDirKernel(static function (ContainerBuilder $container) { $container->register('http_cache', RecordingHttpCache::class) ->setPublic(true); }, new ThrowingHttpKernel(), 'http_cache_worker'); $kernel->boot(); $firstResponse = $kernel->handle(Request::create('/worker-first')); $secondResponse = $kernel->handle(Request::create('/worker-second')); /** @var RecordingHttpCache $httpCache */ $httpCache = $kernel->getContainer()->get('http_cache'); $this->assertSame([ ['/worker-first', HttpKernelInterface::MAIN_REQUEST], ['/worker-second', HttpKernelInterface::MAIN_REQUEST], ], $httpCache->handledPaths); $this->assertSame('cached: /worker-first', $firstResponse->getContent()); $this->assertSame('cached: /worker-second', $secondResponse->getContent()); } #[Group('time-sensitive')] public function testKernelStartTimeIsResetWhileBootingAlreadyBootedKernel() { $kernel = new KernelForTest('test', true); $kernel->boot(); $preReBoot = $kernel->getStartTime(); sleep(3600); // Intentionally large value to detect if ClockMock ever breaks $kernel->reboot(null); $this->assertGreaterThan($preReBoot, $kernel->getStartTime()); } public function testAnonymousKernelGeneratesValidContainerClass() { $kernel = new class('test', true) extends Kernel { public function registerBundles(): iterable { return []; } public function registerContainerConfiguration(LoaderInterface $loader): void { } public function getContainerClass(): string { return parent::getContainerClass(); } }; $this->assertMatchesRegularExpression('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*TestDebugContainer$/', $kernel->getContainerClass()); } public function testTrustedParameters() { $kernel = new CustomProjectDirKernel(static function (ContainerBuilder $container) { $container->setParameter('kernel.trusted_hosts', '^a{2,3}.com$, ^b{2,}.com$'); $container->setParameter('kernel.trusted_proxies', 'a,b'); $container->setParameter('kernel.trusted_headers', 'x-forwarded-for'); }); $kernel->boot(); try { $this->assertSame(['{^a{2,3}.com$}i', '{^b{2,}.com$}i'], Request::getTrustedHosts()); $this->assertSame(['a', 'b'], Request::getTrustedProxies()); $this->assertSame(Request::HEADER_X_FORWARDED_FOR, Request::getTrustedHeaderSet()); } finally { Request::setTrustedHosts([]); Request::setTrustedProxies([], 0); } } public function testSourceDateEpoch() { $sourceDateEpoch = 1609459200; // 2021-01-01 00:00:00 UTC $_SERVER['SOURCE_DATE_EPOCH'] = $sourceDateEpoch; $kernel = new class('test', true) extends Kernel { public function registerBundles(): iterable { return []; } public function registerContainerConfiguration(LoaderInterface $loader): void { } public function getProjectDir(): string { return __DIR__.'/Fixtures'; } }; $kernel->boot(); $container = $kernel->getContainer(); $this->assertSame($sourceDateEpoch, $container->getParameter('container.build_time')); } public function testSourceDateEpochWithKernelContainerBuildTime() { $sourceDateEpoch = 1609459200; // 2021-01-01 00:00:00 UTC $kernelBuildTime = 1609545600; // 2021-01-02 00:00:00 UTC $_SERVER['SOURCE_DATE_EPOCH'] = $sourceDateEpoch; $kernel = new CustomProjectDirKernel(static function (ContainerBuilder $container) use ($kernelBuildTime) { $container->setParameter('kernel.container_build_time', $kernelBuildTime); }); $kernel->boot(); $container = $kernel->getContainer(); // kernel.container_build_time should take precedence over SOURCE_DATE_EPOCH $this->assertSame($kernelBuildTime, $container->getParameter('container.build_time')); } /** * Returns a mock for the abstract kernel. * * @param array $methods Additional methods to mock (besides the abstract ones) * @param array $bundles Bundles to register */ protected function getKernel(array $methods = [], array $bundles = [], bool $debug = false, string $kernelClass = KernelForTest::class): Kernel { $methods[] = 'registerBundles'; $kernelMockBuilder = $this ->getMockBuilder($kernelClass) ->onlyMethods($methods) ->setConstructorArgs(['test', $debug]) ; $kernel = $kernelMockBuilder->getMock(); $kernel ->method('registerBundles') ->willReturn($bundles) ; return $kernel; } } class TestKernel implements HttpKernelInterface { public bool $terminateCalled = false; public function terminate(): void { $this->terminateCalled = true; } public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response { } public function getProjectDir(): string { return __DIR__.'/Fixtures'; } } class CustomProjectDirKernel extends Kernel implements WarmableInterface { public bool $warmedUp = false; public ?string $warmedUpBuildDir = null; public function __construct( private readonly ?\Closure $buildContainer = null, private readonly ?HttpKernelInterface $httpKernel = null, $env = 'custom', ) { parent::__construct($env, true); } public function registerBundles(): iterable { return []; } public function registerContainerConfiguration(LoaderInterface $loader): void { } public function getProjectDir(): string { return __DIR__.'/Fixtures'; } public function warmUp(string $cacheDir, ?string $buildDir = null): array { $this->warmedUp = true; $this->warmedUpBuildDir = $buildDir; return []; } protected function build(ContainerBuilder $container): void { if ($build = $this->buildContainer) { $build($container); } } protected function getHttpKernel(): HttpKernelInterface { return $this->httpKernel; } } class PassKernel extends CustomProjectDirKernel implements CompilerPassInterface { public function __construct() { parent::__construct(); Kernel::__construct('pass', true); } public function process(ContainerBuilder $container): void { $container->setParameter('test.processed', true); } } class FragmentHandlingKernel implements HttpKernelInterface { public array $handledPaths = []; public array $resetCounters = []; public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response { $this->handledPaths[] = [$request->getPathInfo(), $type]; $this->resetCounters[] = ResettableService::$counter; return new Response($request->getPathInfo()); } } class FragmentRenderingHttpCache implements HttpKernelInterface { public function __construct( private KernelInterface $kernel, private string $trackedServiceId = 'one', ) { } public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response { $this->kernel->boot(); $this->kernel->getContainer()->get($this->trackedServiceId); $responses = []; foreach (['/first-fragment', '/second-fragment'] as $path) { $responses[] = $this->kernel->handle(Request::create($path), self::MAIN_REQUEST, $catch); } return new Response(implode('', array_map(static fn (Response $response) => $response->getContent(), $responses))); } } class RecordingHttpCache implements HttpKernelInterface { public array $handledPaths = []; public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response { $this->handledPaths[] = [$request->getPathInfo(), $type]; return new Response('cached: '.$request->getPathInfo()); } } class ThrowingHttpKernel implements HttpKernelInterface { public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response { throw new \LogicException('The worker HTTP kernel should not be reached when the http_cache service handles the request.'); } } class KernelForTest extends Kernel { public function __construct( string $environment, bool $debug, private readonly bool $fakeContainer = true, private array $registeredBundles = [], ) { parent::__construct($environment, $debug); } public function getBundleMap(): array { return []; } public function registerBundles(): iterable { return $this->registeredBundles; } public function registerContainerConfiguration(LoaderInterface $loader): void { } public function isBooted(): bool { return $this->booted; } public function getProjectDir(): string { return __DIR__; } protected function initializeContainer(): void { if ($this->fakeContainer) { $this->container = new ContainerBuilder(); } else { parent::initializeContainer(); } } } class KernelForTestWithLoadClassCache extends KernelForTest { public function doLoadClassCache(): void { } } ================================================ FILE: Tests/Log/LoggerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Log; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Psr\Log\InvalidArgumentException; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\HttpKernel\Log\Logger; /** * @author Kévin Dunglas * @author Jordi Boggiano */ class LoggerTest extends TestCase { private Logger $logger; private string $tmpFile; protected function setUp(): void { $this->tmpFile = tempnam(sys_get_temp_dir(), 'log'); $this->logger = new Logger(LogLevel::DEBUG, $this->tmpFile); } protected function tearDown(): void { if (!@unlink($this->tmpFile)) { file_put_contents($this->tmpFile, ''); } } public static function assertLogsMatch(array $expected, array $given) { foreach ($given as $k => $line) { self::assertSame(1, preg_match('/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[\+-][0-9]{2}:[0-9]{2} '.preg_quote($expected[$k]).'/', $line), "\"$line\" do not match expected pattern \"$expected[$k]\""); } } /** * Return the log messages in order. * * @return string[] */ public function getLogs(): array { return file($this->tmpFile, \FILE_IGNORE_NEW_LINES); } public function testImplements() { $this->assertInstanceOf(LoggerInterface::class, $this->logger); } #[DataProvider('provideLevelsAndMessages')] public function testLogsAtAllLevels($level, $message) { $this->logger->{$level}($message, ['user' => 'Bob']); $this->logger->log($level, $message, ['user' => 'Bob']); $expected = [ "[$level] message of level $level with context: Bob", "[$level] message of level $level with context: Bob", ]; $this->assertLogsMatch($expected, $this->getLogs()); } public static function provideLevelsAndMessages() { return [ LogLevel::EMERGENCY => [LogLevel::EMERGENCY, 'message of level emergency with context: {user}'], LogLevel::ALERT => [LogLevel::ALERT, 'message of level alert with context: {user}'], LogLevel::CRITICAL => [LogLevel::CRITICAL, 'message of level critical with context: {user}'], LogLevel::ERROR => [LogLevel::ERROR, 'message of level error with context: {user}'], LogLevel::WARNING => [LogLevel::WARNING, 'message of level warning with context: {user}'], LogLevel::NOTICE => [LogLevel::NOTICE, 'message of level notice with context: {user}'], LogLevel::INFO => [LogLevel::INFO, 'message of level info with context: {user}'], LogLevel::DEBUG => [LogLevel::DEBUG, 'message of level debug with context: {user}'], ]; } public function testLogLevelDisabled() { $this->logger = new Logger(LogLevel::INFO, $this->tmpFile); $this->logger->debug('test', ['user' => 'Bob']); $this->logger->log(LogLevel::DEBUG, 'test', ['user' => 'Bob']); // Will always be true, but asserts than an exception isn't thrown $this->assertSame([], $this->getLogs()); } public function testThrowsOnInvalidLevel() { $this->expectException(InvalidArgumentException::class); $this->logger->log('invalid level', 'Foo'); } public function testThrowsOnInvalidMinLevel() { $this->expectException(InvalidArgumentException::class); new Logger('invalid'); } public function testInvalidOutput() { $this->expectException(InvalidArgumentException::class); new Logger(LogLevel::DEBUG, '/'); } public function testContextReplacement() { $logger = $this->logger; $logger->info('{Message {nothing} {user} {foo.bar} a}', ['user' => 'Bob', 'foo.bar' => 'Bar']); $expected = ['[info] {Message {nothing} Bob Bar a}']; $this->assertLogsMatch($expected, $this->getLogs()); } public function testObjectCastToString() { $dummy = $this->createPartialMock(DummyTest::class, ['__toString']); $dummy->expects($this->atLeastOnce()) ->method('__toString') ->willReturn('DUMMY'); $this->logger->warning($dummy); $expected = ['[warning] DUMMY']; $this->assertLogsMatch($expected, $this->getLogs()); } public function testContextCanContainAnything() { $context = [ 'bool' => true, 'null' => null, 'string' => 'Foo', 'int' => 0, 'float' => 0.5, 'nested' => ['with object' => new DummyTest()], 'object' => new \DateTimeImmutable(), 'resource' => fopen('php://memory', 'r'), ]; $this->logger->warning('Crazy context data', $context); $expected = ['[warning] Crazy context data']; $this->assertLogsMatch($expected, $this->getLogs()); } public function testContextExceptionKeyCanBeExceptionOrOtherValues() { $logger = $this->logger; $logger->warning('Random message', ['exception' => 'oops']); $logger->critical('Uncaught Exception!', ['exception' => new \LogicException('Fail')]); $expected = [ '[warning] Random message', '[critical] Uncaught Exception!', ]; $this->assertLogsMatch($expected, $this->getLogs()); } public function testFormatter() { $this->logger = new Logger(LogLevel::DEBUG, $this->tmpFile, static fn ($level, $message, $context) => json_encode(['level' => $level, 'message' => $message, 'context' => $context])); $this->logger->error('An error', ['foo' => 'bar']); $this->logger->warning('A warning', ['baz' => 'bar']); $this->assertSame([ '{"level":"error","message":"An error","context":{"foo":"bar"}}', '{"level":"warning","message":"A warning","context":{"baz":"bar"}}', ], $this->getLogs()); } public function testLogsWithoutOutput() { $oldErrorLog = ini_set('error_log', $this->tmpFile); $logger = new Logger(); $logger->error('test'); $logger->critical('test'); $expected = [ '[error] test', '[critical] test', ]; foreach ($this->getLogs() as $k => $line) { $this->assertSame(1, preg_match('/\[[\w\/\-: ]+\] '.preg_quote($expected[$k]).'/', $line), "\"$line\" do not match expected pattern \"$expected[$k]\""); } ini_set('error_log', $oldErrorLog); } } class DummyTest { public function __toString(): string { } } ================================================ FILE: Tests/Logger.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests; use Psr\Log\AbstractLogger; class Logger extends AbstractLogger { protected array $logs; public function __construct() { $this->clear(); } public function getLogsForLevel(string $level): array { return $this->logs[$level]; } public function clear(): void { $this->logs = [ 'emergency' => [], 'alert' => [], 'critical' => [], 'error' => [], 'warning' => [], 'notice' => [], 'info' => [], 'debug' => [], ]; } public function log($level, $message, array $context = []): void { $this->logs[$level][] = $message; } } ================================================ FILE: Tests/Profiler/FileProfilerStorageTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Profiler; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Profiler\FileProfilerStorage; use Symfony\Component\HttpKernel\Profiler\Profile; class FileProfilerStorageTest extends TestCase { private string $tmpDir; private FileProfilerStorage $storage; protected function setUp(): void { $this->tmpDir = sys_get_temp_dir().'/sf_profiler_file_storage'; if (is_dir($this->tmpDir)) { self::cleanDir(); } $this->storage = new FileProfilerStorage('file:'.$this->tmpDir); $this->storage->purge(); } protected function tearDown(): void { self::cleanDir(); } public function testStore() { for ($i = 0; $i < 10; ++$i) { $profile = new Profile('token_'.$i); $profile->setIp('127.0.0.1'); $profile->setUrl('http://foo.bar'); $profile->setMethod('GET'); $this->storage->write($profile); } $this->assertCount(10, $this->storage->find('127.0.0.1', 'http://foo.bar', 20, 'GET'), '->write() stores data in the storage'); } public function testChildren() { $parentProfile = new Profile('token_parent'); $parentProfile->setIp('127.0.0.1'); $parentProfile->setUrl('http://foo.bar/parent'); $parentProfile->setStatusCode(200); $parentProfile->setMethod('GET'); $childProfile = new Profile('token_child'); $childProfile->setIp('127.0.0.1'); $childProfile->setUrl('http://foo.bar/child'); $childProfile->setStatusCode(200); $childProfile->setMethod('GET'); $parentProfile->addChild($childProfile); $this->storage->write($parentProfile); $this->storage->write($childProfile); // Load them from storage $parentProfile = $this->storage->read('token_parent'); $childProfile = $this->storage->read('token_child'); // Check child has link to parent $this->assertNotNull($childProfile->getParent()); $this->assertEquals($parentProfile->getToken(), $childProfile->getParentToken()); // Check parent has child $children = $parentProfile->getChildren(); $this->assertCount(1, $children); $this->assertEquals($childProfile->getToken(), $children[0]->getToken()); } public function testStoreSpecialCharsInUrl() { // The storage accepts special characters in URLs (Even though URLs are not // supposed to contain them) $profile = new Profile('simple_quote'); $profile->setUrl('http://foo.bar/\''); $profile->setIp('127.0.0.1'); $profile->setStatusCode(200); $profile->setMethod('GET'); $this->storage->write($profile); $this->assertNotFalse($this->storage->read('simple_quote'), '->write() accepts single quotes in URL'); $profile = new Profile('double_quote'); $profile->setUrl('http://foo.bar/"'); $profile->setIp('127.0.0.1'); $profile->setStatusCode(200); $profile->setMethod('GET'); $this->storage->write($profile); $this->assertNotFalse($this->storage->read('double_quote'), '->write() accepts double quotes in URL'); $profile = new Profile('backslash'); $profile->setUrl('http://foo.bar/\\'); $profile->setIp('127.0.0.1'); $profile->setStatusCode(200); $profile->setMethod('GET'); $this->storage->write($profile); $this->assertNotFalse($this->storage->read('backslash'), '->write() accepts backslash in URL'); $profile = new Profile('comma'); $profile->setUrl('http://foo.bar/,'); $profile->setIp('127.0.0.1'); $profile->setStatusCode(200); $profile->setMethod('GET'); $this->storage->write($profile); $this->assertNotFalse($this->storage->read('comma'), '->write() accepts comma in URL'); } public function testStoreDuplicateToken() { $profile = new Profile('token'); $profile->setUrl('http://example.com/'); $profile->setIp('127.0.0.1'); $profile->setStatusCode(200); $profile->setMethod('GET'); $this->assertTrue($this->storage->write($profile), '->write() returns true when the token is unique'); $profile->setUrl('http://example.net/'); $this->assertTrue($this->storage->write($profile), '->write() returns true when the token is already present in the storage'); $this->assertEquals('http://example.net/', $this->storage->read('token')->getUrl(), '->write() overwrites the current profile data'); $this->assertCount(1, $this->storage->find('', '', 1000, ''), '->find() does not return the same profile twice'); } public function testRetrieveByIp() { $profile = new Profile('token'); $profile->setIp('127.0.0.1'); $profile->setMethod('GET'); $this->storage->write($profile); $this->assertCount(1, $this->storage->find('127.0.0.1', '', 10, 'GET'), '->find() retrieve a record by IP'); $this->assertCount(0, $this->storage->find('127.0.%.1', '', 10, 'GET'), '->find() does not interpret a "%" as a wildcard in the IP'); $this->assertCount(0, $this->storage->find('127.0._.1', '', 10, 'GET'), '->find() does not interpret a "_" as a wildcard in the IP'); } public function testRetrieveByStatusCode() { $profile200 = new Profile('statuscode200'); $profile200->setStatusCode(200); $this->storage->write($profile200); $profile404 = new Profile('statuscode404'); $profile404->setStatusCode(404); $this->storage->write($profile404); $this->assertCount(1, $this->storage->find(null, null, 10, null, null, null, '200'), '->find() retrieve a record by Status code 200'); $this->assertCount(1, $this->storage->find(null, null, 10, null, null, null, '404'), '->find() retrieve a record by Status code 404'); } public function testRetrieveByUrl() { $profile = new Profile('simple_quote'); $profile->setIp('127.0.0.1'); $profile->setUrl('http://foo.bar/\''); $profile->setMethod('GET'); $this->storage->write($profile); $profile = new Profile('double_quote'); $profile->setIp('127.0.0.1'); $profile->setUrl('http://foo.bar/"'); $profile->setMethod('GET'); $this->storage->write($profile); $profile = new Profile('backslash'); $profile->setIp('127.0.0.1'); $profile->setUrl('http://foo\\bar/'); $profile->setMethod('GET'); $this->storage->write($profile); $profile = new Profile('percent'); $profile->setIp('127.0.0.1'); $profile->setUrl('http://foo.bar/%'); $profile->setMethod('GET'); $this->storage->write($profile); $profile = new Profile('underscore'); $profile->setIp('127.0.0.1'); $profile->setUrl('http://foo.bar/_'); $profile->setMethod('GET'); $this->storage->write($profile); $profile = new Profile('semicolon'); $profile->setIp('127.0.0.1'); $profile->setUrl('http://foo.bar/;'); $profile->setMethod('GET'); $this->storage->write($profile); $profile = new Profile('webp'); $profile->setIp('127.0.0.1'); $profile->setUrl('http://foo.bar/img.webp'); $profile->setMethod('GET'); $this->storage->write($profile); $this->assertCount(1, $this->storage->find('127.0.0.1', 'http://foo.bar/\'', 10, 'GET'), '->find() accepts single quotes in URLs'); $this->assertCount(1, $this->storage->find('127.0.0.1', 'http://foo.bar/"', 10, 'GET'), '->find() accepts double quotes in URLs'); $this->assertCount(1, $this->storage->find('127.0.0.1', 'http://foo\\bar/', 10, 'GET'), '->find() accepts backslash in URLs'); $this->assertCount(1, $this->storage->find('127.0.0.1', 'http://foo.bar/;', 10, 'GET'), '->find() accepts semicolon in URLs'); $this->assertCount(1, $this->storage->find('127.0.0.1', 'http://foo.bar/%', 10, 'GET'), '->find() does not interpret a "%" as a wildcard in the URL'); $this->assertCount(1, $this->storage->find('127.0.0.1', 'http://foo.bar/_', 10, 'GET'), '->find() does not interpret a "_" as a wildcard in the URL'); $this->assertCount(6, $this->storage->find('127.0.0.1', '!.webp', 10, 'GET'), '->find() does not interpret a "!" at the beginning as a negation operator in the URL'); } public function testStoreTime() { $start = $now = time(); for ($i = 0; $i < 3; ++$i) { $now += 60; $profile = new Profile('time_'.$i); $profile->setIp('127.0.0.1'); $profile->setUrl('http://foo.bar'); $profile->setTime($now); $profile->setMethod('GET'); $this->storage->write($profile); } $records = $this->storage->find('', '', 3, 'GET', $start, $start + 3 * 60); $this->assertCount(3, $records, '->find() returns all previously added records'); $this->assertEquals('time_2', $records[0]['token'], '->find() returns records ordered by time in descendant order'); $this->assertEquals('time_1', $records[1]['token'], '->find() returns records ordered by time in descendant order'); $this->assertEquals('time_0', $records[2]['token'], '->find() returns records ordered by time in descendant order'); $records = $this->storage->find('', '', 3, 'GET', $start, $start + 2 * 60); $this->assertCount(2, $records, '->find() should return only first two of the previously added records'); } public function testRetrieveByEmptyUrlAndIp() { for ($i = 0; $i < 5; ++$i) { $profile = new Profile('token_'.$i); $profile->setMethod('GET'); $this->storage->write($profile); } $this->assertCount(5, $this->storage->find('', '', 10, 'GET'), '->find() returns all previously added records'); $this->storage->purge(); } public function testRetrieveByMethodAndLimit() { foreach (['POST', 'GET'] as $method) { for ($i = 0; $i < 5; ++$i) { $profile = new Profile('token_'.$i.$method); $profile->setMethod($method); $this->storage->write($profile); } } $this->assertCount(5, $this->storage->find('', '', 5, 'POST')); $this->storage->purge(); } public function testPurge() { $profile = new Profile('token1'); $profile->setIp('127.0.0.1'); $profile->setUrl('http://example.com/'); $profile->setMethod('GET'); $profile->setStatusCode(200); $this->storage->write($profile); $this->assertNotFalse($this->storage->read('token1')); $this->assertCount(1, $this->storage->find('127.0.0.1', '', 10, 'GET')); $profile = new Profile('token2'); $profile->setIp('127.0.0.1'); $profile->setUrl('http://example.net/'); $profile->setMethod('GET'); $profile->setStatusCode(200); $this->storage->write($profile); $this->assertNotFalse($this->storage->read('token2')); $this->assertCount(2, $this->storage->find('127.0.0.1', '', 10, 'GET')); $this->storage->purge(); $this->assertNull($this->storage->read('token'), '->purge() removes all data stored by profiler'); $this->assertCount(0, $this->storage->find('127.0.0.1', '', 10, 'GET'), '->purge() removes all items from index'); } public function testDuplicates() { for ($i = 1; $i <= 5; ++$i) { $profile = new Profile('foo'.$i); $profile->setIp('127.0.0.1'); $profile->setUrl('http://example.net/'); $profile->setMethod('GET'); // three duplicates $this->storage->write($profile); $this->storage->write($profile); $this->storage->write($profile); } $this->assertCount(3, $this->storage->find('127.0.0.1', 'http://example.net/', 3, 'GET'), '->find() method returns incorrect number of entries'); } public function testStatusCode() { $profile = new Profile('token1'); $profile->setStatusCode(200); $this->storage->write($profile); $profile = new Profile('token2'); $profile->setStatusCode(404); $this->storage->write($profile); $tokens = $this->storage->find('', '', 10, ''); $this->assertCount(2, $tokens); $this->assertContains((int) $tokens[0]['status_code'], [200, 404]); $this->assertContains((int) $tokens[1]['status_code'], [200, 404]); } public function testHasErrors() { $profile = new Profile('token_with_errors'); $profile->setIp('127.0.0.1'); $profile->setUrl('http://foo.bar/error'); $profile->setMethod('GET'); $profile->setStatusCode(500); $profile->setHasErrors(true); $this->storage->write($profile); $profile = new Profile('token_without_errors'); $profile->setIp('127.0.0.1'); $profile->setUrl('http://foo.bar/success'); $profile->setMethod('GET'); $profile->setStatusCode(200); $profile->setHasErrors(false); $this->storage->write($profile); $loadedProfile = $this->storage->read('token_with_errors'); $this->assertTrue($loadedProfile->hasErrors(), '->read() restores hasErrors=true on the Profile object'); $loadedProfile = $this->storage->read('token_without_errors'); $this->assertFalse($loadedProfile->hasErrors(), '->read() restores hasErrors=false on the Profile object'); } public function testHasErrorsBackwardCompatibility() { // Test backward compatibility with old CSV lines that don't have has_errors field $file = $this->tmpDir.'/index.csv'; $time = time(); // Write an old-format CSV line (8 fields, no has_errors) file_put_contents($file, "old_token,127.0.0.1,GET,http://foo.bar/old,{$time},,200,request\n"); $tokens = $this->storage->find('', '', 10, ''); $this->assertCount(1, $tokens); $this->assertFalse($tokens[0]['has_errors'], '->find() returns has_errors=false for old CSV lines without the field'); } public function testMultiRowIndexFile() { $iteration = 3; for ($i = 0; $i < $iteration; ++$i) { $profile = new Profile('token'.$i); $profile->setIp('127.0.0.'.$i); $profile->setUrl('http://foo.bar/'.$i); $this->storage->write($profile); $this->storage->write($profile); $this->storage->write($profile); } $handle = fopen($this->tmpDir.'/index.csv', 'r'); for ($i = 0; $i < $iteration; ++$i) { $row = fgetcsv($handle, null, ',', '"', '\\'); $this->assertEquals('token'.$i, $row[0]); $this->assertEquals('127.0.0.'.$i, $row[1]); $this->assertEquals('http://foo.bar/'.$i, $row[3]); } $this->assertFalse(fgetcsv($handle, null, ',', '"', '\\')); } #[DataProvider('provideExpiredProfiles')] public function testRemoveExpiredProfiles(string $index, string $expectedOffset) { $file = $this->tmpDir.'/index.csv'; file_put_contents($file, $index); $r = new \ReflectionMethod($this->storage, 'removeExpiredProfiles'); $r->invoke($this->storage); $this->assertSame($expectedOffset, file_get_contents($this->tmpDir.'/index.csv.offset')); } public static function provideExpiredProfiles() { $oneHourAgo = new \DateTimeImmutable('-1 hour'); yield 'One unexpired profile' => [ <<getTimestamp()},, CSV, '0', ]; yield 'One unexpired profile with virtual type' => [ <<getTimestamp()},,virtual CSV, '0', ]; $threeDaysAgo = new \DateTimeImmutable('-3 days'); yield 'One expired profile' => [ <<getTimestamp()},, CSV, '48', ]; yield 'One expired profile with virtual type' => [ <<getTimestamp()},,virtual CSV, '55', ]; $fourDaysAgo = new \DateTimeImmutable('-4 days'); $threeDaysAgo = new \DateTimeImmutable('-3 days'); $oneHourAgo = new \DateTimeImmutable('-1 hour'); yield 'Multiple expired profiles' => [ <<getTimestamp()},, token1,127.0.0.1,,http://foo.bar/1,{$threeDaysAgo->getTimestamp()},, token2,127.0.0.2,,http://foo.bar/2,{$oneHourAgo->getTimestamp()},, CSV, '96', ]; yield 'Multiple expired profiles with virtual type' => [ <<getTimestamp()},,virtual token1,127.0.0.1,,http://foo.bar/1,{$threeDaysAgo->getTimestamp()},,virtual token2,127.0.0.2,,http://foo.bar/2,{$oneHourAgo->getTimestamp()},,virtual CSV, '110', ]; } public function testReadLineFromFile() { $r = new \ReflectionMethod($this->storage, 'readLineFromFile'); $h = tmpfile(); fwrite($h, "line1\n\n\nline2\n"); fseek($h, 0, \SEEK_END); $this->assertEquals('line2', $r->invoke($this->storage, $h)); $this->assertEquals('line1', $r->invoke($this->storage, $h)); } protected function cleanDir() { $flags = \FilesystemIterator::SKIP_DOTS; $iterator = new \RecursiveDirectoryIterator($this->tmpDir, $flags); $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); foreach ($iterator as $file) { if (is_file($file)) { unlink($file); } } } } ================================================ FILE: Tests/Profiler/ProfilerTest.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests\Profiler; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface; use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector; use Symfony\Component\HttpKernel\Profiler\FileProfilerStorage; use Symfony\Component\HttpKernel\Profiler\Profiler; class ProfilerTest extends TestCase { private string $tmp; private ?FileProfilerStorage $storage = null; public function testCollect() { $request = new Request(); $request->query->set('foo', 'bar'); $request->server->set('REMOTE_ADDR', '127.0.0.1'); $response = new Response('', 204); $collector = new RequestDataCollector(); $profiler = new Profiler($this->storage); $profiler->add($collector); $profile = $profiler->collect($request, $response); $profiler->saveProfile($profile); $this->assertSame(204, $profile->getStatusCode()); $this->assertSame('GET', $profile->getMethod()); $this->assertSame('bar', $profile->getCollector('request')->getRequestQuery()->all()['foo']->getValue()); } public function testReset() { $collector = $this->getMockBuilder(DataCollectorInterface::class) ->onlyMethods(['collect', 'getName', 'reset']) ->getMock(); $collector->method('getName')->willReturn('mock'); $collector->expects($this->once())->method('reset'); $profiler = new Profiler($this->storage); $profiler->add($collector); $profiler->reset(); } public function testFindWorksWithDates() { $profiler = new Profiler($this->storage); $this->assertCount(0, $profiler->find(null, null, null, null, '7th April 2014', '9th April 2014')); } public function testFindWorksWithTimestamps() { $profiler = new Profiler($this->storage); $this->assertCount(0, $profiler->find(null, null, null, null, '1396828800', '1397001600')); } public function testFindWorksWithInvalidDates() { $profiler = new Profiler($this->storage); $this->assertCount(0, $profiler->find(null, null, null, null, 'some string', '')); } public function testFindWorksWithStatusCode() { $profiler = new Profiler($this->storage); $this->assertCount(0, $profiler->find(null, null, null, null, null, null, '204')); } public function testIsInitiallyEnabled() { self::assertTrue((new Profiler($this->storage))->isEnabled()); } public function testDisable() { $profiler = new Profiler($this->storage); $profiler->disable(); self::assertFalse($profiler->isEnabled()); } public function testEnable() { $profiler = new Profiler($this->storage); $profiler->disable(); $profiler->enable(); self::assertTrue($profiler->isEnabled()); } protected function setUp(): void { $this->tmp = tempnam(sys_get_temp_dir(), 'sf_profiler'); if (file_exists($this->tmp)) { @unlink($this->tmp); } $this->storage = new FileProfilerStorage('file:'.$this->tmp); $this->storage->purge(); } protected function tearDown(): void { if (null !== $this->storage) { $this->storage->purge(); $this->storage = null; @unlink($this->tmp); } } } ================================================ FILE: Tests/TestHttpKernel.php ================================================ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Tests; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\HttpKernel; class TestHttpKernel extends HttpKernel implements ControllerResolverInterface, ArgumentResolverInterface { public function __construct() { parent::__construct(new EventDispatcher(), $this, null, $this); } public function getController(Request $request): callable|false { return $this->callController(...); } public function getArguments(Request $request, callable $controller, ?\ReflectionFunctionAbstract $reflector = null): array { return [$request]; } public function callController(Request $request) { return new Response('Request: '.$request->getRequestUri()); } } ================================================ FILE: composer.json ================================================ { "name": "symfony/http-kernel", "type": "library", "description": "Provides a structured process for converting a Request into a Response", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", "authors": [ { "name": "Fabien Potencier", "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], "require": { "php": ">=8.4", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^7.4|^8.0", "symfony/event-dispatcher": "^7.4|^8.0", "symfony/http-foundation": "^7.4|^8.0", "symfony/polyfill-ctype": "^1.8" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", "symfony/browser-kit": "^7.4|^8.0", "symfony/clock": "^7.4|^8.0", "symfony/config": "^7.4|^8.0", "symfony/console": "^7.4|^8.0", "symfony/css-selector": "^7.4|^8.0", "symfony/dependency-injection": "^7.4|^8.0", "symfony/dom-crawler": "^7.4|^8.0", "symfony/expression-language": "^7.4|^8.0", "symfony/finder": "^7.4|^8.0", "symfony/http-client-contracts": "^2.5|^3", "symfony/process": "^7.4|^8.0", "symfony/property-access": "^7.4|^8.0", "symfony/routing": "^7.4|^8.0", "symfony/serializer": "^7.4|^8.0", "symfony/stopwatch": "^7.4|^8.0", "symfony/translation": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3", "symfony/uid": "^7.4|^8.0", "symfony/validator": "^7.4|^8.0", "symfony/var-dumper": "^7.4|^8.0", "symfony/var-exporter": "^7.4|^8.0", "twig/twig": "^3.21" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "conflict": { "symfony/flex": "<2.10", "symfony/http-client-contracts": "<2.5", "symfony/translation-contracts": "<2.5", "twig/twig": "<3.21" }, "autoload": { "psr-4": { "Symfony\\Component\\HttpKernel\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, "minimum-stability": "dev" } ================================================ FILE: phpunit.xml.dist ================================================ ./Tests/ ./ ./Tests ./vendor