diff --git a/composer.json b/composer.json index 4b03f7f8..ded77f2b 100644 --- a/composer.json +++ b/composer.json @@ -15,20 +15,17 @@ ], "require": { "php": "^8.1", - "chubbyphp/chubbyphp-api-http": "^6.0", "chubbyphp/chubbyphp-clean-directories": "^1.3.1", "chubbyphp/chubbyphp-cors": "^1.5", "chubbyphp/chubbyphp-decode-encode": "^1.1", - "chubbyphp/chubbyphp-deserialization": "^4.1", "chubbyphp/chubbyphp-framework": "^5.1.1", "chubbyphp/chubbyphp-framework-router-fastroute": "^2.1", "chubbyphp/chubbyphp-http-exception": "^1.1", "chubbyphp/chubbyphp-laminas-config": "^1.4", "chubbyphp/chubbyphp-laminas-config-doctrine": "^2.1", "chubbyphp/chubbyphp-laminas-config-factory": "^1.3", - "chubbyphp/chubbyphp-negotiation": "^2.0", - "chubbyphp/chubbyphp-serialization": "^4.0", - "chubbyphp/chubbyphp-validation": "^4.0", + "chubbyphp/chubbyphp-negotiation": "^2.1", + "chubbyphp/chubbyphp-parsing": "^1.0@dev", "doctrine/orm": "^2.17.2", "monolog/monolog": "^3.5", "ramsey/uuid": "^4.7.5", diff --git a/config/prod.php b/config/prod.php index 54cca0f7..94ca177d 100644 --- a/config/prod.php +++ b/config/prod.php @@ -2,12 +2,14 @@ declare(strict_types=1); -use App\Factory\Collection\PetCollectionFactory; -use App\Factory\Model\PetFactory; +use App\Enrich\EnrichInterface; use App\Mapping\Orm\PetMapping; use App\Mapping\Orm\VaccinationMapping; +use App\Middleware\ApiExceptionMiddleware; use App\Model\Pet; use App\Model\Vaccination; +use App\Parsing\PetCollectionParsing; +use App\Parsing\PetParsing; use App\Repository\PetRepository; use App\RequestHandler\Api\Crud\CreateRequestHandler; use App\RequestHandler\Api\Crud\DeleteRequestHandler; @@ -20,9 +22,7 @@ use App\ServiceFactory\Command\CommandsFactory; use App\ServiceFactory\DecodeEncode\TypeDecodersFactory; use App\ServiceFactory\DecodeEncode\TypeEncodersFactory; -use App\ServiceFactory\Deserialization\DenormalizationObjectMappingsFactory; -use App\ServiceFactory\Factory\Collection\PetCollectionFactoryFactory; -use App\ServiceFactory\Factory\Model\PetFactoryFactory; +use App\ServiceFactory\Enrich\PetEnrichFactory; use App\ServiceFactory\Framework\ExceptionMiddlewareFactory; use App\ServiceFactory\Framework\MiddlewaresFactory; use App\ServiceFactory\Framework\RouteMatcherFactory; @@ -32,8 +32,12 @@ use App\ServiceFactory\Http\ResponseFactoryFactory; use App\ServiceFactory\Http\StreamFactoryFactory; use App\ServiceFactory\Logger\LoggerFactory; +use App\ServiceFactory\Middleware\ApiExceptionMiddlewareFactory; use App\ServiceFactory\Negotiation\AcceptNegotiatorSupportedMediaTypesFactory; use App\ServiceFactory\Negotiation\ContentTypeNegotiatorSupportedMediaTypesFactory; +use App\ServiceFactory\Parsing\ParserFactory; +use App\ServiceFactory\Parsing\PetCollectionParsingFactory; +use App\ServiceFactory\Parsing\PetParsingFactory; use App\ServiceFactory\Repository\PetRepositoryFactory; use App\ServiceFactory\RequestHandler\Api\Crud\PetCreateRequestHandlerFactory; use App\ServiceFactory\RequestHandler\Api\Crud\PetDeleteRequestHandlerFactory; @@ -42,23 +46,14 @@ use App\ServiceFactory\RequestHandler\Api\Crud\PetUpdateRequestHandlerFactory; use App\ServiceFactory\RequestHandler\PingRequestHandlerFactory; use App\ServiceFactory\RequestHandler\OpenapiRequestHandlerFactory; -use App\ServiceFactory\Serialization\NormalizationObjectMappingsFactory; -use App\ServiceFactory\Validation\ValidationMappingProviderFactory; -use Chubbyphp\ApiHttp\Manager\RequestManagerInterface; -use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface; -use Chubbyphp\ApiHttp\Middleware\AcceptAndContentTypeMiddleware; -use Chubbyphp\ApiHttp\Middleware\ApiExceptionMiddleware; -use Chubbyphp\ApiHttp\ServiceFactory\AcceptAndContentTypeMiddlewareFactory; -use Chubbyphp\ApiHttp\ServiceFactory\ApiExceptionMiddlewareFactory; -use Chubbyphp\ApiHttp\ServiceFactory\RequestManagerFactory; -use Chubbyphp\ApiHttp\ServiceFactory\ResponseManagerFactory; use Chubbyphp\Cors\CorsMiddleware; use Chubbyphp\Cors\ServiceFactory\CorsMiddlewareFactory; +use Chubbyphp\DecodeEncode\Decoder\DecoderInterface; use Chubbyphp\DecodeEncode\Decoder\TypeDecoderInterface; +use Chubbyphp\DecodeEncode\Encoder\EncoderInterface; use Chubbyphp\DecodeEncode\Encoder\TypeEncoderInterface; -use Chubbyphp\Deserialization\DeserializerInterface; -use Chubbyphp\Deserialization\Mapping\DenormalizationObjectMappingInterface; -use Chubbyphp\Deserialization\ServiceFactory\DeserializerFactory; +use Chubbyphp\DecodeEncode\ServiceFactory\DecoderFactory; +use Chubbyphp\DecodeEncode\ServiceFactory\EncoderFactory; use Chubbyphp\Framework\Middleware\ExceptionMiddleware; use Chubbyphp\Framework\Middleware\RouteMatcherMiddleware; use Chubbyphp\Framework\Router\RouteMatcherInterface; @@ -72,16 +67,13 @@ use Chubbyphp\Laminas\Config\Doctrine\ServiceFactory\Persistence\Mapping\Driver\ClassMapDriverFactory; use Chubbyphp\Negotiation\AcceptNegotiatorInterface; use Chubbyphp\Negotiation\ContentTypeNegotiatorInterface; +use Chubbyphp\Negotiation\Middleware\AcceptMiddleware; +use Chubbyphp\Negotiation\Middleware\ContentTypeMiddleware; +use Chubbyphp\Negotiation\ServiceFactory\AcceptMiddlewareFactory; use Chubbyphp\Negotiation\ServiceFactory\AcceptNegotiatorFactory; +use Chubbyphp\Negotiation\ServiceFactory\ContentTypeMiddlewareFactory; use Chubbyphp\Negotiation\ServiceFactory\ContentTypeNegotiatorFactory; -use Chubbyphp\Serialization\Mapping\NormalizationObjectMappingInterface; -use Chubbyphp\Serialization\SerializerInterface; -use Chubbyphp\Serialization\ServiceFactory\SerializerFactory; -use Chubbyphp\Validation\Mapping\ValidationMappingProviderInterface; -use Chubbyphp\Validation\Mapping\ValidationMappingProviderRegistryInterface; -use Chubbyphp\Validation\ServiceFactory\ValidationMappingProviderRegistryFactory; -use Chubbyphp\Validation\ServiceFactory\ValidatorFactory; -use Chubbyphp\Validation\ValidatorInterface; +use Chubbyphp\Parsing\ParserInterface; use Doctrine\DBAL\Tools\Console\ConnectionProvider; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; @@ -116,7 +108,7 @@ EntityManager::class => EntityManagerInterface::class, ], 'factories' => [ - AcceptAndContentTypeMiddleware::class => AcceptAndContentTypeMiddlewareFactory::class, + AcceptMiddleware::class => AcceptMiddlewareFactory::class, AcceptNegotiatorInterface::class . 'supportedMediaTypes[]' => AcceptNegotiatorSupportedMediaTypesFactory::class, AcceptNegotiatorInterface::class => AcceptNegotiatorFactory::class, ApiExceptionMiddleware::class => ApiExceptionMiddlewareFactory::class, @@ -124,42 +116,38 @@ Command::class . '[]' => CommandsFactory::class, Connection::class => ConnectionFactory::class, ConnectionProvider::class => ContainerConnectionProviderFactory::class, + ContentTypeMiddleware::class => ContentTypeMiddlewareFactory::class, ContentTypeNegotiatorInterface::class . 'supportedMediaTypes[]' => ContentTypeNegotiatorSupportedMediaTypesFactory::class, ContentTypeNegotiatorInterface::class => ContentTypeNegotiatorFactory::class, CorsMiddleware::class => CorsMiddlewareFactory::class, - DenormalizationObjectMappingInterface::class . '[]' => DenormalizationObjectMappingsFactory::class, - DeserializerInterface::class => DeserializerFactory::class, + DecoderInterface::class => DecoderFactory::class, + EncoderInterface::class => EncoderFactory::class, EntityManagerInterface::class => EntityManagerFactory::class, EntityManagerProvider::class => ContainerEntityManagerProviderFactory::class, ExceptionMiddleware::class => ExceptionMiddlewareFactory::class, LoggerInterface::class => LoggerFactory::class, MappingDriver::class => ClassMapDriverFactory::class, MiddlewareInterface::class . '[]' => MiddlewaresFactory::class, - NormalizationObjectMappingInterface::class . '[]' => NormalizationObjectMappingsFactory::class, OpenapiRequestHandler::class => OpenapiRequestHandlerFactory::class, + ParserInterface::class => ParserFactory::class, Pet::class . CreateRequestHandler::class => PetCreateRequestHandlerFactory::class, Pet::class . DeleteRequestHandler::class => PetDeleteRequestHandlerFactory::class, + Pet::class . EnrichInterface::class => PetEnrichFactory::class, Pet::class . ListRequestHandler::class => PetListRequestHandlerFactory::class, Pet::class . ReadRequestHandler::class => PetReadRequestHandlerFactory::class, Pet::class . UpdateRequestHandler::class => PetUpdateRequestHandlerFactory::class, - PetCollectionFactory::class => PetCollectionFactoryFactory::class, - PetFactory::class => PetFactoryFactory::class, + PetCollectionParsing::class => PetCollectionParsingFactory::class, + PetParsing::class => PetParsingFactory::class, PetRepository::class => PetRepositoryFactory::class, PingRequestHandler::class => PingRequestHandlerFactory::class, - RequestManagerInterface::class => RequestManagerFactory::class, ResponseFactoryInterface::class => ResponseFactoryFactory::class, - ResponseManagerInterface::class => ResponseManagerFactory::class, RouteMatcherInterface::class => RouteMatcherFactory::class, RouteMatcherMiddleware::class => RouteMatcherMiddlewareFactory::class, RoutesByNameInterface::class => RoutesByNameFactory::class, - SerializerInterface::class => SerializerFactory::class, StreamFactoryInterface::class => StreamFactoryFactory::class, TypeDecoderInterface::class . '[]' => TypeDecodersFactory::class, TypeEncoderInterface::class . '[]' => TypeEncodersFactory::class, UrlGeneratorInterface::class => UrlGeneratorFactory::class, - ValidationMappingProviderInterface::class . '[]' => ValidationMappingProviderFactory::class, - ValidationMappingProviderRegistryInterface::class => ValidationMappingProviderRegistryFactory::class, - ValidatorInterface::class => ValidatorFactory::class, ], ], 'directories' => [ diff --git a/src/Collection/AbstractCollection.php b/src/Collection/AbstractCollection.php index 83962e22..e3c2a12b 100644 --- a/src/Collection/AbstractCollection.php +++ b/src/Collection/AbstractCollection.php @@ -106,4 +106,24 @@ final public function getItems(): array { return $this->items; } + + /** + * @return array{offset: int, limit: int, filters: array, sort: array, items: array, count: int} + */ + public function jsonSerialize(): array + { + $items = []; + foreach ($this->items as $item) { + $items[] = $item->jsonSerialize(); + } + + return [ + 'offset' => $this->offset, + 'limit' => $this->limit, + 'filters' => $this->filters, + 'sort' => $this->sort, + 'items' => $items, + 'count' => $this->count, + ]; + } } diff --git a/src/Collection/CollectionInterface.php b/src/Collection/CollectionInterface.php index 3d209e90..632b81b0 100644 --- a/src/Collection/CollectionInterface.php +++ b/src/Collection/CollectionInterface.php @@ -6,7 +6,7 @@ use App\Model\ModelInterface; -interface CollectionInterface +interface CollectionInterface extends \JsonSerializable { public const LIMIT = 20; diff --git a/src/Dto/Collection/CollectionRequestInterface.php b/src/Dto/Collection/CollectionRequestInterface.php new file mode 100644 index 00000000..7b4c09c1 --- /dev/null +++ b/src/Dto/Collection/CollectionRequestInterface.php @@ -0,0 +1,12 @@ + + */ + public array $filters; + + /** + * @var array + */ + public array $sort; + + public function createCollection(): CollectionInterface + { + $collection = new PetCollection(); + $collection->setOffset($this->offset); + $collection->setLimit($this->limit); + $collection->setFilters($this->filters); + $collection->setSort($this->sort); + + return $collection; + } +} diff --git a/src/Dto/Collection/PetCollectionResponse.php b/src/Dto/Collection/PetCollectionResponse.php new file mode 100644 index 00000000..70dd7fdd --- /dev/null +++ b/src/Dto/Collection/PetCollectionResponse.php @@ -0,0 +1,31 @@ + + */ + public array $filters; + + /** + * @var array + */ + public array $sort; + + public PetCollectionResponseEmbedded $_embedded; + + public int $count; + + /** + * @var array + */ + public array $_links; +} diff --git a/src/Dto/Collection/PetCollectionResponseEmbedded.php b/src/Dto/Collection/PetCollectionResponseEmbedded.php new file mode 100644 index 00000000..bf64e403 --- /dev/null +++ b/src/Dto/Collection/PetCollectionResponseEmbedded.php @@ -0,0 +1,13 @@ + + */ + public array $items; +} diff --git a/src/Dto/LinkResponse.php b/src/Dto/LinkResponse.php new file mode 100644 index 00000000..206c9f8c --- /dev/null +++ b/src/Dto/LinkResponse.php @@ -0,0 +1,22 @@ + + */ + public array $rel; + + /** + * @var array + */ + public array $attributes; +} diff --git a/src/Dto/Model/ModelRequestInterface.php b/src/Dto/Model/ModelRequestInterface.php new file mode 100644 index 00000000..3f4d377f --- /dev/null +++ b/src/Dto/Model/ModelRequestInterface.php @@ -0,0 +1,14 @@ + + */ + public array $vaccinations; + + public function createModel(): ModelInterface + { + $vaccinations = []; + foreach ($this->vaccinations as $vaccinationRequest) { + $vaccination = new Vaccination(); + $vaccination->setName($vaccinationRequest->name); + + $vaccinations[] = $vaccination; + } + + $model = new Pet(); + $model->setName($this->name); + $model->setTag($this->tag); + $model->setVaccinations($vaccinations); + + return $model; + } + + /** + * @param Pet $model + */ + public function updateModel(ModelInterface $model): ModelInterface + { + $vaccinations = []; + foreach ($this->vaccinations as $vaccinationRequest) { + $vaccination = new Vaccination(); + $vaccination->setName($vaccinationRequest->name); + + $vaccinations[] = $vaccination; + } + + $model->setUpdatedAt(new \DateTimeImmutable()); + $model->setName($this->name); + $model->setTag($this->tag); + $model->setVaccinations($vaccinations); + + return $model; + } +} diff --git a/src/Dto/Model/PetResponse.php b/src/Dto/Model/PetResponse.php new file mode 100644 index 00000000..fd821e40 --- /dev/null +++ b/src/Dto/Model/PetResponse.php @@ -0,0 +1,32 @@ + + */ + public array $vaccinations; + + public string $_type; + + /** + * @var array + */ + public array $_links; +} diff --git a/src/Dto/Model/VaccinationRequest.php b/src/Dto/Model/VaccinationRequest.php new file mode 100644 index 00000000..5e298f5c --- /dev/null +++ b/src/Dto/Model/VaccinationRequest.php @@ -0,0 +1,10 @@ + + */ + public function enrichModel(ModelInterface $model): array + { + return $this->enrichItem($model->jsonSerialize()); + } + + /** + * @return array + */ + public function enrichCollection(CollectionInterface $collection): array + { + $collectionData = $collection->jsonSerialize(); + + $items = $collectionData['items']; + + unset($collectionData['items']); + + return [ + ...$collectionData, + '_embedded' => [ + 'items' => array_map(fn (array $item) => $this->enrichItem($item), $items), + ], + '_links' => [ + 'create' => ['href' => $this->basePath.'/', 'templated' => false, 'rel' => [], 'attributes' => ['method' => 'POST']], + ], + ]; + } + + private function enrichItem(array $item): array + { + return [ + ...$item, + '_links' => [ + 'read' => ['href' => $this->basePath.'/'.$item['id'], 'templated' => false, 'rel' => [], 'attributes' => ['method' => 'GET']], + 'update' => ['href' => $this->basePath.'/'.$item['id'], 'templated' => false, 'rel' => [], 'attributes' => ['method' => 'PUT']], + 'delete' => ['href' => $this->basePath.'/'.$item['id'], 'templated' => false, 'rel' => [], 'attributes' => ['method' => 'DELETE']], + ], + ]; + } +} diff --git a/src/Enrich/EnrichInterface.php b/src/Enrich/EnrichInterface.php new file mode 100644 index 00000000..8b4a3485 --- /dev/null +++ b/src/Enrich/EnrichInterface.php @@ -0,0 +1,21 @@ + + */ + public function enrichModel(ModelInterface $model): array; + + /** + * @return array + */ + public function enrichCollection(CollectionInterface $collection): array; +} diff --git a/src/Factory/Collection/PetCollectionFactory.php b/src/Factory/Collection/PetCollectionFactory.php deleted file mode 100644 index 9a3f04a9..00000000 --- a/src/Factory/Collection/PetCollectionFactory.php +++ /dev/null @@ -1,16 +0,0 @@ -getClass(); - - return new $class(); - }; - } - - /** - * @return array - */ - public function getDenormalizationFieldMappings(string $path, ?string $type = null): array - { - return [ - $this->denormalizationFieldMappingFactory->createConvertType('offset', ConvertTypeFieldDenormalizer::TYPE_INT), - $this->denormalizationFieldMappingFactory->createConvertType('limit', ConvertTypeFieldDenormalizer::TYPE_INT), - $this->denormalizationFieldMappingFactory->create('filters'), - $this->denormalizationFieldMappingFactory->create('sort'), - ]; - } -} diff --git a/src/Mapping/Deserialization/PetMapping.php b/src/Mapping/Deserialization/PetMapping.php deleted file mode 100644 index a243a109..00000000 --- a/src/Mapping/Deserialization/PetMapping.php +++ /dev/null @@ -1,49 +0,0 @@ -getClass(); - - return new $class(); - }; - } - - /** - * @return array - */ - public function getDenormalizationFieldMappings(string $path, ?string $type = null): array - { - return [ - $this->denormalizationFieldMappingFactory->createConvertType('name', ConvertTypeFieldDenormalizer::TYPE_STRING), - $this->denormalizationFieldMappingFactory->createConvertType('tag', ConvertTypeFieldDenormalizer::TYPE_STRING), - $this->denormalizationFieldMappingFactory->create( - 'vaccinations', - false, - new EmbedManyFieldDenormalizer(Vaccination::class, new MethodAccessor('vaccinations')) - ), - ]; - } -} diff --git a/src/Mapping/Deserialization/VaccinationMapping.php b/src/Mapping/Deserialization/VaccinationMapping.php deleted file mode 100644 index 3b87cd1b..00000000 --- a/src/Mapping/Deserialization/VaccinationMapping.php +++ /dev/null @@ -1,40 +0,0 @@ -getClass(); - - return new $class(); - }; - } - - /** - * @return array - */ - public function getDenormalizationFieldMappings(string $path, ?string $type = null): array - { - return [ - $this->denormalizationFieldMappingFactory->createConvertType('name', ConvertTypeFieldDenormalizer::TYPE_STRING), - ]; - } -} diff --git a/src/Mapping/Serialization/AbstractCollectionMapping.php b/src/Mapping/Serialization/AbstractCollectionMapping.php deleted file mode 100644 index 643ab5b8..00000000 --- a/src/Mapping/Serialization/AbstractCollectionMapping.php +++ /dev/null @@ -1,84 +0,0 @@ - - */ - final public function getNormalizationFieldMappings(string $path): array - { - return [ - NormalizationFieldMappingBuilder::create('offset')->getMapping(), - NormalizationFieldMappingBuilder::create('limit')->getMapping(), - NormalizationFieldMappingBuilder::create('count')->getMapping(), - NormalizationFieldMappingBuilder::create('filters')->getMapping(), - NormalizationFieldMappingBuilder::create('sort')->getMapping(), - ]; - } - - /** - * @return array - */ - final public function getNormalizationEmbeddedFieldMappings(string $path): array - { - return [ - NormalizationFieldMappingBuilder::createEmbedMany('items')->getMapping(), - ]; - } - - /** - * @return array - */ - final public function getNormalizationLinkMappings(string $path): array - { - return [ - NormalizationLinkMappingBuilder::createCallback('list', function ( - string $path, - CollectionInterface $collection, - NormalizerContextInterface $context - ) { - $queryParams = []; - - if (null !== $request = $context->getRequest()) { - $queryParams = $request->getQueryParams(); - } - - /** @var array $queryParams */ - $queryParams = array_merge($queryParams, [ - 'offset' => $collection->getOffset(), - 'limit' => $collection->getLimit(), - ]); - - return LinkBuilder::create( - $this->urlGenerator->generatePath($this->getListRouteName(), [], $queryParams) - ) - ->setAttributes(['method' => 'GET']) - ->getLink() - ; - })->getMapping(), - NormalizationLinkMappingBuilder::createCallback('create', fn () => LinkBuilder::create($this->urlGenerator->generatePath($this->getCreateRouteName())) - ->setAttributes(['method' => 'POST']) - ->getLink())->getMapping(), - ]; - } - - abstract protected function getListRouteName(): string; - - abstract protected function getCreateRouteName(): string; -} diff --git a/src/Mapping/Serialization/AbstractModelMapping.php b/src/Mapping/Serialization/AbstractModelMapping.php deleted file mode 100644 index ab21effb..00000000 --- a/src/Mapping/Serialization/AbstractModelMapping.php +++ /dev/null @@ -1,69 +0,0 @@ - - */ - public function getNormalizationFieldMappings(string $path): array - { - return [ - NormalizationFieldMappingBuilder::create('id')->getMapping(), - NormalizationFieldMappingBuilder::createDateTime('createdAt', \DateTime::ATOM)->getMapping(), - NormalizationFieldMappingBuilder::createDateTime('updatedAt', \DateTime::ATOM)->getMapping(), - ]; - } - - /** - * @return array - */ - final public function getNormalizationEmbeddedFieldMappings(string $path): array - { - return []; - } - - /** - * @return array - */ - final public function getNormalizationLinkMappings(string $path): array - { - return [ - NormalizationLinkMappingBuilder::createCallback('read', fn (string $path, ModelInterface $model) => LinkBuilder::create( - $this->urlGenerator->generatePath($this->getReadRouteName(), ['id' => $model->getId()]) - ) - ->setAttributes(['method' => 'GET']) - ->getLink())->getMapping(), - NormalizationLinkMappingBuilder::createCallback('update', fn (string $path, ModelInterface $model) => LinkBuilder::create( - $this->urlGenerator->generatePath($this->getUpdateRouteName(), ['id' => $model->getId()]) - ) - ->setAttributes(['method' => 'PUT']) - ->getLink())->getMapping(), - NormalizationLinkMappingBuilder::createCallback('delete', fn (string $path, ModelInterface $model) => LinkBuilder::create( - $this->urlGenerator->generatePath($this->getDeleteRouteName(), ['id' => $model->getId()]) - ) - ->setAttributes(['method' => 'DELETE']) - ->getLink())->getMapping(), - ]; - } - - abstract protected function getReadRouteName(): string; - - abstract protected function getUpdateRouteName(): string; - - abstract protected function getDeleteRouteName(): string; -} diff --git a/src/Mapping/Serialization/PetCollectionMapping.php b/src/Mapping/Serialization/PetCollectionMapping.php deleted file mode 100644 index 8edf4f2c..00000000 --- a/src/Mapping/Serialization/PetCollectionMapping.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ - public function getNormalizationFieldMappings(string $path): array - { - $normalizationFieldMappings = parent::getNormalizationFieldMappings($path); - $normalizationFieldMappings[] = NormalizationFieldMappingBuilder::create('name')->getMapping(); - $normalizationFieldMappings[] = NormalizationFieldMappingBuilder::create('tag')->getMapping(); - $normalizationFieldMappings[] = NormalizationFieldMappingBuilder::createEmbedMany('vaccinations')->getMapping(); - - return $normalizationFieldMappings; - } - - protected function getReadRouteName(): string - { - return 'pet_read'; - } - - protected function getUpdateRouteName(): string - { - return 'pet_update'; - } - - protected function getDeleteRouteName(): string - { - return 'pet_delete'; - } -} diff --git a/src/Mapping/Serialization/VaccinationMapping.php b/src/Mapping/Serialization/VaccinationMapping.php deleted file mode 100644 index 9b08f32f..00000000 --- a/src/Mapping/Serialization/VaccinationMapping.php +++ /dev/null @@ -1,50 +0,0 @@ - - */ - public function getNormalizationFieldMappings(string $path): array - { - return [ - NormalizationFieldMappingBuilder::create('name')->getMapping(), - ]; - } - - /** - * @return array - */ - public function getNormalizationEmbeddedFieldMappings(string $path): array - { - return []; - } - - /** - * @return array - */ - public function getNormalizationLinkMappings(string $path): array - { - return []; - } -} diff --git a/src/Mapping/Validation/PetCollectionMapping.php b/src/Mapping/Validation/PetCollectionMapping.php deleted file mode 100644 index f5f82977..00000000 --- a/src/Mapping/Validation/PetCollectionMapping.php +++ /dev/null @@ -1,56 +0,0 @@ - - */ - public function getValidationPropertyMappings(string $path, ?string $type = null): array - { - return [ - ValidationPropertyMappingBuilder::create('offset', [ - new NotBlankConstraint(), - new TypeConstraint('integer'), - ])->getMapping(), - ValidationPropertyMappingBuilder::create('limit', [ - new NotBlankConstraint(), - new TypeConstraint('integer'), - ])->getMapping(), - ValidationPropertyMappingBuilder::create('filters', [ - new MapConstraint([ - 'name' => [ - new NotBlankConstraint(), - new TypeConstraint('string'), - ], - ]), - ])->getMapping(), - ValidationPropertyMappingBuilder::create('sort', [ - new SortConstraint(['name']), - ])->getMapping(), - ]; - } -} diff --git a/src/Mapping/Validation/PetMapping.php b/src/Mapping/Validation/PetMapping.php deleted file mode 100644 index 10c4aaa8..00000000 --- a/src/Mapping/Validation/PetMapping.php +++ /dev/null @@ -1,49 +0,0 @@ - - */ - public function getValidationPropertyMappings(string $path, ?string $type = null): array - { - return [ - ValidationPropertyMappingBuilder::create('name', [ - new NotNullConstraint(), - new NotBlankConstraint(), - new TypeConstraint('string'), - ])->getMapping(), - ValidationPropertyMappingBuilder::create('tag', [ - new NotBlankConstraint(), - new TypeConstraint('string'), - ])->getMapping(), - ValidationPropertyMappingBuilder::create('vaccinations', [ - new ValidConstraint(), - ])->getMapping(), - ]; - } -} diff --git a/src/Mapping/Validation/VaccinationMapping.php b/src/Mapping/Validation/VaccinationMapping.php deleted file mode 100644 index d7c67c27..00000000 --- a/src/Mapping/Validation/VaccinationMapping.php +++ /dev/null @@ -1,41 +0,0 @@ - - */ - public function getValidationPropertyMappings(string $path, ?string $type = null): array - { - return [ - ValidationPropertyMappingBuilder::create('name', [ - new NotNullConstraint(), - new NotBlankConstraint(), - new TypeConstraint('string'), - ])->getMapping(), - ]; - } -} diff --git a/src/Middleware/ApiExceptionMiddleware.php b/src/Middleware/ApiExceptionMiddleware.php new file mode 100644 index 00000000..03476845 --- /dev/null +++ b/src/Middleware/ApiExceptionMiddleware.php @@ -0,0 +1,108 @@ +logger = $logger ?? new NullLogger(); + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + try { + return $handler->handle($request); + } catch (\Throwable $exception) { + return $this->handleException($request, $exception); + } + } + + private function handleException(ServerRequestInterface $request, \Throwable $exception): ResponseInterface + { + $backtrace = $this->backtrace($exception); + + $httpException = $this->exceptionToHttpException($exception, $backtrace); + + $logLevel = $httpException->getStatus() >= 500 ? 'error' : 'info'; + + $this->logger->{$logLevel}('Http Exception', ['backtrace' => $backtrace]); + + if (null === $accept = $request->getAttribute('accept')) { + throw $exception; + } + + $status = $httpException->getStatus(); + + $response = $this->responseFactory->createResponse($status) + ->withHeader('Content-Type', str_replace('/', '/problem+', $accept)) + ; + + $data = $httpException->jsonSerialize(); + $data['_type'] = 'apiProblem'; + + $body = $this->encoder->encode($data, $accept); + + $response->getBody()->write($body); + + return $response; + } + + /** + * @return array> + */ + private function backtrace(\Throwable $exception): array + { + $exceptions = []; + do { + $exceptions[] = [ + 'class' => $exception::class, + 'message' => $exception->getMessage(), + 'code' => $exception->getCode(), + 'file' => $exception->getFile(), + 'line' => $exception->getLine(), + 'trace' => $exception->getTraceAsString(), + ]; + } while ($exception = $exception->getPrevious()); + + return $exceptions; + } + + /** + * @param array> $backtrace + */ + private function exceptionToHttpException(\Throwable $exception, array $backtrace): HttpExceptionInterface + { + if ($exception instanceof HttpExceptionInterface) { + return $exception; + } + + if (!$this->debug) { + return HttpException::createInternalServerError(); + } + + return HttpException::createInternalServerError([ + 'detail' => $exception->getMessage(), + 'backtrace' => $backtrace, + ]); + } +} diff --git a/src/Model/ModelInterface.php b/src/Model/ModelInterface.php index 3742af10..4c4dcd29 100644 --- a/src/Model/ModelInterface.php +++ b/src/Model/ModelInterface.php @@ -4,7 +4,7 @@ namespace App\Model; -interface ModelInterface +interface ModelInterface extends \JsonSerializable { public function getId(): string; diff --git a/src/Model/Pet.php b/src/Model/Pet.php index c341ec8d..8ce07efc 100644 --- a/src/Model/Pet.php +++ b/src/Model/Pet.php @@ -106,4 +106,21 @@ public function getVaccinations(): array { return $this->vaccinations->getValues(); } + + public function jsonSerialize(): array + { + $vaccinations = []; + foreach ($this->vaccinations as $vaccination) { + $vaccinations[] = $vaccination->jsonSerialize(); + } + + return [ + 'id' => $this->id, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + 'name' => $this->name, + 'tag' => $this->tag, + 'vaccinations' => $vaccinations, + ]; + } } diff --git a/src/Model/Vaccination.php b/src/Model/Vaccination.php index a5087ea3..70fe1fe0 100644 --- a/src/Model/Vaccination.php +++ b/src/Model/Vaccination.php @@ -6,7 +6,7 @@ use Ramsey\Uuid\Uuid; -final class Vaccination +final class Vaccination implements \JsonSerializable { private string $id; @@ -38,4 +38,11 @@ public function setPet(?Pet $pet): void { $this->pet = $pet; } + + public function jsonSerialize(): array + { + return [ + 'name' => $this->name, + ]; + } } diff --git a/src/Parsing/ParsingInterface.php b/src/Parsing/ParsingInterface.php new file mode 100644 index 00000000..b730d88d --- /dev/null +++ b/src/Parsing/ParsingInterface.php @@ -0,0 +1,14 @@ +parser; + + return $p->object([ + 'offset' => $p->union([$p->string()->default('0')->toInt(), $p->int()->default(0)]), + 'limit' => $p->union([ + $p->string()->default((string) CollectionInterface::LIMIT)->toInt(), + $p->int()->default(CollectionInterface::LIMIT), + ]), + 'filters' => $p->record($p->string())->default([]), + 'sort' => $p->record($p->string())->default([]), + ], PetCollectionRequest::class); + } + + public function getResponseSchema(): SchemaInterface + { + $p = $this->parser; + + return $p->object([ + 'offset' => $p->int(), + 'limit' => $p->int(), + 'filters' => $p->record($p->string()), + 'sort' => $p->record($p->string()), + '_embedded' => $p->object(['items' => $p->array($this->petParsing->getResponseSchema())], PetCollectionResponseEmbedded::class), + 'count' => $p->int(), + '_links' => $p->record( + $p->object([ + 'href' => $p->string(), + 'attributes' => $p->array($p->string()), + ], LinkResponse::class) + ), + ], PetCollectionResponse::class); + } +} diff --git a/src/Parsing/PetParsing.php b/src/Parsing/PetParsing.php new file mode 100644 index 00000000..352194e9 --- /dev/null +++ b/src/Parsing/PetParsing.php @@ -0,0 +1,56 @@ +parser; + + return $p->object([ + 'name' => $p->string()->minLength(1), + 'tag' => $p->string()->minLength(1)->nullable(), + 'vaccinations' => $p->array($p->object([ + 'name' => $p->string(), + ], VaccinationRequest::class)->ignore(['_type']))->default([]), + ], PetRequest::class)->ignore(['id', 'createdAt', 'updatedAt', '_type', '_links']); + } + + public function getResponseSchema(): SchemaInterface + { + $p = $this->parser; + + return $p->object([ + 'id' => $p->string(), + 'createdAt' => $p->dateTime()->toString(), + 'updatedAt' => $p->dateTime()->nullable()->toString(), + 'name' => $p->string(), + 'tag' => $p->string()->nullable(), + 'vaccinations' => $p->array($p->object([ + 'name' => $p->string(), + '_type' => $p->literal('vaccination')->default('vaccination'), + ], VaccinationResponse::class)), + '_type' => $p->literal('pet')->default('pet'), + '_links' => $p->record( + $p->object(['href' => $p->string(), + 'templated' => $p->bool(), + 'rel' => $p->array($p->string()), + 'attributes' => $p->record($p->string()), + ], LinkResponse::class) + ), + ], PetResponse::class); + } +} diff --git a/src/RequestHandler/Api/Crud/CreateRequestHandler.php b/src/RequestHandler/Api/Crud/CreateRequestHandler.php index 820d7453..f0818d9d 100644 --- a/src/RequestHandler/Api/Crud/CreateRequestHandler.php +++ b/src/RequestHandler/Api/Crud/CreateRequestHandler.php @@ -4,18 +4,15 @@ namespace App\RequestHandler\Api\Crud; -use App\Factory\ModelFactoryInterface; -use App\Model\ModelInterface; +use App\Dto\Model\ModelRequestInterface; +use App\Enrich\EnrichInterface; +use App\Parsing\ParsingInterface; use App\Repository\RepositoryInterface; -use Chubbyphp\ApiHttp\Manager\RequestManagerInterface; -use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface; -use Chubbyphp\Deserialization\Denormalizer\DenormalizerContextBuilder; -use Chubbyphp\Deserialization\Denormalizer\DenormalizerContextInterface; +use Chubbyphp\DecodeEncode\Decoder\DecoderInterface; +use Chubbyphp\DecodeEncode\Encoder\EncoderInterface; use Chubbyphp\HttpException\HttpException; -use Chubbyphp\Serialization\Normalizer\NormalizerContextBuilder; -use Chubbyphp\Serialization\Normalizer\NormalizerContextInterface; -use Chubbyphp\Validation\Error\ApiProblemErrorMessages; -use Chubbyphp\Validation\ValidatorInterface; +use Chubbyphp\Parsing\ParserErrorException; +use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -23,11 +20,12 @@ final class CreateRequestHandler implements RequestHandlerInterface { public function __construct( - private ModelFactoryInterface $factory, + private DecoderInterface $decoder, + private ParsingInterface $parsing, private RepositoryInterface $repository, - private RequestManagerInterface $requestManager, - private ResponseManagerInterface $responseManager, - private ValidatorInterface $validator + private EnrichInterface $enrich, + private EncoderInterface $encoder, + private ResponseFactoryInterface $responseFactory, ) {} public function handle(ServerRequestInterface $request): ResponseInterface @@ -35,31 +33,28 @@ public function handle(ServerRequestInterface $request): ResponseInterface $accept = $request->getAttribute('accept'); $contentType = $request->getAttribute('contentType'); - /** @var ModelInterface $model */ - $model = $this->requestManager->getDataFromRequestBody( - $request, - $this->factory->create(), - $contentType, - $this->getDenormalizerContext() - ); + $input = $this->decoder->decode((string) $request->getBody(), $contentType); - if ([] !== $errors = $this->validator->validate($model)) { - throw HttpException::createUnprocessableEntity(['invalidParameters' => (new ApiProblemErrorMessages($errors))->getMessages()]); + try { + /** @var ModelRequestInterface $modelRequest */ + $modelRequest = $this->parsing->getRequestSchema()->parse($input); + } catch (ParserErrorException $e) { + throw HttpException::createUnprocessableEntity(['invalidParameters' => $e->getApiProblemErrorMessages()]); } + $model = $modelRequest->createModel(); + $this->repository->persist($model); $this->repository->flush(); - return $this->responseManager->create($model, $accept, 201, $this->getNormalizerContext($request)); - } + $output = $this->encoder->encode( + (array) $this->parsing->getResponseSchema()->parse($this->enrich->enrichModel($model)), + $accept + ); - private function getDenormalizerContext(): DenormalizerContextInterface - { - return DenormalizerContextBuilder::create()->setClearMissing(true)->getContext(); - } + $response = $this->responseFactory->createResponse(201)->withHeader('Content-Type', $accept); + $response->getBody()->write($output); - private function getNormalizerContext(ServerRequestInterface $request): NormalizerContextInterface - { - return NormalizerContextBuilder::create()->setRequest($request)->getContext(); + return $response; } } diff --git a/src/RequestHandler/Api/Crud/DeleteRequestHandler.php b/src/RequestHandler/Api/Crud/DeleteRequestHandler.php index d370e51d..834e3d68 100644 --- a/src/RequestHandler/Api/Crud/DeleteRequestHandler.php +++ b/src/RequestHandler/Api/Crud/DeleteRequestHandler.php @@ -5,18 +5,18 @@ namespace App\RequestHandler\Api\Crud; use App\Repository\RepositoryInterface; -use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface; use Chubbyphp\HttpException\HttpException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Ramsey\Uuid\Uuid; +use Slim\Psr7\Factory\ResponseFactory; final class DeleteRequestHandler implements RequestHandlerInterface { public function __construct( private RepositoryInterface $repository, - private ResponseManagerInterface $responseManager + private ResponseFactory $responseFactory ) {} public function handle(ServerRequestInterface $request): ResponseInterface @@ -31,6 +31,6 @@ public function handle(ServerRequestInterface $request): ResponseInterface $this->repository->remove($model); $this->repository->flush(); - return $this->responseManager->createEmpty($accept); + return $this->responseFactory->createResponse(204); } } diff --git a/src/RequestHandler/Api/Crud/ListRequestHandler.php b/src/RequestHandler/Api/Crud/ListRequestHandler.php index 66ac1367..bc646bd9 100644 --- a/src/RequestHandler/Api/Crud/ListRequestHandler.php +++ b/src/RequestHandler/Api/Crud/ListRequestHandler.php @@ -4,15 +4,15 @@ namespace App\RequestHandler\Api\Crud; -use App\Collection\CollectionInterface; -use App\Factory\CollectionFactoryInterface; +use App\Dto\Collection\CollectionRequestInterface; +use App\Enrich\EnrichInterface; +use App\Parsing\ParsingInterface; use App\Repository\RepositoryInterface; -use Chubbyphp\ApiHttp\Manager\RequestManagerInterface; -use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface; +use Chubbyphp\DecodeEncode\Decoder\DecoderInterface; +use Chubbyphp\DecodeEncode\Encoder\EncoderInterface; use Chubbyphp\HttpException\HttpException; -use Chubbyphp\Serialization\Normalizer\NormalizerContextBuilder; -use Chubbyphp\Validation\Error\ApiProblemErrorMessages; -use Chubbyphp\Validation\ValidatorInterface; +use Chubbyphp\Parsing\ParserErrorException; +use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -20,30 +20,39 @@ final class ListRequestHandler implements RequestHandlerInterface { public function __construct( - private CollectionFactoryInterface $factory, + private DecoderInterface $decoder, + private ParsingInterface $parsing, private RepositoryInterface $repository, - private RequestManagerInterface $requestManager, - private ResponseManagerInterface $responseManager, - private ValidatorInterface $validator + private EnrichInterface $enrich, + private EncoderInterface $encoder, + private ResponseFactoryInterface $responseFactory, ) {} public function handle(ServerRequestInterface $request): ResponseInterface { $accept = $request->getAttribute('accept'); - /** @var CollectionInterface $collection */ - $collection = $this->requestManager->getDataFromRequestQuery($request, $this->factory->create()); + $input = $request->getQueryParams(); - if ([] !== $errors = $this->validator->validate($collection)) { - throw HttpException::createBadRequest([ - 'invalidParameters' => (new ApiProblemErrorMessages($errors))->getMessages(), - ]); + try { + /** @var CollectionRequestInterface $collectionRequest */ + $collectionRequest = $this->parsing->getRequestSchema()->parse($input); + } catch (ParserErrorException $e) { + throw HttpException::createBadRequest(['invalidParameters' => $e->getApiProblemErrorMessages()]); } + $collection = $collectionRequest->createCollection(); + $this->repository->resolveCollection($collection); - $context = NormalizerContextBuilder::create()->setRequest($request)->getContext(); + $output = $this->encoder->encode( + (array) $this->parsing->getResponseSchema()->parse($this->enrich->enrichCollection($collection)), + $accept + ); + + $response = $this->responseFactory->createResponse(200)->withHeader('Content-Type', $accept); + $response->getBody()->write($output); - return $this->responseManager->create($collection, $accept, 200, $context); + return $response; } } diff --git a/src/RequestHandler/Api/Crud/ReadRequestHandler.php b/src/RequestHandler/Api/Crud/ReadRequestHandler.php index 3c4cc671..9ee11198 100644 --- a/src/RequestHandler/Api/Crud/ReadRequestHandler.php +++ b/src/RequestHandler/Api/Crud/ReadRequestHandler.php @@ -4,11 +4,14 @@ namespace App\RequestHandler\Api\Crud; +use App\Enrich\EnrichInterface; +use App\Parsing\ParsingInterface; use App\Repository\RepositoryInterface; -use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface; +use Chubbyphp\DecodeEncode\Encoder\EncoderInterface; use Chubbyphp\HttpException\HttpException; use Chubbyphp\Serialization\Normalizer\NormalizerContextBuilder; use Chubbyphp\Serialization\Normalizer\NormalizerContextInterface; +use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -17,8 +20,11 @@ final class ReadRequestHandler implements RequestHandlerInterface { public function __construct( + private ParsingInterface $parsing, private RepositoryInterface $repository, - private ResponseManagerInterface $responseManager + private EnrichInterface $enrich, + private EncoderInterface $encoder, + private ResponseFactoryInterface $responseFactory, ) {} public function handle(ServerRequestInterface $request): ResponseInterface @@ -30,7 +36,15 @@ public function handle(ServerRequestInterface $request): ResponseInterface throw HttpException::createNotFound(); } - return $this->responseManager->create($model, $accept, 200, $this->getNormalizerContext($request)); + $output = $this->encoder->encode( + (array) $this->parsing->getResponseSchema()->parse($this->enrich->enrichModel($model)), + $accept + ); + + $response = $this->responseFactory->createResponse(200)->withHeader('Content-Type', $accept); + $response->getBody()->write($output); + + return $response; } private function getNormalizerContext(ServerRequestInterface $request): NormalizerContextInterface diff --git a/src/RequestHandler/Api/Crud/UpdateRequestHandler.php b/src/RequestHandler/Api/Crud/UpdateRequestHandler.php index 6690580f..6c61e2af 100644 --- a/src/RequestHandler/Api/Crud/UpdateRequestHandler.php +++ b/src/RequestHandler/Api/Crud/UpdateRequestHandler.php @@ -4,17 +4,15 @@ namespace App\RequestHandler\Api\Crud; -use App\Model\ModelInterface; +use App\Dto\ModelRequestInterface; +use App\Enrich\EnrichInterface; +use App\Parsing\ParsingInterface; use App\Repository\RepositoryInterface; -use Chubbyphp\ApiHttp\Manager\RequestManagerInterface; -use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface; -use Chubbyphp\Deserialization\Denormalizer\DenormalizerContextBuilder; -use Chubbyphp\Deserialization\Denormalizer\DenormalizerContextInterface; +use Chubbyphp\DecodeEncode\Decoder\DecoderInterface; +use Chubbyphp\DecodeEncode\Encoder\EncoderInterface; use Chubbyphp\HttpException\HttpException; -use Chubbyphp\Serialization\Normalizer\NormalizerContextBuilder; -use Chubbyphp\Serialization\Normalizer\NormalizerContextInterface; -use Chubbyphp\Validation\Error\ApiProblemErrorMessages; -use Chubbyphp\Validation\ValidatorInterface; +use Chubbyphp\Parsing\ParserErrorException; +use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -23,10 +21,12 @@ final class UpdateRequestHandler implements RequestHandlerInterface { public function __construct( + private DecoderInterface $decoder, + private ParsingInterface $parsing, private RepositoryInterface $repository, - private RequestManagerInterface $requestManager, - private ResponseManagerInterface $responseManager, - private ValidatorInterface $validator + private EnrichInterface $enrich, + private EncoderInterface $encoder, + private ResponseFactoryInterface $responseFactory, ) {} public function handle(ServerRequestInterface $request): ResponseInterface @@ -39,39 +39,28 @@ public function handle(ServerRequestInterface $request): ResponseInterface throw HttpException::createNotFound(); } - /** @var ModelInterface $model */ - $model = $this->requestManager->getDataFromRequestBody( - $request, - $model, - $contentType, - $this->getDenormalizerContext() - ); + $input = $this->decoder->decode((string) $request->getBody(), $contentType); - if ([] !== $errors = $this->validator->validate($model)) { - throw HttpException::createUnprocessableEntity([ - 'invalidParameters' => (new ApiProblemErrorMessages($errors))->getMessages(), - ]); + try { + /** @var ModelRequestInterface $modelRequest */ + $modelRequest = $this->parsing->getRequestSchema()->parse($input); + } catch (ParserErrorException $e) { + throw HttpException::createUnprocessableEntity(['invalidParameters' => $e->getApiProblemErrorMessages()]); } - $model->setUpdatedAt(new \DateTimeImmutable()); + $model = $modelRequest->updateModel($model); $this->repository->persist($model); $this->repository->flush(); - return $this->responseManager->create($model, $accept, 200, $this->getNormalizerContext($request)); - } + $output = $this->encoder->encode( + (array) $this->parsing->getResponseSchema()->parse($this->enrich->enrichModel($model)), + $accept + ); - private function getDenormalizerContext(): DenormalizerContextInterface - { - return DenormalizerContextBuilder::create() - ->setAllowedAdditionalFields(['id', 'createdAt', 'updatedAt', '_links']) - ->setClearMissing(true) - ->getContext() - ; - } + $response = $this->responseFactory->createResponse(200)->withHeader('Content-Type', $accept); + $response->getBody()->write($output); - private function getNormalizerContext(ServerRequestInterface $request): NormalizerContextInterface - { - return NormalizerContextBuilder::create()->setRequest($request)->getContext(); + return $response; } } diff --git a/src/ServiceFactory/Deserialization/DenormalizationObjectMappingsFactory.php b/src/ServiceFactory/Deserialization/DenormalizationObjectMappingsFactory.php deleted file mode 100644 index 8952d99c..00000000 --- a/src/ServiceFactory/Deserialization/DenormalizationObjectMappingsFactory.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ - public function __invoke(ContainerInterface $container): array - { - /** @var DenormalizationFieldMappingFactoryInterface $denormalizationFieldMappingFactory */ - $denormalizationFieldMappingFactory = $this->resolveDependency($container, DenormalizationFieldMappingFactoryInterface::class, DenormalizationFieldMappingFactoryFactory::class); - - return [ - new PetCollectionMapping($denormalizationFieldMappingFactory), - new PetMapping($denormalizationFieldMappingFactory), - new VaccinationMapping($denormalizationFieldMappingFactory), - ]; - } -} diff --git a/src/ServiceFactory/Enrich/PetEnrichFactory.php b/src/ServiceFactory/Enrich/PetEnrichFactory.php new file mode 100644 index 00000000..9c531ba0 --- /dev/null +++ b/src/ServiceFactory/Enrich/PetEnrichFactory.php @@ -0,0 +1,18 @@ +getRoutes() ); } diff --git a/src/ServiceFactory/Middleware/ApiExceptionMiddlewareFactory.php b/src/ServiceFactory/Middleware/ApiExceptionMiddlewareFactory.php new file mode 100644 index 00000000..b10a871b --- /dev/null +++ b/src/ServiceFactory/Middleware/ApiExceptionMiddlewareFactory.php @@ -0,0 +1,37 @@ + + */ + public function __invoke(ContainerInterface $container): ApiExceptionMiddleware + { + $debug = $container->get('config')['debug']; + + /** @var ResponseFactory $responseFactory */ + $responseFactory = $this->resolveDependency($container, ResponseFactory::class, ResponseFactoryFactory::class); + + /** @var EncoderInterface $encoder */ + $encoder = $this->resolveDependency($container, EncoderInterface::class, EncoderFactory::class); + + /** @var LoggerInterface $logger */ + $logger = $this->resolveDependency($container, LoggerInterface::class, LoggerFactory::class); + + return new ApiExceptionMiddleware($responseFactory, $encoder, $debug, $logger); + } +} diff --git a/src/ServiceFactory/Parsing/ParserFactory.php b/src/ServiceFactory/Parsing/ParserFactory.php new file mode 100644 index 00000000..2e52462c --- /dev/null +++ b/src/ServiceFactory/Parsing/ParserFactory.php @@ -0,0 +1,18 @@ +resolveDependency($container, Parser::class, ParserFactory::class); + + /** @var PetParsing $petParsing */ + $petParsing = $this->resolveDependency($container, PetParsing::class, PetParsingFactory::class); + + return new PetCollectionParsing($parser, $petParsing); + } +} diff --git a/src/ServiceFactory/Parsing/PetParsingFactory.php b/src/ServiceFactory/Parsing/PetParsingFactory.php new file mode 100644 index 00000000..a714eafc --- /dev/null +++ b/src/ServiceFactory/Parsing/PetParsingFactory.php @@ -0,0 +1,21 @@ +resolveDependency($container, Parser::class, ParserFactory::class); + + return new PetParsing($parser); + } +} diff --git a/src/ServiceFactory/RequestHandler/Api/Crud/PetCreateRequestHandlerFactory.php b/src/ServiceFactory/RequestHandler/Api/Crud/PetCreateRequestHandlerFactory.php index 1e83dc35..a4340aac 100644 --- a/src/ServiceFactory/RequestHandler/Api/Crud/PetCreateRequestHandlerFactory.php +++ b/src/ServiceFactory/RequestHandler/Api/Crud/PetCreateRequestHandlerFactory.php @@ -4,24 +4,27 @@ namespace App\ServiceFactory\RequestHandler\Api\Crud; -use App\Factory\Model\PetFactory; +use App\Enrich\EnrichInterface; +use App\Model\Pet; +use App\Parsing\PetParsing; use App\Repository\PetRepository; use App\RequestHandler\Api\Crud\CreateRequestHandler; -use Chubbyphp\ApiHttp\Manager\RequestManagerInterface; -use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface; -use Chubbyphp\Validation\ValidatorInterface; +use Chubbyphp\DecodeEncode\Decoder\DecoderInterface; +use Chubbyphp\DecodeEncode\Encoder\EncoderInterface; use Psr\Container\ContainerInterface; +use Psr\Http\Message\ResponseFactoryInterface; final class PetCreateRequestHandlerFactory { public function __invoke(ContainerInterface $container): CreateRequestHandler { return new CreateRequestHandler( - $container->get(PetFactory::class), + $container->get(DecoderInterface::class), + $container->get(PetParsing::class), $container->get(PetRepository::class), - $container->get(RequestManagerInterface::class), - $container->get(ResponseManagerInterface::class), - $container->get(ValidatorInterface::class) + $container->get(Pet::class.EnrichInterface::class), + $container->get(EncoderInterface::class), + $container->get(ResponseFactoryInterface::class), ); } } diff --git a/src/ServiceFactory/RequestHandler/Api/Crud/PetDeleteRequestHandlerFactory.php b/src/ServiceFactory/RequestHandler/Api/Crud/PetDeleteRequestHandlerFactory.php index 70322369..ee64861f 100644 --- a/src/ServiceFactory/RequestHandler/Api/Crud/PetDeleteRequestHandlerFactory.php +++ b/src/ServiceFactory/RequestHandler/Api/Crud/PetDeleteRequestHandlerFactory.php @@ -6,8 +6,8 @@ use App\Repository\PetRepository; use App\RequestHandler\Api\Crud\DeleteRequestHandler; -use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface; use Psr\Container\ContainerInterface; +use Psr\Http\Message\ResponseFactoryInterface; final class PetDeleteRequestHandlerFactory { @@ -15,7 +15,7 @@ public function __invoke(ContainerInterface $container): DeleteRequestHandler { return new DeleteRequestHandler( $container->get(PetRepository::class), - $container->get(ResponseManagerInterface::class) + $container->get(ResponseFactoryInterface::class) ); } } diff --git a/src/ServiceFactory/RequestHandler/Api/Crud/PetListRequestHandlerFactory.php b/src/ServiceFactory/RequestHandler/Api/Crud/PetListRequestHandlerFactory.php index ec68b929..909d7d8d 100644 --- a/src/ServiceFactory/RequestHandler/Api/Crud/PetListRequestHandlerFactory.php +++ b/src/ServiceFactory/RequestHandler/Api/Crud/PetListRequestHandlerFactory.php @@ -4,24 +4,27 @@ namespace App\ServiceFactory\RequestHandler\Api\Crud; -use App\Factory\Collection\PetCollectionFactory; +use App\Enrich\EnrichInterface; +use App\Model\Pet; +use App\Parsing\PetCollectionParsing; use App\Repository\PetRepository; use App\RequestHandler\Api\Crud\ListRequestHandler; -use Chubbyphp\ApiHttp\Manager\RequestManagerInterface; -use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface; -use Chubbyphp\Validation\ValidatorInterface; +use Chubbyphp\DecodeEncode\Decoder\DecoderInterface; +use Chubbyphp\DecodeEncode\Encoder\EncoderInterface; use Psr\Container\ContainerInterface; +use Psr\Http\Message\ResponseFactoryInterface; final class PetListRequestHandlerFactory { public function __invoke(ContainerInterface $container): ListRequestHandler { return new ListRequestHandler( - $container->get(PetCollectionFactory::class), + $container->get(DecoderInterface::class), + $container->get(PetCollectionParsing::class), $container->get(PetRepository::class), - $container->get(RequestManagerInterface::class), - $container->get(ResponseManagerInterface::class), - $container->get(ValidatorInterface::class) + $container->get(Pet::class.EnrichInterface::class), + $container->get(EncoderInterface::class), + $container->get(ResponseFactoryInterface::class), ); } } diff --git a/src/ServiceFactory/RequestHandler/Api/Crud/PetReadRequestHandlerFactory.php b/src/ServiceFactory/RequestHandler/Api/Crud/PetReadRequestHandlerFactory.php index 4ce4666c..47c9ee55 100644 --- a/src/ServiceFactory/RequestHandler/Api/Crud/PetReadRequestHandlerFactory.php +++ b/src/ServiceFactory/RequestHandler/Api/Crud/PetReadRequestHandlerFactory.php @@ -4,18 +4,25 @@ namespace App\ServiceFactory\RequestHandler\Api\Crud; +use App\Enrich\EnrichInterface; +use App\Model\Pet; +use App\Parsing\PetParsing; use App\Repository\PetRepository; use App\RequestHandler\Api\Crud\ReadRequestHandler; -use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface; +use Chubbyphp\DecodeEncode\Encoder\EncoderInterface; use Psr\Container\ContainerInterface; +use Psr\Http\Message\ResponseFactoryInterface; final class PetReadRequestHandlerFactory { public function __invoke(ContainerInterface $container): ReadRequestHandler { return new ReadRequestHandler( + $container->get(PetParsing::class), $container->get(PetRepository::class), - $container->get(ResponseManagerInterface::class) + $container->get(Pet::class.EnrichInterface::class), + $container->get(EncoderInterface::class), + $container->get(ResponseFactoryInterface::class), ); } } diff --git a/src/ServiceFactory/RequestHandler/Api/Crud/PetUpdateRequestHandlerFactory.php b/src/ServiceFactory/RequestHandler/Api/Crud/PetUpdateRequestHandlerFactory.php index b092ab8e..ab6486ad 100644 --- a/src/ServiceFactory/RequestHandler/Api/Crud/PetUpdateRequestHandlerFactory.php +++ b/src/ServiceFactory/RequestHandler/Api/Crud/PetUpdateRequestHandlerFactory.php @@ -4,22 +4,27 @@ namespace App\ServiceFactory\RequestHandler\Api\Crud; +use App\Enrich\EnrichInterface; +use App\Model\Pet; +use App\Parsing\PetParsing; use App\Repository\PetRepository; use App\RequestHandler\Api\Crud\UpdateRequestHandler; -use Chubbyphp\ApiHttp\Manager\RequestManagerInterface; -use Chubbyphp\ApiHttp\Manager\ResponseManagerInterface; -use Chubbyphp\Validation\ValidatorInterface; +use Chubbyphp\DecodeEncode\Decoder\DecoderInterface; +use Chubbyphp\DecodeEncode\Encoder\EncoderInterface; use Psr\Container\ContainerInterface; +use Psr\Http\Message\ResponseFactoryInterface; final class PetUpdateRequestHandlerFactory { public function __invoke(ContainerInterface $container): UpdateRequestHandler { return new UpdateRequestHandler( + $container->get(DecoderInterface::class), + $container->get(PetParsing::class), $container->get(PetRepository::class), - $container->get(RequestManagerInterface::class), - $container->get(ResponseManagerInterface::class), - $container->get(ValidatorInterface::class) + $container->get(Pet::class.EnrichInterface::class), + $container->get(EncoderInterface::class), + $container->get(ResponseFactoryInterface::class), ); } } diff --git a/src/ServiceFactory/Serialization/NormalizationObjectMappingsFactory.php b/src/ServiceFactory/Serialization/NormalizationObjectMappingsFactory.php deleted file mode 100644 index 20084d9c..00000000 --- a/src/ServiceFactory/Serialization/NormalizationObjectMappingsFactory.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ - public function __invoke(ContainerInterface $container): array - { - /** @var UrlGeneratorInterface $urlGenerator */ - $urlGenerator = $container->get(UrlGeneratorInterface::class); - - return [ - new PetCollectionMapping($urlGenerator), - new PetMapping($urlGenerator), - new VaccinationMapping(), - ]; - } -} diff --git a/src/ServiceFactory/Validation/ValidationMappingProviderFactory.php b/src/ServiceFactory/Validation/ValidationMappingProviderFactory.php deleted file mode 100644 index 25d1e67d..00000000 --- a/src/ServiceFactory/Validation/ValidationMappingProviderFactory.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ - public function __invoke(): array - { - return [ - new PetCollectionMapping(), - new PetMapping(), - new VaccinationMapping(), - ]; - } -} diff --git a/tests/Integration/PetCrudRequestHandlerTest.php b/tests/Integration/PetCrudRequestHandlerTest.php index 3eba84d3..be45246e 100644 --- a/tests/Integration/PetCrudRequestHandlerTest.php +++ b/tests/Integration/PetCrudRequestHandlerTest.php @@ -23,25 +23,9 @@ public function testCreateWithUnsupportedAccept(): void self::assertSame(406, $response['status']['code'], $response['body'] ?? ''); - self::assertSame('application/problem+json', $response['headers']['content-type'][0]); - - $apiProblem = json_decode($response['body'], true, 512, JSON_THROW_ON_ERROR); + self::assertSame('text/html;charset=utf-8', $response['headers']['content-type'][0]); - self::assertEquals([ - 'type' => 'https://datatracker.ietf.org/doc/html/rfc2616#section-10.4.7', - 'status' => 406, - 'title' => 'Not Acceptable', - 'detail' => 'Not supported accept, supportedValues: "application/json", application/jsonx+xml", application/x-www-form-urlencoded", application/x-yaml"', - 'instance' => null, - 'value' => 'text/html', - 'supportedValues' => [ - 'application/json', - 'application/jsonx+xml', - 'application/x-www-form-urlencoded', - 'application/x-yaml', - ], - '_type' => 'apiProblem', - ], $apiProblem); + self::assertMatchesRegularExpression('/Not supported accept/', $response['body']); } public function testCreateWithUnsupportedContentType(): void @@ -105,8 +89,12 @@ public function testCreateWithValidationError(): void 'invalidParameters' => [ [ 'name' => 'name', - 'reason' => 'constraint.notblank.blank', - 'details' => [], + 'reason' => 'Min length {{min}}, 0 given', + 'details' => [ + '_template' => 'Min length {{min}}, {{given}} given', + 'minLength' => 1, + 'given' => 0, + ], ], ], '_type' => 'apiProblem', @@ -136,25 +124,9 @@ public function testListWithUnsupportedAccept(): void self::assertSame(406, $response['status']['code'], $response['body'] ?? ''); - self::assertSame('application/problem+json', $response['headers']['content-type'][0]); - - $apiProblem = json_decode($response['body'], true, 512, JSON_THROW_ON_ERROR); + self::assertSame('text/html;charset=utf-8', $response['headers']['content-type'][0]); - self::assertEquals([ - 'type' => 'https://datatracker.ietf.org/doc/html/rfc2616#section-10.4.7', - 'status' => 406, - 'title' => 'Not Acceptable', - 'detail' => 'Not supported accept, supportedValues: "application/json", application/jsonx+xml", application/x-www-form-urlencoded", application/x-yaml"', - 'instance' => null, - 'value' => 'text/html', - 'supportedValues' => [ - 'application/json', - 'application/jsonx+xml', - 'application/x-www-form-urlencoded', - 'application/x-yaml', - ], - '_type' => 'apiProblem', - ], $apiProblem); + self::assertMatchesRegularExpression('/Not supported accept/', $response['body']); } public function testListWithValidationError(): void @@ -285,25 +257,9 @@ public function testReadWithUnsupportedAccept(): void self::assertSame(406, $response['status']['code'], $response['body'] ?? ''); - self::assertSame('application/problem+json', $response['headers']['content-type'][0]); - - $apiProblem = json_decode($response['body'], true, 512, JSON_THROW_ON_ERROR); + self::assertSame('text/html;charset=utf-8', $response['headers']['content-type'][0]); - self::assertEquals([ - 'type' => 'https://datatracker.ietf.org/doc/html/rfc2616#section-10.4.7', - 'status' => 406, - 'title' => 'Not Acceptable', - 'detail' => 'Not supported accept, supportedValues: "application/json", application/jsonx+xml", application/x-www-form-urlencoded", application/x-yaml"', - 'instance' => null, - 'value' => 'text/html', - 'supportedValues' => [ - 'application/json', - 'application/jsonx+xml', - 'application/x-www-form-urlencoded', - 'application/x-yaml', - ], - '_type' => 'apiProblem', - ], $apiProblem); + self::assertMatchesRegularExpression('/Not supported accept/', $response['body']); } public function testReadWithNotFound(): void @@ -369,25 +325,9 @@ public function testUpdateWithUnsupportedAccept(): void self::assertSame(406, $response['status']['code'], $response['body'] ?? ''); - self::assertSame('application/problem+json', $response['headers']['content-type'][0]); - - $apiProblem = json_decode($response['body'], true, 512, JSON_THROW_ON_ERROR); + self::assertSame('text/html;charset=utf-8', $response['headers']['content-type'][0]); - self::assertEquals([ - 'type' => 'https://datatracker.ietf.org/doc/html/rfc2616#section-10.4.7', - 'status' => 406, - 'title' => 'Not Acceptable', - 'detail' => 'Not supported accept, supportedValues: "application/json", application/jsonx+xml", application/x-www-form-urlencoded", application/x-yaml"', - 'instance' => null, - 'value' => 'text/html', - 'supportedValues' => [ - 'application/json', - 'application/jsonx+xml', - 'application/x-www-form-urlencoded', - 'application/x-yaml', - ], - '_type' => 'apiProblem', - ], $apiProblem); + self::assertMatchesRegularExpression('/Not supported accept/', $response['body']); } public function testUpdateWithUnsupportedContentType(): void @@ -480,8 +420,12 @@ public function testUpdateWithValidationError(): void 'invalidParameters' => [ [ 'name' => 'name', - 'reason' => 'constraint.notblank.blank', - 'details' => [], + 'reason' => 'Min length {{min}}, 0 given', + 'details' => [ + '_template' => 'Min length {{min}}, {{given}} given', + 'minLength' => 1, + 'given' => 0, + ], ], ], '_type' => 'apiProblem', @@ -552,25 +496,9 @@ public function testDeleteWithUnsupportedAccept(): void self::assertSame(406, $response['status']['code'], $response['body'] ?? ''); - self::assertSame('application/problem+json', $response['headers']['content-type'][0]); + self::assertSame('text/html;charset=utf-8', $response['headers']['content-type'][0]); - $apiProblem = json_decode($response['body'], true, 512, JSON_THROW_ON_ERROR); - - self::assertEquals([ - 'type' => 'https://datatracker.ietf.org/doc/html/rfc2616#section-10.4.7', - 'status' => 406, - 'title' => 'Not Acceptable', - 'detail' => 'Not supported accept, supportedValues: "application/json", application/jsonx+xml", application/x-www-form-urlencoded", application/x-yaml"', - 'instance' => null, - 'value' => 'text/html', - 'supportedValues' => [ - 'application/json', - 'application/jsonx+xml', - 'application/x-www-form-urlencoded', - 'application/x-yaml', - ], - '_type' => 'apiProblem', - ], $apiProblem); + self::assertMatchesRegularExpression('/Not supported accept/', $response['body']); } public function testDeleteWithNotFound(): void