diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4d50e6a..67d5b14 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -150,6 +150,7 @@ jobs: - name: 'Upload to Codecov' uses: codecov/codecov-action@v3 with: + token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml fail_ci_if_error: true flags: unittests diff --git a/DependencyInjection/StfalconApiExtension.php b/DependencyInjection/StfalconApiExtension.php index add6521..1ff78a2 100644 --- a/DependencyInjection/StfalconApiExtension.php +++ b/DependencyInjection/StfalconApiExtension.php @@ -12,6 +12,7 @@ namespace StfalconStudio\ApiBundle\DependencyInjection; +use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ORM\EntityManagerInterface; use StfalconStudio\ApiBundle\Security\JwtBlackListService; use StfalconStudio\ApiBundle\Service\Exception\ResponseProcessor\CustomAppExceptionResponseProcessorInterface; @@ -49,6 +50,9 @@ public function load(array $configs, ContainerBuilder $container): void if (\interface_exists(EntityManagerInterface::class)) { $loader->load('orm.php'); } + if (\interface_exists(DocumentManager::class)) { + $loader->load('odm.php'); + } $container->setParameter('stfalcon_api.api_host', $config['api_host']); $container->setParameter('stfalcon_api.json_schema_dir', $config['json_schema_dir']); diff --git a/Error/BaseErrorNames.php b/Error/BaseErrorNames.php index 64beaf0..3e0e4b3 100644 --- a/Error/BaseErrorNames.php +++ b/Error/BaseErrorNames.php @@ -41,6 +41,9 @@ class BaseErrorNames // 405 public final const METHOD_NOT_ALLOWED = 'method_not_allowed'; + // 406 + public final const NOT_ACCEPTABLE = 'not_acceptable'; + // 409 public final const CONFLICT_TARGET_RESOURCE_UPDATE = 'conflict_target_resource_update'; diff --git a/EventListener/JWT/JwtSubscriber.php b/EventListener/JWT/JwtSubscriber.php index abaf837..dd78719 100644 --- a/EventListener/JWT/JwtSubscriber.php +++ b/EventListener/JWT/JwtSubscriber.php @@ -56,19 +56,12 @@ public static function getSubscribedEvents(): iterable */ public function onAuthenticationFailureResponse(AuthenticationFailureEvent $event): void { - switch (true) { - case $event instanceof JWTInvalidEvent: - $message = 'invalid_jwt_token_message'; - break; - case $event instanceof JWTNotFoundEvent: - $message = 'not_found_jwt_token_message'; - break; - case $event instanceof JWTExpiredEvent: - $message = 'expired_jwt_token_message'; - break; - default: - $message = 'unauthorised_user_message'; - } + $message = match (true) { + $event instanceof JWTInvalidEvent => 'invalid_jwt_token_message', + $event instanceof JWTNotFoundEvent => 'not_found_jwt_token_message', + $event instanceof JWTExpiredEvent => 'expired_jwt_token_message', + default => 'unauthorised_user_message', + }; $data = [ 'error' => BaseErrorNames::UNAUTHORISED_USER, diff --git a/EventListener/Kernel/ApiExceptionFormatterListener.php b/EventListener/Kernel/ApiExceptionFormatterListener.php index bc5b32c..04d02e1 100644 --- a/EventListener/Kernel/ApiExceptionFormatterListener.php +++ b/EventListener/Kernel/ApiExceptionFormatterListener.php @@ -26,6 +26,7 @@ use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; +use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Security\Core\Exception\AccessDeniedException; @@ -95,6 +96,11 @@ public function __invoke(ExceptionEvent $event): void $statusCode = Response::HTTP_METHOD_NOT_ALLOWED; $errorName = BaseErrorNames::METHOD_NOT_ALLOWED; break; + case $e instanceof NotAcceptableHttpException: + $message = 'not_acceptable_exception_message'; + $statusCode = Response::HTTP_NOT_ACCEPTABLE; + $errorName = BaseErrorNames::NOT_ACCEPTABLE; + break; case $e instanceof NotFoundHttpException: $message = $e->getMessage(); $statusCode = $e->getStatusCode(); @@ -127,6 +133,9 @@ public function __invoke(ExceptionEvent $event): void case Response::HTTP_TOO_MANY_REQUESTS: $errorName = BaseErrorNames::HTTP_TOO_MANY_REQUESTS; break; + case Response::HTTP_NOT_ACCEPTABLE: + $errorName = BaseErrorNames::NOT_ACCEPTABLE; + break; case Response::HTTP_INTERNAL_SERVER_ERROR: $errorName = BaseErrorNames::INTERNAL_SERVER_ERROR; diff --git a/EventListener/ODM/Aggregate/AggregatePartListener.php b/EventListener/ODM/Aggregate/AggregatePartListener.php new file mode 100644 index 0000000..4fa04ce --- /dev/null +++ b/EventListener/ODM/Aggregate/AggregatePartListener.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\EventListener\ODM\Aggregate; + +use Doctrine\ODM\MongoDB\Event\OnFlushEventArgs; +use Fresh\DateTime\DateTimeHelper; +use StfalconStudio\ApiBundle\Model\ODM\Aggregate\AggregatePartInterface; +use StfalconStudio\ApiBundle\Model\ODM\Aggregate\AggregateRootInterface; + +/** + * AggregatePartListener. + */ +final class AggregatePartListener +{ + /** @var array */ + private array $aggregateRoots = []; + + /** + * @param DateTimeHelper $dateTimeHelper + */ + public function __construct(private readonly DateTimeHelper $dateTimeHelper) + { + } + + /** + * @param OnFlushEventArgs $eventArgs + */ + public function onFlush(OnFlushEventArgs $eventArgs): void + { + $em = $eventArgs->getDocumentManager(); + $uow = $em->getUnitOfWork(); + + foreach ($uow->getScheduledDocumentUpdates() as $entity) { + $this->processEntity($entity); + } + foreach ($uow->getScheduledDocumentInsertions() as $entity) { + $this->processEntity($entity); + } + foreach ($uow->getScheduledDocumentDeletions() as $entity) { + $this->processEntity($entity); + } + + foreach ($this->aggregateRoots as $aggregateRoot) { + $aggregateRoot->setUpdatedAt($this->dateTimeHelper->getCurrentDatetime()); + $uow->recomputeSingleDocumentChangeSet($em->getClassMetadata(\get_class($aggregateRoot)), $aggregateRoot); + } + + $this->aggregateRoots = []; + } + + /** + * @param object $entity + */ + private function processEntity(object $entity): void + { + if ($entity instanceof AggregatePartInterface) { + $aggregateRoot = $entity->getAggregateRoot(); + + if (!\in_array($aggregateRoot, $this->aggregateRoots, true)) { + $this->aggregateRoots[$aggregateRoot->getId()] = $aggregateRoot; + $this->processEntity($aggregateRoot); // Bubble aggregate root to the top root + } + } + } +} diff --git a/EventListener/ORM/Aggregate/AggregatePartListener.php b/EventListener/ORM/Aggregate/AggregatePartListener.php index 018836e..df58e1a 100644 --- a/EventListener/ORM/Aggregate/AggregatePartListener.php +++ b/EventListener/ORM/Aggregate/AggregatePartListener.php @@ -14,8 +14,8 @@ use Doctrine\ORM\Event\OnFlushEventArgs; use Fresh\DateTime\DateTimeHelper; -use StfalconStudio\ApiBundle\Model\Aggregate\AggregatePartInterface; -use StfalconStudio\ApiBundle\Model\Aggregate\AggregateRootInterface; +use StfalconStudio\ApiBundle\Model\ORM\Aggregate\AggregatePartInterface; +use StfalconStudio\ApiBundle\Model\ORM\Aggregate\AggregateRootInterface; /** * AggregatePartListener. diff --git a/Model/Aggregate/AggregatePartInterface.php b/Model/ODM/Aggregate/AggregatePartInterface.php similarity index 88% rename from Model/Aggregate/AggregatePartInterface.php rename to Model/ODM/Aggregate/AggregatePartInterface.php index c367aca..7453a36 100644 --- a/Model/Aggregate/AggregatePartInterface.php +++ b/Model/ODM/Aggregate/AggregatePartInterface.php @@ -10,7 +10,7 @@ declare(strict_types=1); -namespace StfalconStudio\ApiBundle\Model\Aggregate; +namespace StfalconStudio\ApiBundle\Model\ODM\Aggregate; /** * AggregatePartInterface. diff --git a/Model/Aggregate/AggregateRootInterface.php b/Model/ODM/Aggregate/AggregateRootInterface.php similarity index 66% rename from Model/Aggregate/AggregateRootInterface.php rename to Model/ODM/Aggregate/AggregateRootInterface.php index 08000bc..d843fc6 100644 --- a/Model/Aggregate/AggregateRootInterface.php +++ b/Model/ODM/Aggregate/AggregateRootInterface.php @@ -10,10 +10,10 @@ declare(strict_types=1); -namespace StfalconStudio\ApiBundle\Model\Aggregate; +namespace StfalconStudio\ApiBundle\Model\ODM\Aggregate; -use StfalconStudio\ApiBundle\Model\Timestampable\TimestampableInterface; -use StfalconStudio\ApiBundle\Model\UUID\UuidInterface; +use StfalconStudio\ApiBundle\Model\ODM\Timestampable\TimestampableInterface; +use StfalconStudio\ApiBundle\Model\ODM\UUID\UuidInterface; /** * AggregateRootInterface. diff --git a/Model/ODM/Credentials/CredentialsInterface.php b/Model/ODM/Credentials/CredentialsInterface.php new file mode 100644 index 0000000..f8e50af --- /dev/null +++ b/Model/ODM/Credentials/CredentialsInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Model\ODM\Credentials; + +use StfalconStudio\ApiBundle\Model\Credentials\CredentialsInterface as BaseCredentialsInterface; + +/** + * CredentialsInterface. + */ +interface CredentialsInterface extends BaseCredentialsInterface +{ +} diff --git a/Model/ODM/Credentials/CredentialsTrait.php b/Model/ODM/Credentials/CredentialsTrait.php new file mode 100644 index 0000000..76fd5f3 --- /dev/null +++ b/Model/ODM/Credentials/CredentialsTrait.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Model\ODM\Credentials; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB; + +/** + * CredentialsTrait. + */ +trait CredentialsTrait +{ + #[MongoDB\Field(type: 'date', nullable: true)] + protected ?\DateTimeInterface $credentialsLastChangedAt = null; + + /** + * @param \DateTimeInterface|null $credentialsLastChangedAt + * + * @return self + */ + public function setCredentialsLastChangedAt(?\DateTimeInterface $credentialsLastChangedAt): self + { + $this->credentialsLastChangedAt = $credentialsLastChangedAt; + + return $this; + } + + /** + * @return \DateTimeInterface|null + */ + public function getCredentialsLastChangedAt(): ?\DateTimeInterface + { + return $this->credentialsLastChangedAt; + } +} diff --git a/Model/ODM/Timestampable/TimestampableInterface.php b/Model/ODM/Timestampable/TimestampableInterface.php new file mode 100644 index 0000000..45e763f --- /dev/null +++ b/Model/ODM/Timestampable/TimestampableInterface.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Model\ODM\Timestampable; + +/** + * TimestampableInterface. + */ +interface TimestampableInterface +{ + /** + * @param \DateTimeImmutable|null $createdAt + */ + public function setCreatedAt(\DateTimeInterface $createdAt = null): void; + + /** + * @return \DateTimeInterface|null + */ + public function getCreatedAt(): ?\DateTimeInterface; + + /** + * @param \DateTimeInterface|null $createdAt + */ + public function setUpdatedAt(\DateTimeInterface $createdAt = null): void; + + /** + * @return \DateTimeInterface|null + */ + public function getUpdatedAt(): ?\DateTimeInterface; + + /** + * Init timestampable fields. + */ + public function initTimestampableFields(): void; +} diff --git a/Model/ODM/Timestampable/TimestampableTrait.php b/Model/ODM/Timestampable/TimestampableTrait.php new file mode 100644 index 0000000..72f9979 --- /dev/null +++ b/Model/ODM/Timestampable/TimestampableTrait.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Model\ODM\Timestampable; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB; +use Gedmo\Mapping\Annotation as Gedmo; + +/** + * TimestampableTrait. + */ +trait TimestampableTrait +{ + #[MongoDB\Field(type: 'date_immutable')] + protected \DateTimeInterface|null $createdAt = null; + + #[MongoDB\Field(type: 'date')] + #[Gedmo\Timestampable(on: 'update')] + protected \DateTimeInterface|null $updatedAt = null; + + /** + * @param \DateTimeImmutable|null $createdAt + */ + public function setCreatedAt(\DateTimeInterface $createdAt = null): void + { + $this->createdAt = $createdAt; + } + + /** + * @return \DateTimeImmutable + */ + public function getCreatedAt(): \DateTimeInterface + { + return $this->createdAt; + } + + /** + * @param \DateTimeInterface|null $updatedAt + */ + public function setUpdatedAt(\DateTimeInterface $updatedAt = null): void + { + $this->updatedAt = $updatedAt; + } + + /** + * @return \DateTimeInterface + */ + public function getUpdatedAt(): \DateTimeInterface + { + return $this->updatedAt; + } + + /** + * Init timestampable fields. + */ + public function initTimestampableFields(): void + { + $this->createdAt = new \DateTimeImmutable('now'); + $this->updatedAt = new \DateTime('now'); + } +} diff --git a/Model/ODM/UUID/UuidInterface.php b/Model/ODM/UUID/UuidInterface.php new file mode 100644 index 0000000..52bc76f --- /dev/null +++ b/Model/ODM/UUID/UuidInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Model\ODM\UUID; + +use StfalconStudio\ApiBundle\Model\UUID\UuidInterface as BaseUuidInterface; + +/** + * UuidInterface. + */ +interface UuidInterface extends BaseUuidInterface +{ +} diff --git a/Model/UUID/UuidTrait.php b/Model/ODM/UUID/UuidTrait.php similarity index 89% rename from Model/UUID/UuidTrait.php rename to Model/ODM/UUID/UuidTrait.php index 3c079d0..a17b2eb 100644 --- a/Model/UUID/UuidTrait.php +++ b/Model/ODM/UUID/UuidTrait.php @@ -10,7 +10,7 @@ declare(strict_types=1); -namespace StfalconStudio\ApiBundle\Model\UUID; +namespace StfalconStudio\ApiBundle\Model\ODM\UUID; /** * UuidTrait. diff --git a/Model/ORM/Aggregate/AggregatePartInterface.php b/Model/ORM/Aggregate/AggregatePartInterface.php new file mode 100644 index 0000000..a14b8a8 --- /dev/null +++ b/Model/ORM/Aggregate/AggregatePartInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Model\ORM\Aggregate; + +/** + * AggregatePartInterface. + */ +interface AggregatePartInterface +{ + /** + * @return AggregateRootInterface + */ + public function getAggregateRoot(): AggregateRootInterface; +} diff --git a/Model/ORM/Aggregate/AggregateRootInterface.php b/Model/ORM/Aggregate/AggregateRootInterface.php new file mode 100644 index 0000000..f8ec3b1 --- /dev/null +++ b/Model/ORM/Aggregate/AggregateRootInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Model\ORM\Aggregate; + +use StfalconStudio\ApiBundle\Model\ORM\Timestampable\TimestampableInterface; +use StfalconStudio\ApiBundle\Model\ORM\UUID\UuidInterface; + +/** + * AggregateRootInterface. + */ +interface AggregateRootInterface extends TimestampableInterface, UuidInterface +{ +} diff --git a/Model/ORM/Credentials/CredentialsInterface.php b/Model/ORM/Credentials/CredentialsInterface.php new file mode 100644 index 0000000..4743f62 --- /dev/null +++ b/Model/ORM/Credentials/CredentialsInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Model\ORM\Credentials; + +use StfalconStudio\ApiBundle\Model\Credentials\CredentialsInterface as BaseCredentialsInterface; + +/** + * CredentialsInterface. + */ +interface CredentialsInterface extends BaseCredentialsInterface +{ +} diff --git a/Model/Credentials/CredentialsTrait.php b/Model/ORM/Credentials/CredentialsTrait.php similarity index 94% rename from Model/Credentials/CredentialsTrait.php rename to Model/ORM/Credentials/CredentialsTrait.php index df78aa4..fd721f8 100644 --- a/Model/Credentials/CredentialsTrait.php +++ b/Model/ORM/Credentials/CredentialsTrait.php @@ -10,7 +10,7 @@ declare(strict_types=1); -namespace StfalconStudio\ApiBundle\Model\Credentials; +namespace StfalconStudio\ApiBundle\Model\ORM\Credentials; use Doctrine\ORM\Mapping as ORM; diff --git a/Model/Timestampable/TimestampableInterface.php b/Model/ORM/Timestampable/TimestampableInterface.php similarity index 93% rename from Model/Timestampable/TimestampableInterface.php rename to Model/ORM/Timestampable/TimestampableInterface.php index cf97877..608350f 100644 --- a/Model/Timestampable/TimestampableInterface.php +++ b/Model/ORM/Timestampable/TimestampableInterface.php @@ -10,7 +10,7 @@ declare(strict_types=1); -namespace StfalconStudio\ApiBundle\Model\Timestampable; +namespace StfalconStudio\ApiBundle\Model\ORM\Timestampable; /** * TimestampableInterface. diff --git a/Model/Timestampable/TimestampableTrait.php b/Model/ORM/Timestampable/TimestampableTrait.php similarity index 96% rename from Model/Timestampable/TimestampableTrait.php rename to Model/ORM/Timestampable/TimestampableTrait.php index 593b622..dcee13d 100644 --- a/Model/Timestampable/TimestampableTrait.php +++ b/Model/ORM/Timestampable/TimestampableTrait.php @@ -10,7 +10,7 @@ declare(strict_types=1); -namespace StfalconStudio\ApiBundle\Model\Timestampable; +namespace StfalconStudio\ApiBundle\Model\ORM\Timestampable; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; diff --git a/Model/ORM/UUID/UuidInterface.php b/Model/ORM/UUID/UuidInterface.php new file mode 100644 index 0000000..3d86af6 --- /dev/null +++ b/Model/ORM/UUID/UuidInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Model\ORM\UUID; + +use StfalconStudio\ApiBundle\Model\UUID\UuidInterface as BaseUuidInterface; + +/** + * UuidInterface. + */ +interface UuidInterface extends BaseUuidInterface +{ +} diff --git a/Model/ORM/UUID/UuidTrait.php b/Model/ORM/UUID/UuidTrait.php new file mode 100644 index 0000000..0bb6c5d --- /dev/null +++ b/Model/ORM/UUID/UuidTrait.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace StfalconStudio\ApiBundle\Model\ORM\UUID; + +/** + * UuidTrait. + */ +trait UuidTrait +{ + /** + * @return string + */ + public function getId(): string + { + return (string) $this->id; + } +} diff --git a/Model/UUID/UuidInterface.php b/Model/UUID/UuidInterface.php index d06346d..46d0973 100644 --- a/Model/UUID/UuidInterface.php +++ b/Model/UUID/UuidInterface.php @@ -18,7 +18,7 @@ interface UuidInterface { /** - * @return string + * @return string|null */ - public function getId(): string; + public function getId(): ?string; } diff --git a/Resources/config/odm.php b/Resources/config/odm.php new file mode 100644 index 0000000..18e05fd --- /dev/null +++ b/Resources/config/odm.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +use StfalconStudio\ApiBundle\EventListener\ODM\Aggregate\AggregatePartListener; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + +return static function (ContainerConfigurator $containerConfigurator): void { + $services = $containerConfigurator->services(); + + $services + ->defaults() + ->autowire() + ->autoconfigure() + ; + + $services->load('StfalconStudio\ApiBundle\EventListener\ORM\\', __DIR__.'/../../EventListener/ORM'); + + $services->set(AggregatePartListener::class, AggregatePartListener::class)->tag('doctrine.event_listener', ['event' => 'onFlush']); +}; diff --git a/Resources/config/orm.php b/Resources/config/orm.php index d798661..25932c0 100644 --- a/Resources/config/orm.php +++ b/Resources/config/orm.php @@ -10,6 +10,7 @@ declare(strict_types=1); +use StfalconStudio\ApiBundle\EventListener\ORM\Aggregate\AggregatePartListener; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { @@ -23,4 +24,7 @@ $services->load('StfalconStudio\ApiBundle\Service\Repository\\', __DIR__.'/../../Service/Repository'); $services->load('StfalconStudio\ApiBundle\Service\DependentEntity\\', __DIR__.'/../../Service/DependentEntity'); + $services->load('StfalconStudio\ApiBundle\EventListener\ORM\\', __DIR__.'/../../EventListener/ORM'); + + $services->set(AggregatePartListener::class, AggregatePartListener::class)->tag('doctrine.event_listener', ['event' => 'onFlush']); }; diff --git a/Resources/config/services.php b/Resources/config/services.php index 1b48674..4b86cd3 100644 --- a/Resources/config/services.php +++ b/Resources/config/services.php @@ -10,10 +10,11 @@ declare(strict_types=1); +use Doctrine\ORM\EntityManagerInterface; use Fresh\DateTime\DateTimeHelper; use JsonSchema\Validator; use Predis\Client; -use StfalconStudio\ApiBundle\EventListener\ORM\Aggregate\AggregatePartListener; +use StfalconStudio\ApiBundle\Validator\Constraints\Entity\EntityExistsValidator; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Reference; @@ -32,12 +33,14 @@ ->bind('$symfonyConstraintViolationListNormalizer', new Reference('serializer.normalizer.constraint_violation_list')) ; - $services->load('StfalconStudio\ApiBundle\\', __DIR__.'/../../{Asset,Request,Serializer,Service,Util,Validator}/'); + $services->load('StfalconStudio\ApiBundle\\', __DIR__.'/../../{Asset,Request,Serializer,Util,Validator}/'); + $services->load('StfalconStudio\ApiBundle\Service\\', __DIR__.'/../../Service/{AttributeProcessor,DependentEntity,Exception}/'); $services->load('StfalconStudio\ApiBundle\EventListener\Console\\', __DIR__.'/../../EventListener/Console'); $services->load('StfalconStudio\ApiBundle\EventListener\Kernel\\', __DIR__.'/../../EventListener/Kernel'); - $services->load('StfalconStudio\ApiBundle\EventListener\ORM\\', __DIR__.'/../../EventListener/ORM'); + if (!\interface_exists(EntityManagerInterface::class)) { + $services->remove(EntityExistsValidator::class); + } - $services->set(AggregatePartListener::class, AggregatePartListener::class)->tag('doctrine.event_listener', ['event' => 'onFlush']); $services->set(Client::class, Client::class); $services->set(Validator::class, Validator::class); $services->set(DateTimeHelper::class, DateTimeHelper::class); diff --git a/Resources/translations/messages.ar.xlf b/Resources/translations/messages.ar.xlf index 9b971cf..544e096 100644 --- a/Resources/translations/messages.ar.xlf +++ b/Resources/translations/messages.ar.xlf @@ -50,6 +50,10 @@ internal_server_error_exception_message خطأ في الخادم الداخلي. + + not_acceptable_exception_message + غير مقبول + diff --git a/Resources/translations/messages.de.xlf b/Resources/translations/messages.de.xlf new file mode 100644 index 0000000..448def1 --- /dev/null +++ b/Resources/translations/messages.de.xlf @@ -0,0 +1,59 @@ + + + + + + unauthorised_user_message + Schlechte Anmeldedaten. Bitte überprüfen Sie, ob Ihr Login/Passwort richtig eingestellt ist. + + + invalid_jwt_token_message + Schlechte Anmeldedaten. Versuchen Sie erneut, sich anzumelden. + + + not_found_jwt_token_message + Schlechte Anmeldedaten. Versuchen Sie erneut, sich anzumelden. + + + expired_jwt_token_message + Schlechte Anmeldedaten. Versuchen Sie erneut, sich anzumelden. + + + invalid_refresh_token_exception_message + Schlechte Anmeldedaten. Versuchen Sie erneut, sich anzumelden. + + + optimistic_lock_exception_message + Die Ressource wurde aktualisiert. Bitte holen Sie sich die aktuellen Daten. + + + access_denied_exception_message + Zugriff abgelehnt. + + + method_not_allowed_exception_message + Nicht zulässige HTTP-Methode. + + + resource_not_found_exception_message + Ressource nicht gefunden. + + + invalid_json_schema_exception_message + Ihre Anfrage entspricht nicht dem angegebenen JSON-Schema. + + + invalid_entity_exception_message + Die Entität erfüllt die Validierungsregeln nicht. + + + internal_server_error_exception_message + Internal Server Error. + + + not_acceptable_exception_message + Inakzeptabel. + + + + diff --git a/Resources/translations/messages.en.xlf b/Resources/translations/messages.en.xlf index 597ee92..db0e1e7 100644 --- a/Resources/translations/messages.en.xlf +++ b/Resources/translations/messages.en.xlf @@ -50,6 +50,10 @@ internal_server_error_exception_message Internal Server Error. + + not_acceptable_exception_message + Not Acceptable. + diff --git a/Resources/translations/messages.fr.xlf b/Resources/translations/messages.fr.xlf index e33584a..9075f45 100644 --- a/Resources/translations/messages.fr.xlf +++ b/Resources/translations/messages.fr.xlf @@ -1,6 +1,6 @@ - + unauthorised_user_message @@ -50,6 +50,10 @@ internal_server_error_exception_message Erreur Interne Serveur. + + not_acceptable_exception_message + Pas acceptable. + diff --git a/Resources/translations/messages.ru.xlf b/Resources/translations/messages.ru.xlf index a6b6917..1a85f1d 100644 --- a/Resources/translations/messages.ru.xlf +++ b/Resources/translations/messages.ru.xlf @@ -50,6 +50,10 @@ internal_server_error_exception_message Внутренняя ошибка сервера. + + not_acceptable_exception_message + Недопустимый запрос. + diff --git a/Resources/translations/messages.uk.xlf b/Resources/translations/messages.uk.xlf index df113c5..98ed513 100644 --- a/Resources/translations/messages.uk.xlf +++ b/Resources/translations/messages.uk.xlf @@ -50,6 +50,10 @@ internal_server_error_exception_message Внутрішня помилка сервера. + + not_acceptable_exception_message + Не прийнятний запит. + diff --git a/Resources/translations/validators.de.xlf b/Resources/translations/validators.de.xlf new file mode 100644 index 0000000..5f56ae9 --- /dev/null +++ b/Resources/translations/validators.de.xlf @@ -0,0 +1,11 @@ + + + + + + password_does_not_meet_special_requirements + Passwort entspricht nicht der Anforderungen. Es muss einen Kleinbuchstaben, einen Großbuchstaben und eine Ziffer enthalten. + + + + diff --git a/Resources/translations/validators.fr.xlf b/Resources/translations/validators.fr.xlf index 55be2af..0ddd2c9 100644 --- a/Resources/translations/validators.fr.xlf +++ b/Resources/translations/validators.fr.xlf @@ -1,6 +1,6 @@ - + password_does_not_meet_special_requirements diff --git a/Serializer/Normalizer/ConstraintViolationListNormalizer.php b/Serializer/Normalizer/ConstraintViolationListNormalizer.php index f67583a..dd0072b 100644 --- a/Serializer/Normalizer/ConstraintViolationListNormalizer.php +++ b/Serializer/Normalizer/ConstraintViolationListNormalizer.php @@ -63,6 +63,8 @@ public function normalize(mixed $object, string $format = null, array $context = { $result = $this->symfonyConstraintViolationListNormalizer->normalize($object, $format, $context); + $this->removeInternalViolationFields($result); + if (\is_array($result) && \array_key_exists('detail', $result) && $result['detail']) { $messages = explode("\n", $result['detail']); @@ -79,4 +81,18 @@ public function normalize(mixed $object, string $format = null, array $context = return $result; } + + /** + * @param array $data + */ + private function removeInternalViolationFields(array &$data): void + { + if (isset($data['violations'])) { + foreach ($data['violations'] as &$violation) { + unset($violation['template'], $violation['parameters']); + $this->removeInternalViolationFields($violation); + } + unset($violation); + } + } } diff --git a/Tests/EventListener/Kernel/ApiExceptionFormatterListenerTest.php b/Tests/EventListener/Kernel/ApiExceptionFormatterListenerTest.php index ff93d19..06f801b 100644 --- a/Tests/EventListener/Kernel/ApiExceptionFormatterListenerTest.php +++ b/Tests/EventListener/Kernel/ApiExceptionFormatterListenerTest.php @@ -30,6 +30,7 @@ use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; +use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; @@ -649,6 +650,56 @@ public function testOnKernelExceptionMethodNotAllowedHttpException(): void self::assertInstanceOf(MethodNotAllowedHttpException::class, $exceptionEvent->getThrowable()); } + public function testOnKernelExceptionNotAcceptableHttpException(): void + { + $exceptionMessage = 'not_acceptable_exception_message'; + $httpException = new NotAcceptableHttpException($exceptionMessage); + + $exceptionEvent = new ExceptionEvent( + $this->kernel, + $this->request, + HttpKernelInterface::MAIN_REQUEST, + $httpException + ); + + $this->request + ->expects(self::once()) + ->method('getHost') + ->willReturn(self::API_HOST) + ; + + $this->translator + ->expects(self::once()) + ->method('trans') + ->with($exceptionMessage) + ->willReturn($exceptionMessage) + ; + + $message = 'Not Acceptable.'; + $this->serializer + ->expects(self::once()) + ->method('serialize') + ->willReturn(sprintf('{"error":"not_acceptable", "error_description":"%s"}', $message)) + ; + + $this->exceptionResponseProcessor + ->expects(self::never()) + ->method('processResponseForException') + ; + + $json = '{"error":"not_acceptable", "error_description":"Not Acceptable."}'; + $this->exceptionResponseFactory + ->expects(self::once()) + ->method('createJsonResponse') + ->with($json, Response::HTTP_NOT_ACCEPTABLE) + ->willReturn($this->response) + ; + + $this->exceptionFormatterListener->__invoke($exceptionEvent); + + self::assertInstanceOf(NotAcceptableHttpException::class, $exceptionEvent->getThrowable()); + } + public function testOnKernelExceptionDummyHttpException(): void { $exceptionMessage = 'exception_message'; diff --git a/Tests/EventListener/ORM/Aggregate/AggregatePartListenerTest.php b/Tests/EventListener/ORM/Aggregate/AggregatePartListenerTest.php index 27b2358..a5cc5ca 100644 --- a/Tests/EventListener/ORM/Aggregate/AggregatePartListenerTest.php +++ b/Tests/EventListener/ORM/Aggregate/AggregatePartListenerTest.php @@ -20,8 +20,8 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use StfalconStudio\ApiBundle\EventListener\ORM\Aggregate\AggregatePartListener; -use StfalconStudio\ApiBundle\Model\Aggregate\AggregatePartInterface; -use StfalconStudio\ApiBundle\Model\Aggregate\AggregateRootInterface; +use StfalconStudio\ApiBundle\Model\ORM\Aggregate\AggregatePartInterface; +use StfalconStudio\ApiBundle\Model\ORM\Aggregate\AggregateRootInterface; final class AggregatePartListenerTest extends TestCase { diff --git a/Tests/EventListener/Security/DummyUser.php b/Tests/EventListener/Security/DummyUser.php index 2d9f585..f5f8183 100644 --- a/Tests/EventListener/Security/DummyUser.php +++ b/Tests/EventListener/Security/DummyUser.php @@ -13,7 +13,7 @@ namespace StfalconStudio\ApiBundle\Tests\EventListener\Security; use StfalconStudio\ApiBundle\Model\Credentials\CredentialsInterface; -use StfalconStudio\ApiBundle\Model\Credentials\CredentialsTrait; +use StfalconStudio\ApiBundle\Model\ORM\Credentials\CredentialsTrait; use Symfony\Component\Security\Core\User\UserInterface; class DummyUser implements UserInterface, CredentialsInterface diff --git a/Tests/Model/Credentials/DummyCredentialsEntity.php b/Tests/Model/Credentials/DummyCredentialsEntity.php index 0a15184..1e3f213 100644 --- a/Tests/Model/Credentials/DummyCredentialsEntity.php +++ b/Tests/Model/Credentials/DummyCredentialsEntity.php @@ -12,8 +12,8 @@ namespace StfalconStudio\ApiBundle\Tests\Model\Credentials; -use StfalconStudio\ApiBundle\Model\Credentials\CredentialsInterface; -use StfalconStudio\ApiBundle\Model\Credentials\CredentialsTrait; +use StfalconStudio\ApiBundle\Model\ORM\Credentials\CredentialsInterface; +use StfalconStudio\ApiBundle\Model\ORM\Credentials\CredentialsTrait; class DummyCredentialsEntity implements CredentialsInterface { diff --git a/Tests/Model/Timestampable/DummyTimestampableEntity.php b/Tests/Model/Timestampable/DummyTimestampableEntity.php index 2607281..663c1fa 100644 --- a/Tests/Model/Timestampable/DummyTimestampableEntity.php +++ b/Tests/Model/Timestampable/DummyTimestampableEntity.php @@ -12,7 +12,7 @@ namespace StfalconStudio\ApiBundle\Tests\Model\Timestampable; -use StfalconStudio\ApiBundle\Model\Timestampable\TimestampableTrait; +use StfalconStudio\ApiBundle\Model\ORM\Timestampable\TimestampableTrait; class DummyTimestampableEntity { diff --git a/Tests/Model/UUID/DummyUuidEntity.php b/Tests/Model/UUID/DummyUuidEntity.php index a6fc466..d80477d 100644 --- a/Tests/Model/UUID/DummyUuidEntity.php +++ b/Tests/Model/UUID/DummyUuidEntity.php @@ -4,7 +4,7 @@ namespace StfalconStudio\ApiBundle\Tests\Model\UUID; -use StfalconStudio\ApiBundle\Model\UUID\UuidTrait; +use StfalconStudio\ApiBundle\Model\ORM\UUID\UuidTrait; use Symfony\Component\Uid\Uuid; class DummyUuidEntity diff --git a/Tests/Security/DummyUser.php b/Tests/Security/DummyUser.php index a9b72ae..fa35dbc 100644 --- a/Tests/Security/DummyUser.php +++ b/Tests/Security/DummyUser.php @@ -12,8 +12,8 @@ namespace StfalconStudio\ApiBundle\Tests\Security; -use StfalconStudio\ApiBundle\Model\Credentials\CredentialsInterface; -use StfalconStudio\ApiBundle\Model\Credentials\CredentialsTrait; +use StfalconStudio\ApiBundle\Model\ORM\Credentials\CredentialsInterface; +use StfalconStudio\ApiBundle\Model\ORM\Credentials\CredentialsTrait; use Symfony\Component\Security\Core\User\UserInterface; class DummyUser implements UserInterface, CredentialsInterface diff --git a/Tests/Serializer/CircularReferenceHandlerTest.php b/Tests/Serializer/CircularReferenceHandlerTest.php index 147dc08..9d72652 100644 --- a/Tests/Serializer/CircularReferenceHandlerTest.php +++ b/Tests/Serializer/CircularReferenceHandlerTest.php @@ -13,7 +13,7 @@ namespace StfalconStudio\ApiBundle\Tests\Serializer; use PHPUnit\Framework\TestCase; -use StfalconStudio\ApiBundle\Model\UUID\UuidInterface; +use StfalconStudio\ApiBundle\Model\ORM\UUID\UuidInterface; use StfalconStudio\ApiBundle\Serializer\CircularReferenceHandler; final class CircularReferenceHandlerTest extends TestCase diff --git a/Tests/Serializer/Normalizer/ConstraintViolationListNormalizerTest.php b/Tests/Serializer/Normalizer/ConstraintViolationListNormalizerTest.php index 1c79fc5..f34c77c 100644 --- a/Tests/Serializer/Normalizer/ConstraintViolationListNormalizerTest.php +++ b/Tests/Serializer/Normalizer/ConstraintViolationListNormalizerTest.php @@ -49,13 +49,32 @@ public function testNormalize(string $originDetail, string $resultDetail): void ->expects(self::once()) ->method('normalize') ->with($object, $format, $context) - ->willReturn(['detail' => $originDetail]) + ->willReturn([ + 'detail' => $originDetail, + 'violations' => [ + [ + 'propertyPath' => 'test', + 'title' => 'test', + 'type' => 'test', + 'template' => 'test', + 'parameters' => 'test', + ] + ] + ]) ; $result = (array) $this->normalizer->normalize($object, $format, $context); self::assertArrayHasKey('detail', $result); self::assertSame($resultDetail, $result['detail']); + self::assertSame( + [ + 'propertyPath' => 'test', + 'title' => 'test', + 'type' => 'test', + ], + $result['violations'][0], + ); } public static function dataProviderForTestNormalize(): iterable