diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4b05237..c1a8ae9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -31,10 +31,10 @@ jobs:
- '4.*'
- '5.*'
- '6.*'
+ - '7.*'
dependency:
+ - 'lowest'
- 'highest'
- include:
- - { php: '7.1', symfony: '3.*', dependency: 'lowest' }
exclude:
- { php: '8.0', symfony: '3.*' }
- { php: '8.1', symfony: '3.*' }
@@ -46,6 +46,20 @@ jobs:
- { php: '7.3', symfony: '6.*' }
- { php: '7.4', symfony: '6.*' }
- { php: '8.0', symfony: '6.*' }
+ - { php: '7.1', symfony: '7.*' }
+ - { php: '7.2', symfony: '7.*' }
+ - { php: '7.3', symfony: '7.*' }
+ - { php: '7.4', symfony: '7.*' }
+ - { php: '8.0', symfony: '7.*' }
+ - { php: '8.1', symfony: '7.*' }
+ - { php: '8.0', symfony: '4.*', dependency: 'lowest' }
+ - { php: '8.1', dependency: 'lowest' }
+ - { php: '8.2', symfony: '4.*', dependency: 'lowest' }
+ - { php: '8.2', symfony: '5.*', dependency: 'lowest' }
+ - { php: '8.2', symfony: '6.*', dependency: 'lowest' }
+ - { php: '8.3', symfony: '4.*', dependency: 'lowest' }
+ - { php: '8.3', symfony: '5.*', dependency: 'lowest' }
+ - { php: '8.3', symfony: '6.*', dependency: 'lowest' }
steps:
- name: Checkout
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67337b9..001bb0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.9.0]
+### Added
+- Support for Symfony 7.4
+- Support for `psr/log` 3
+
+### Changed
+- The Symfony components the bundle uses directly are required explicitly: `symfony/config`, `symfony/dependency-injection`,
+ `symfony/http-foundation`, `symfony/http-kernel`, `symfony/property-access`, `symfony/routing` and `symfony/security-core`
+- `Configuration::getConfigTreeBuilder()` declares its `TreeBuilder` return type and `PayseraApiExtension::load()` declares
+ `void`. Breaking for subclasses that override either method without the return type: add `: TreeBuilder` or `: void` to
+ the override
+- Symfony 7 reads no docblock annotations, so on Symfony 7 loading a route whose controller still uses the bundle's
+ docblock annotations (`@Body`, `@Query`, `@PathAttribute`, `@ResponseNormalization`, `@RequiredPermissions`,
+ `@Validation`, `@BodyContentType`) fails with a `ConfigurationException` naming them: use the attributes of the same
+ name. Symfony 4.4 to 6.4 are unchanged
+- Optional parameters are declared nullable explicitly (`?Type $parameter = null`), as PHP 8.4 expects
+- CI runs the tests on Symfony 7 with PHP 8.2 and 8.3
+
+### Fixed
+- On Symfony 7.1 and later, `LocaleListener` picks the locale from `Accept-Language` the same way as on older Symfony
+ versions: a request asking for German (`de`) kept the default locale there
+
## [1.8.2]
### Changed
- CI allows packages with security advisories, so Symfony 3.4 and 4.4 jobs can install dependencies with Composer 2.10
diff --git a/README.md b/README.md
index eec01c8..0f6c466 100644
--- a/README.md
+++ b/README.md
@@ -645,6 +645,12 @@ cursor and iterating this way until we have `"has_previous": false` is a reliabl
## Annotations/Attributes reference
+Every option below exists as a docblock annotation (`Paysera\Bundle\ApiBundle\Annotation\*`) and, since 1.8.0, as a PHP
+attribute of the same name (`Paysera\Bundle\ApiBundle\Attribute\*`, read on Symfony 6.4 and later). On Symfony 7 use the
+attributes: Symfony 7 reads no docblock annotations, so loading a route whose controller still uses the bundle's docblock
+annotations fails with an error naming them, and the routes themselves need `#[Route]` with `type: attribute` imports. The attributes take the same options, passed by name:
+`@RequiredPermissions(permissions={"ROLE_ADMIN"})` becomes `#[RequiredPermissions(permissions: ['ROLE_ADMIN'])]`.
+
### `Body`
Instructs to convert request body into an object and pass to the controller as an argument.
diff --git a/composer.json b/composer.json
index f80e408..cbcd5c2 100644
--- a/composer.json
+++ b/composer.json
@@ -17,24 +17,31 @@
"require": {
"php": "^7.1 || ^8.0",
"ext-json": "*",
- "symfony/framework-bundle": "^3.4.34|^4.3|^5.4|^6.0",
- "symfony/security-bundle": "^3.4.34|^4.3|^5.4|^6.0",
- "symfony/validator": "^3.4.34|^4.3|^5.4|^6.0",
+ "symfony/config": "^3.4.34|^4.3|^5.4|^6.0|^7.4",
+ "symfony/dependency-injection": "^3.4.34|^4.3|^5.4|^6.0|^7.4",
+ "symfony/framework-bundle": "^3.4.34|^4.3|^5.4|^6.0|^7.4",
+ "symfony/http-foundation": "^3.4.34|^4.3|^5.4|^6.0|^7.4",
+ "symfony/http-kernel": "^3.4.34|^4.3|^5.4|^6.0|^7.4",
+ "symfony/property-access": "^3.4.34|^4.3|^5.4|^6.0|^7.4",
+ "symfony/routing": "^3.4.34|^4.3|^5.4|^6.0|^7.4",
+ "symfony/security-bundle": "^3.4.34|^4.3|^5.4|^6.0|^7.4",
+ "symfony/security-core": "^3.4.34|^4.3|^5.4|^6.0|^7.4",
+ "symfony/validator": "^3.4.34|^4.3|^5.4|^6.0|^7.4",
"paysera/lib-normalization-bundle": "^1.1.0",
"paysera/lib-normalization": "^1.2",
"paysera/lib-object-wrapper": "~0.1",
"paysera/lib-pagination": "^1.0",
"paysera/lib-dependency-injection": "^1.3.0",
- "psr/log": "^1.0|^2.0",
+ "psr/log": "^1.0|^2.0|^3.0",
"doctrine/persistence": "^1.3.8 || ^2.0.1 || ^3.0",
"doctrine/annotations": "^1.14 || ^2.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^9.6",
"mockery/mockery": "^1.3.6",
- "symfony/yaml": "^3.4.34|^4.3|^5.4|^6.0",
+ "symfony/yaml": "^3.4.34|^4.3|^5.4|^6.0|^7.4",
"doctrine/doctrine-bundle": "^1.12.0|^2.1",
- "doctrine/orm": "^2.5.14"
+ "doctrine/orm": "^2.6.3"
},
"config": {
"bin-dir": "bin"
diff --git a/src/Attribute/Body.php b/src/Attribute/Body.php
index a04ded8..8dcd5f5 100644
--- a/src/Attribute/Body.php
+++ b/src/Attribute/Body.php
@@ -34,7 +34,7 @@ class Body implements RestAttributeInterface
public function __construct(
array $options = [],
- string $parameterName = null,
+ ?string $parameterName = null,
?string $denormalizationType = null,
?string $denormalizationGroup = null,
?bool $optional = null
diff --git a/src/Attribute/BodyContentType.php b/src/Attribute/BodyContentType.php
index d1b0f1a..d42f9ea 100644
--- a/src/Attribute/BodyContentType.php
+++ b/src/Attribute/BodyContentType.php
@@ -23,7 +23,7 @@ class BodyContentType implements RestAttributeInterface
public function __construct(
array $options = [],
- array $supportedContentTypes = null,
+ ?array $supportedContentTypes = null,
bool $jsonEncodedBody = false
) {
$this->setSupportedContentTypes($options['supportedContentTypes'] ?? $supportedContentTypes);
diff --git a/src/Attribute/PathAttribute.php b/src/Attribute/PathAttribute.php
index 471f9a7..fca6827 100644
--- a/src/Attribute/PathAttribute.php
+++ b/src/Attribute/PathAttribute.php
@@ -35,8 +35,8 @@ class PathAttribute implements RestAttributeInterface
public function __construct(
array $options = [],
- string $parameterName = null,
- string $pathPartName = null,
+ ?string $parameterName = null,
+ ?string $pathPartName = null,
?string $resolverType = null,
?bool $resolutionMandatory = null
) {
diff --git a/src/Attribute/Query.php b/src/Attribute/Query.php
index ea47fb8..0a23815 100644
--- a/src/Attribute/Query.php
+++ b/src/Attribute/Query.php
@@ -35,7 +35,7 @@ class Query implements RestAttributeInterface
public function __construct(
array $options = [],
- string $parameterName = null,
+ ?string $parameterName = null,
?string $denormalizationType = null,
?string $denormalizationGroup = null,
?Validation $validation = null
diff --git a/src/Attribute/RequiredPermissions.php b/src/Attribute/RequiredPermissions.php
index 648d0c1..4960d7a 100644
--- a/src/Attribute/RequiredPermissions.php
+++ b/src/Attribute/RequiredPermissions.php
@@ -18,7 +18,7 @@ class RequiredPermissions implements RestAttributeInterface
public function __construct(
array $options = [],
- array $permissions = null
+ ?array $permissions = null
) {
$this->setPermissions($options['permissions'] ?? $permissions);
}
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 99b4c98..8faa5a7 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -10,7 +10,7 @@
class Configuration implements ConfigurationInterface
{
- public function getConfigTreeBuilder()
+ public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('paysera_api');
$rootNode = method_exists($treeBuilder, 'getRootNode')
diff --git a/src/DependencyInjection/PayseraApiExtension.php b/src/DependencyInjection/PayseraApiExtension.php
index 2a1db01..a60661b 100644
--- a/src/DependencyInjection/PayseraApiExtension.php
+++ b/src/DependencyInjection/PayseraApiExtension.php
@@ -17,7 +17,7 @@
class PayseraApiExtension extends Extension
{
- public function load(array $configs, ContainerBuilder $container)
+ public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
diff --git a/src/Entity/QueryResolverOptions.php b/src/Entity/QueryResolverOptions.php
index b898a53..e3701c9 100644
--- a/src/Entity/QueryResolverOptions.php
+++ b/src/Entity/QueryResolverOptions.php
@@ -58,7 +58,7 @@ public function setDenormalizationGroup($denormalizationGroup): self
* @param ValidationOptions|null $validationOptions
* @return $this
*/
- public function setValidationOptions(ValidationOptions $validationOptions = null): self
+ public function setValidationOptions(?ValidationOptions $validationOptions = null): self
{
$this->validationOptions = $validationOptions;
return $this;
diff --git a/src/Exception/ApiException.php b/src/Exception/ApiException.php
index ae8d096..010c1ad 100644
--- a/src/Exception/ApiException.php
+++ b/src/Exception/ApiException.php
@@ -60,7 +60,7 @@ public function __construct(
$errorCode,
$message = null,
$statusCode = null,
- Exception $previous = null,
+ ?Exception $previous = null,
$properties = null,
$data = null,
array $violations = []
diff --git a/src/Listener/LocaleListener.php b/src/Listener/LocaleListener.php
index 241101f..aebfd9a 100644
--- a/src/Listener/LocaleListener.php
+++ b/src/Listener/LocaleListener.php
@@ -4,6 +4,7 @@
namespace Paysera\Bundle\ApiBundle\Listener;
use Paysera\Bundle\ApiBundle\Service\RestRequestHelper;
+use Symfony\Component\HttpFoundation\AcceptHeader;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
@@ -42,15 +43,53 @@ public function onKernelRequest($event)
}
}
- private function resolveFromHeaders(Request $request)
+ private function resolveFromHeaders(Request $request): ?string
{
- $defaultLocale = 'default';
- $preferredLanguage = $request->getPreferredLanguage(array_merge([$defaultLocale], $this->locales));
+ $languages = $this->readLanguages($request);
+ $candidates = [];
+ foreach ($languages as $language) {
+ $candidates[] = $language;
+ $separatorPosition = strpos($language, '_');
+ if ($separatorPosition === false) {
+ continue;
+ }
+ $primaryLanguage = substr($language, 0, $separatorPosition);
+ if (!in_array($primaryLanguage, $languages, true)) {
+ $candidates[] = $primaryLanguage;
+ }
+ }
- if ($preferredLanguage !== null && $preferredLanguage !== $defaultLocale) {
- return $preferredLanguage;
+ foreach ($candidates as $candidate) {
+ if (in_array($candidate, $this->locales, true)) {
+ return $candidate;
+ }
}
return null;
}
+
+ /**
+ * @return string[]
+ */
+ private function readLanguages(Request $request): array
+ {
+ $languages = [];
+ foreach (AcceptHeader::fromString($request->headers->get('Accept-Language'))->all() as $item) {
+ $language = (string)$item->getValue();
+ if (strpos($language, '-') !== false) {
+ $codes = explode('-', $language);
+ if ($codes[0] === 'i') {
+ $language = $codes[1];
+ } else {
+ $language = strtolower($codes[0]);
+ for ($i = 1, $count = count($codes); $i < $count; $i++) {
+ $language .= '_' . strtoupper($codes[$i]);
+ }
+ }
+ }
+ $languages[] = $language;
+ }
+
+ return $languages;
+ }
}
diff --git a/src/Listener/RestExceptionListener.php b/src/Listener/RestExceptionListener.php
index 3d71a9b..6b3e9e8 100644
--- a/src/Listener/RestExceptionListener.php
+++ b/src/Listener/RestExceptionListener.php
@@ -32,7 +32,7 @@ public function __construct(
ErrorBuilderInterface $errorBuilder,
CoreNormalizer $coreNormalizer,
ResponseBuilder $responseBuilder,
- LoggerInterface $logger = null
+ ?LoggerInterface $logger = null
) {
$this->requestHelper = $requestHelper;
$this->errorBuilder = $errorBuilder;
diff --git a/src/Service/RoutingLoader/RoutingAttributeLoader.php b/src/Service/RoutingLoader/RoutingAttributeLoader.php
index 955f27d..1774270 100644
--- a/src/Service/RoutingLoader/RoutingAttributeLoader.php
+++ b/src/Service/RoutingLoader/RoutingAttributeLoader.php
@@ -6,6 +6,7 @@
use Paysera\Bundle\ApiBundle\Annotation\RestAnnotationInterface;
use Paysera\Bundle\ApiBundle\Attribute\RestAttributeInterface;
+use Paysera\Bundle\ApiBundle\Exception\ConfigurationException;
use Paysera\Bundle\ApiBundle\Service\RestRequestHelper;
use ReflectionClass;
use ReflectionMethod;
@@ -17,6 +18,16 @@
*/
class RoutingAttributeLoader extends AttributeRouteControllerLoader
{
+ private const ANNOTATION_NAMES = [
+ 'Body',
+ 'BodyContentType',
+ 'PathAttribute',
+ 'Query',
+ 'RequiredPermissions',
+ 'ResponseNormalization',
+ 'Validation',
+ ];
+
/**
* @var RestRequestHelper
*/
@@ -59,8 +70,17 @@ protected function configureRoute(
$this->loadAttributes($route, $class, $method);
}
+ /**
+ * @throws ConfigurationException
+ */
private function loadAnnotations(Route $route, ReflectionClass $class, ReflectionMethod $method): void
{
+ if (!property_exists($this, 'reader')) {
+ $this->refuseDocblockAnnotations($class, $method);
+
+ return;
+ }
+
if (!isset($this->reader)) {
return;
}
@@ -88,6 +108,27 @@ private function loadAnnotations(Route $route, ReflectionClass $class, Reflectio
);
}
+ /**
+ * @throws ConfigurationException
+ */
+ private function refuseDocblockAnnotations(ReflectionClass $class, ReflectionMethod $method): void
+ {
+ $pattern = '/(?getDocComment() . $method->getDocComment(), $matches);
+ $names = array_values(array_unique($matches[1]));
+ if ($names === []) {
+ return;
+ }
+
+ throw new ConfigurationException(sprintf(
+ '%s::%s() uses docblock annotations of paysera/lib-api-bundle (@%s), which Symfony 7 does not read. '
+ . 'Use the attributes of the same name from Paysera\\Bundle\\ApiBundle\\Attribute instead.',
+ $class->getName(),
+ $method->getName(),
+ implode(', @', $names)
+ ));
+ }
+
private function loadAttributes(Route $route, ReflectionClass $class, ReflectionMethod $method): void
{
$attributes = array_merge($class->getAttributes(), $method->getAttributes());
diff --git a/src/Service/Validation/EntityValidator.php b/src/Service/Validation/EntityValidator.php
index 830d215..4335bae 100644
--- a/src/Service/Validation/EntityValidator.php
+++ b/src/Service/Validation/EntityValidator.php
@@ -26,8 +26,8 @@ class EntityValidator
protected $propertyPathConverter;
public function __construct(
- ValidatorInterface $validator = null,
- PropertyPathConverterInterface $propertyPathConverter = null
+ ?ValidatorInterface $validator = null,
+ ?PropertyPathConverterInterface $propertyPathConverter = null
) {
$this->validator = $validator;
$this->propertyPathConverter = $propertyPathConverter;
diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedPagedQueryController.php b/tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedPagedQueryController.php
new file mode 100644
index 0000000..5d5e77b
--- /dev/null
+++ b/tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedPagedQueryController.php
@@ -0,0 +1,34 @@
+entityManager = $entityManager;
+ }
+
+ #[Route(path: '/paged-query/simple', methods: ['GET'])]
+ #[Query(parameterName: 'pager')]
+ #[Query(parameterName: 'filter')]
+ public function findSimplePersistedEntities(Pager $pager, PersistedEntityFilter $filter): PagedQuery
+ {
+ /** @var PersistedEntityRepository $repository */
+ $repository = $this->entityManager->getRepository(PersistedEntity::class);
+ $configuredQuery = $repository->buildConfiguredQuery($filter);
+ return new PagedQuery($configuredQuery, $pager);
+ }
+}
diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedPersistedEntityController.php b/tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedPersistedEntityController.php
new file mode 100644
index 0000000..1fcbc06
--- /dev/null
+++ b/tests/Functional/Fixtures/FixtureTestBundle/Controller/Attribute/AttributedPersistedEntityController.php
@@ -0,0 +1,27 @@
+getId());
+ }
+
+ #[Route(path: '/simple-persisted-entities/{identifier}', methods: ['GET'])]
+ #[PathAttribute(parameterName: 'entity', pathPartName: 'identifier')]
+ public function findSimplePersistedEntity(SimplePersistedEntity $entity): Response
+ {
+ return new Response((string)$entity->getId());
+ }
+}
diff --git a/tests/Functional/Fixtures/FixtureTestBundle/DependencyInjection/Configuration.php b/tests/Functional/Fixtures/FixtureTestBundle/DependencyInjection/Configuration.php
index 31ced6a..87392e8 100644
--- a/tests/Functional/Fixtures/FixtureTestBundle/DependencyInjection/Configuration.php
+++ b/tests/Functional/Fixtures/FixtureTestBundle/DependencyInjection/Configuration.php
@@ -13,10 +13,7 @@
*/
class Configuration implements ConfigurationInterface
{
- /**
- * {@inheritdoc}
- */
- public function getConfigTreeBuilder()
+ public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('paysera_fixture_test');
diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Entity/PersistedEntity.php b/tests/Functional/Fixtures/FixtureTestBundle/Entity/PersistedEntity.php
index 5864530..3246bbf 100644
--- a/tests/Functional/Fixtures/FixtureTestBundle/Entity/PersistedEntity.php
+++ b/tests/Functional/Fixtures/FixtureTestBundle/Entity/PersistedEntity.php
@@ -3,24 +3,15 @@
namespace Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Entity;
-use Doctrine\ORM\Mapping as ORM;
-
-/**
- * @ORM\Entity(repositoryClass="Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Repository\PersistedEntityRepository")
- */
class PersistedEntity
{
/**
- * @ORM\Id
- * @ORM\GeneratedValue(strategy="NONE")
- * @ORM\Column(type="integer")
* @var int|null
*/
private $id;
/**
* @var string|null
- * @ORM\Column(type="string")
*/
private $someField;
diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Entity/SimplePersistedEntity.php b/tests/Functional/Fixtures/FixtureTestBundle/Entity/SimplePersistedEntity.php
index 81a8437..2bcbf96 100644
--- a/tests/Functional/Fixtures/FixtureTestBundle/Entity/SimplePersistedEntity.php
+++ b/tests/Functional/Fixtures/FixtureTestBundle/Entity/SimplePersistedEntity.php
@@ -3,17 +3,9 @@
namespace Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Entity;
-use Doctrine\ORM\Mapping as ORM;
-
-/**
- * @ORM\Entity
- */
class SimplePersistedEntity
{
/**
- * @ORM\Id
- * @ORM\GeneratedValue(strategy="NONE")
- * @ORM\Column(type="integer")
* @var int|null
*/
private $id;
diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/doctrine/PersistedEntity.orm.xml b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/doctrine/PersistedEntity.orm.xml
new file mode 100644
index 0000000..9587ae8
--- /dev/null
+++ b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/doctrine/PersistedEntity.orm.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/doctrine/SimplePersistedEntity.orm.xml b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/doctrine/SimplePersistedEntity.orm.xml
new file mode 100644
index 0000000..e3b92e0
--- /dev/null
+++ b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/doctrine/SimplePersistedEntity.orm.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/explicit_routing.xml b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/explicit_routing.xml
new file mode 100644
index 0000000..64b6faf
--- /dev/null
+++ b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/explicit_routing.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+ Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Controller\DefaultController::action1Action
+
+
+
+ Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Controller\DefaultController::action1bAction
+
+
+
+
+ Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Controller\DefaultController::action2
+
+
+
+ paysera_fixture_test.controller.default_controller::action3
+
+
+ paysera_fixture_test.controller.default_controller::action4
+
+
+
+ Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Controller\DefaultController::action5
+
+
+
+
+ paysera_fixture_test.controller.default_controller::action
+
+
+ paysera_fixture_test.controller.default_controller::actionWithReturn
+
+
+
+ Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Controller\DefaultController::action
+
+
+
+ paysera_fixture_test.controller.default_controller::actionWithMultipleParameters
+
+
+ paysera_fixture_test.controller.default_controller::actionWithMultipleParameters
+
+
diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/routing.xml b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/routing.xml
index 3a06c43..3118327 100644
--- a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/routing.xml
+++ b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/routing.xml
@@ -11,46 +11,5 @@
-
- Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Controller\DefaultController::action1Action
-
-
-
- Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Controller\DefaultController::action1bAction
-
-
-
-
- Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Controller\DefaultController::action2
-
-
-
- paysera_fixture_test.controller.default_controller::action3
-
-
- paysera_fixture_test.controller.default_controller::action4
-
-
-
- Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Controller\DefaultController::action5
-
-
-
-
- paysera_fixture_test.controller.default_controller::action
-
-
- paysera_fixture_test.controller.default_controller::actionWithReturn
-
-
-
- Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Controller\DefaultController::action
-
-
-
- paysera_fixture_test.controller.default_controller::actionWithMultipleParameters
-
-
- paysera_fixture_test.controller.default_controller::actionWithMultipleParameters
-
+
diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/services.xml b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/services.xml
index 46836bd..24833de 100644
--- a/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/services.xml
+++ b/tests/Functional/Fixtures/FixtureTestBundle/Resources/config/services.xml
@@ -92,6 +92,10 @@
public="true">
+
+
+
diff --git a/tests/Functional/Fixtures/FixtureTestBundle/Service/TestHelper.php b/tests/Functional/Fixtures/FixtureTestBundle/Service/TestHelper.php
index a3133ff..e092728 100644
--- a/tests/Functional/Fixtures/FixtureTestBundle/Service/TestHelper.php
+++ b/tests/Functional/Fixtures/FixtureTestBundle/Service/TestHelper.php
@@ -5,6 +5,7 @@
namespace Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Service;
use Symfony\Bundle\FrameworkBundle\Routing\AttributeRouteControllerLoader;
+use Symfony\Component\Routing\Loader\AttributeClassLoader;
class TestHelper
{
@@ -12,4 +13,9 @@ public static function phpAttributeSupportExists(): bool
{
return class_exists(AttributeRouteControllerLoader::class);
}
+
+ public static function docblockRoutingSupportExists(): bool
+ {
+ return !class_exists(AttributeClassLoader::class) || property_exists(AttributeClassLoader::class, 'reader');
+ }
}
diff --git a/tests/Functional/Fixtures/TestKernel.php b/tests/Functional/Fixtures/TestKernel.php
index 9fc2a51..e0810d4 100644
--- a/tests/Functional/Fixtures/TestKernel.php
+++ b/tests/Functional/Fixtures/TestKernel.php
@@ -43,7 +43,7 @@ public function registerContainerConfiguration(LoaderInterface $loader)
$loader->load(__DIR__ . '/config/' . $this->configFile);
$loader->load(__DIR__ . '/config/' . $this->commonFile);
- if (TestHelper::phpAttributeSupportExists()) {
+ if (TestHelper::phpAttributeSupportExists() && TestHelper::docblockRoutingSupportExists()) {
$loader->load(__DIR__ . '/config/attributed_common.yml');
}
}
diff --git a/tests/Functional/Fixtures/config/sf7_common.yml b/tests/Functional/Fixtures/config/sf7_common.yml
new file mode 100644
index 0000000..9fe4ff7
--- /dev/null
+++ b/tests/Functional/Fixtures/config/sf7_common.yml
@@ -0,0 +1,17 @@
+framework:
+ router:
+ resource: '%kernel.project_dir%/tests/Functional/Fixtures/config/sf7_routing.yml'
+ validation:
+ email_validation_mode: html5
+
+security:
+ firewalls:
+ config:
+ pattern: ^/(config)/
+ security: false
+ main:
+ http_basic: ~
+ stateless: true
+ password_hashers:
+ Symfony\Component\Security\Core\User\InMemoryUser:
+ algorithm: plaintext
diff --git a/tests/Functional/Fixtures/config/sf7_routing.yml b/tests/Functional/Fixtures/config/sf7_routing.yml
new file mode 100644
index 0000000..2b876f8
--- /dev/null
+++ b/tests/Functional/Fixtures/config/sf7_routing.yml
@@ -0,0 +1,7 @@
+paysera_fixture_test:
+ resource: "@PayseraFixtureTestBundle/Resources/config/explicit_routing.xml"
+ prefix: /
+
+paysera_fixture_attributed_test:
+ resource: '@PayseraFixtureTestBundle/Resources/config/attributed_routing.xml'
+ prefix: /
diff --git a/tests/Functional/FunctionalAnnotationsTest.php b/tests/Functional/FunctionalAnnotationsTest.php
index 41b6f7f..bd04693 100644
--- a/tests/Functional/FunctionalAnnotationsTest.php
+++ b/tests/Functional/FunctionalAnnotationsTest.php
@@ -24,7 +24,7 @@ protected function setUp(): void
public function testAnnotatedRestRequestConfiguration(
Response $expectedResponse,
Request $request,
- Response $extraResponseVersion = null
+ ?Response $extraResponseVersion = null
) {
$this->makeTest('annotated', $expectedResponse, $request, $extraResponseVersion);
}
@@ -38,7 +38,7 @@ public function testAnnotatedRestRequestConfiguration(
public function testAttributedRestRequestConfiguration(
Response $expectedResponse,
Request $request,
- Response $extraResponseVersion = null
+ ?Response $extraResponseVersion = null
) {
$this->makeTest('attributed', $expectedResponse, $request, $extraResponseVersion);
}
@@ -47,11 +47,14 @@ private function makeTest(
string $pathPrefix,
Response $expectedResponse,
Request $request,
- Response $extraResponseVersion = null
+ ?Response $extraResponseVersion = null
): void {
if ($pathPrefix === 'attributed' && !TestHelper::phpAttributeSupportExists()) {
$this->markTestSkipped('Unsupported environment');
}
+ if ($pathPrefix === 'annotated' && !TestHelper::docblockRoutingSupportExists()) {
+ $this->markTestSkipped('Symfony 7 reads no @Route docblocks');
+ }
$request->server->set(
'REQUEST_URI',
@@ -177,6 +180,29 @@ public function restRequestsConfigurationProvider(): array
['Content-Type' => 'text/plain']
),
],
+ 'testBodyNormalizationWithCustomContentType and no content type' => [
+ new Response(
+ '{"error":"invalid_request","error_description":"Content-Type must be provided"}',
+ 400
+ ),
+ $this->createRequest(
+ 'POST',
+ '/testBodyNormalizationWithCustomContentType',
+ 'my_text'
+ ),
+ ],
+ 'testBodyNormalizationWithExtractedKeyValue and a body that is not JSON' => [
+ new Response(
+ '{"error":"invalid_request","error_description":"Cannot decode request body to JSON"}',
+ 400
+ ),
+ $this->createRequest(
+ 'POST',
+ '/testBodyNormalizationWithExtractedKeyValue',
+ '{"key": ',
+ ['Content-Type' => 'application/json']
+ ),
+ ],
'testBodyNormalizationWithCustomContentTypeAndJsonDecode and JSON content-type' => [
new Response(
'{"error":"invalid_request","error_description":"This Content-Type (application/json) is not supported"}',
diff --git a/tests/Functional/FunctionalTestCase.php b/tests/Functional/FunctionalTestCase.php
index 0a1f80e..58f14e2 100644
--- a/tests/Functional/FunctionalTestCase.php
+++ b/tests/Functional/FunctionalTestCase.php
@@ -5,6 +5,7 @@
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\SchemaTool;
+use Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\FixtureTestBundle\Service\TestHelper;
use Paysera\Bundle\ApiBundle\Tests\Functional\Fixtures\TestKernel;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Routing\AttributeRouteControllerLoader;
@@ -31,7 +32,12 @@ abstract class FunctionalTestCase extends TestCase
*/
protected function setUpContainer($testCase, $commonFile = 'common.yml')
{
- $prefix = Kernel::MAJOR_VERSION <= 4 ? 'legacy_' : '';
+ $prefix = '';
+ if (Kernel::MAJOR_VERSION <= 4) {
+ $prefix = 'legacy_';
+ } elseif (!TestHelper::docblockRoutingSupportExists()) {
+ $prefix = 'sf7_';
+ }
$this->kernel = new TestKernel($testCase, $prefix . $commonFile);
$this->kernel->boot();
return $this->kernel->getContainer();
@@ -57,9 +63,9 @@ protected function makeGetRequest(string $uri): Response
protected function createRequest(
string $method,
string $uri,
- string $content = null,
+ ?string $content = null,
array $headers = [],
- string $username = null
+ ?string $username = null
): Request {
$parts = parse_url($uri);
parse_str($parts['query'] ?? '', $query);
diff --git a/tests/Unit/Helper/HttpKernelHelper.php b/tests/Unit/Helper/HttpKernelHelper.php
index 67a50b0..4e3d23a 100644
--- a/tests/Unit/Helper/HttpKernelHelper.php
+++ b/tests/Unit/Helper/HttpKernelHelper.php
@@ -10,7 +10,7 @@ class HttpKernelHelper
{
public static function getMainRequestConstValue(): int
{
- if (defined(HttpKernelInterface::class . 'MAIN_REQUEST')) {
+ if (defined(HttpKernelInterface::class . '::MAIN_REQUEST')) {
return HttpKernelInterface::MAIN_REQUEST;
}
diff --git a/tests/Unit/Listener/LocaleListenerTest.php b/tests/Unit/Listener/LocaleListenerTest.php
index c404e8e..b56a774 100644
--- a/tests/Unit/Listener/LocaleListenerTest.php
+++ b/tests/Unit/Listener/LocaleListenerTest.php
@@ -8,10 +8,12 @@
use Paysera\Bundle\ApiBundle\Listener\LocaleListener;
use Paysera\Bundle\ApiBundle\Service\RestRequestHelper;
use Paysera\Bundle\ApiBundle\Tests\Unit\Helper\HttpKernelHelper;
+use Symfony\Component\HttpFoundation\AcceptHeader;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Throwable;
class LocaleListenerTest extends MockeryTestCase
{
@@ -24,6 +26,25 @@ class LocaleListenerTest extends MockeryTestCase
* @param bool $rest
*/
public function testOnKernelRequest(string $expectedLocale, array $locales, string $acceptLanguage, bool $rest)
+ {
+ $this->assertSame($expectedLocale, $this->resolveLocale($locales, $acceptLanguage, $rest));
+ }
+
+ public function testAnItemOfOnlyASemicolonIsNotALanguage()
+ {
+ try {
+ AcceptHeader::fromString(';');
+ } catch (Throwable $error) {
+ $this->markTestSkipped('This http-foundation fails on an empty Accept-Language item itself');
+ }
+
+ $this->assertSame('unchanged', $this->resolveLocale(['de'], ';', true));
+ }
+
+ /**
+ * @param string[] $locales
+ */
+ private function resolveLocale(array $locales, string $acceptLanguage, bool $rest): string
{
$helper = Mockery::mock(RestRequestHelper::class);
$kernel = Mockery::mock(HttpKernelInterface::class);
@@ -42,7 +63,7 @@ public function testOnKernelRequest(string $expectedLocale, array $locales, stri
$listener->onKernelRequest($event);
- $this->assertSame($expectedLocale, $request->getLocale());
+ return $request->getLocale();
}
public function provider()
@@ -96,6 +117,78 @@ public function provider()
'en-US,en;q=0.8, de-CH;q=0.9',
true,
],
+ 'German among other locales' => [
+ 'de',
+ ['en', 'lt', 'de'],
+ 'de',
+ true,
+ ],
+ 'German region among other locales' => [
+ 'de',
+ ['en', 'lt', 'de'],
+ 'de-DE, en;q=0.5',
+ true,
+ ],
+ 'primary language is not added when the header lists it' => [
+ 'de',
+ ['de', 'en'],
+ 'en-US, en;q=0.5, de;q=0.9',
+ true,
+ ],
+ 'no match keeps the locale' => [
+ 'unchanged',
+ ['en', 'lt', 'de'],
+ 'fr-FR, fr;q=0.9',
+ true,
+ ],
+ 'no header keeps the locale' => [
+ 'unchanged',
+ ['en', 'lt', 'de'],
+ '',
+ true,
+ ],
+ 'a tag without a region keeps its case, as Symfony 3.4 to 7.0 read it' => [
+ 'unchanged',
+ ['de'],
+ 'DE',
+ true,
+ ],
+ 'a wrong-case tag does not match before a matching one' => [
+ 'de',
+ ['en', 'lt', 'de'],
+ 'EN,de',
+ true,
+ ],
+ 'a numeric tag is not a language' => [
+ 'unchanged',
+ ['de'],
+ '1',
+ true,
+ ],
+ 'a numeric tag next to a language' => [
+ 'de',
+ ['de'],
+ 'de, 1',
+ true,
+ ],
+ 'a language registered with the i- prefix' => [
+ 'cherokee',
+ ['en', 'cherokee'],
+ 'i-cherokee',
+ true,
+ ],
+ 'a malformed item is not a language' => [
+ 'unchanged',
+ ['de'],
+ "'",
+ true,
+ ],
+ 'a malformed item next to a language' => [
+ 'de',
+ ['de'],
+ "de, '",
+ true,
+ ],
];
}
}
diff --git a/tests/Unit/Listener/RestExceptionListenerTest.php b/tests/Unit/Listener/RestExceptionListenerTest.php
index 3bdc879..f042855 100644
--- a/tests/Unit/Listener/RestExceptionListenerTest.php
+++ b/tests/Unit/Listener/RestExceptionListenerTest.php
@@ -29,7 +29,7 @@ class RestExceptionListenerTest extends MockeryTestCase
* @param int $statusCode
* @param string|null $logLevel
*/
- public function testOnKernelException(bool $restRequest, int $statusCode = 400, string $logLevel = null)
+ public function testOnKernelException(bool $restRequest, int $statusCode = 400, ?string $logLevel = null)
{
$helper = Mockery::mock(RestRequestHelper::class);
$errorBuilder = Mockery::mock(ErrorBuilderInterface::class);
diff --git a/tests/Unit/Normalizer/PagedQueryNormalizerTest.php b/tests/Unit/Normalizer/PagedQueryNormalizerTest.php
index 9cf702a..a912137 100644
--- a/tests/Unit/Normalizer/PagedQueryNormalizerTest.php
+++ b/tests/Unit/Normalizer/PagedQueryNormalizerTest.php
@@ -85,7 +85,7 @@ public function testNormalize(
array $explicitlyIncluded,
string $defaultStrategy,
string $queryStrategy,
- int $maximumOffset = null
+ ?int $maximumOffset = null
) {
$normalizationContext = Mockery::mock(NormalizationContext::class);
diff --git a/tests/Unit/Service/RoutingLoader/Fixtures/DocblockOptionsOnAttributeRouteController.php b/tests/Unit/Service/RoutingLoader/Fixtures/DocblockOptionsOnAttributeRouteController.php
new file mode 100644
index 0000000..384715d
--- /dev/null
+++ b/tests/Unit/Service/RoutingLoader/Fixtures/DocblockOptionsOnAttributeRouteController.php
@@ -0,0 +1,19 @@
+expectException(ConfigurationException::class);
+ $this->expectExceptionMessage('Only one annotation of type ' . Body::class . ' is supported');
$builder->buildOptions([
- new Body(['parameterName' => 'a']),
- new Body(['parameterName' => 'b']),
+ new Body(['parameterName' => 'a', 'denormalizationType' => 'type_a', 'optional' => false]),
+ new Body(['parameterName' => 'b', 'denormalizationType' => 'type_b', 'optional' => false]),
], new ReflectionMethod(self::class, 'fixtureMethod'));
}
diff --git a/tests/Unit/Service/RoutingLoader/RoutingAttributeLoaderTest.php b/tests/Unit/Service/RoutingLoader/RoutingAttributeLoaderTest.php
new file mode 100644
index 0000000..8a5d1fd
--- /dev/null
+++ b/tests/Unit/Service/RoutingLoader/RoutingAttributeLoaderTest.php
@@ -0,0 +1,32 @@
+markTestSkipped('Symfony 6.4 and older read docblock annotations through an annotation reader');
+ }
+
+ $this->expectException(ConfigurationException::class);
+ $this->expectExceptionMessage(
+ DocblockOptionsOnAttributeRouteController::class . '::show() uses docblock annotations of '
+ . 'paysera/lib-api-bundle (@RequiredPermissions), which Symfony 7 does not read. Use the attributes of the '
+ . 'same name from Paysera\Bundle\ApiBundle\Attribute instead.'
+ );
+
+ (new RoutingAttributeLoader())->load(DocblockOptionsOnAttributeRouteController::class);
+ }
+}