Added Validation to API requests.
CI / Docker Lint (push) Successful in 21s

- Added Symfony Validation Lib.
- Updated API DTOs to include validation constraints.
This commit is contained in:
2024-10-18 20:32:36 +02:00
parent fbee23c022
commit 37462e4646
11 changed files with 75 additions and 67 deletions
+1
View File
@@ -34,6 +34,7 @@
"symfony/security-bundle": "7.0.*", "symfony/security-bundle": "7.0.*",
"symfony/serializer": "7.0.*", "symfony/serializer": "7.0.*",
"symfony/twig-bundle": "7.0.*", "symfony/twig-bundle": "7.0.*",
"symfony/validator": "7.0.*",
"symfony/var-exporter": "7.0.4", "symfony/var-exporter": "7.0.4",
"symfony/yaml": "7.0.*", "symfony/yaml": "7.0.*",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^2.12|^3.0",
Generated
+9 -8
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "1f7a502767330ce66e9e7f69229604a3", "content-hash": "974cb22b15d6dc64bd6dac49da5d671d",
"packages": [ "packages": [
{ {
"name": "composer/semver", "name": "composer/semver",
@@ -6582,16 +6582,16 @@
}, },
{ {
"name": "symfony/validator", "name": "symfony/validator",
"version": "v7.0.6", "version": "v7.0.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/validator.git", "url": "https://github.com/symfony/validator.git",
"reference": "a2df2c63b7944a162dee86ab8065f2f91b7d6e36" "reference": "b3e4d838cdae9f2882402c2ad8018a27d469c075"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/validator/zipball/a2df2c63b7944a162dee86ab8065f2f91b7d6e36", "url": "https://api.github.com/repos/symfony/validator/zipball/b3e4d838cdae9f2882402c2ad8018a27d469c075",
"reference": "a2df2c63b7944a162dee86ab8065f2f91b7d6e36", "reference": "b3e4d838cdae9f2882402c2ad8018a27d469c075",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -6636,7 +6636,8 @@
"Symfony\\Component\\Validator\\": "" "Symfony\\Component\\Validator\\": ""
}, },
"exclude-from-classmap": [ "exclude-from-classmap": [
"/Tests/" "/Tests/",
"/Resources/bin/"
] ]
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
@@ -6656,7 +6657,7 @@
"description": "Provides tools to validate values", "description": "Provides tools to validate values",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/validator/tree/v7.0.6" "source": "https://github.com/symfony/validator/tree/v7.0.10"
}, },
"funding": [ "funding": [
{ {
@@ -6672,7 +6673,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-03-28T09:20:36+00:00" "time": "2024-07-26T12:31:22+00:00"
}, },
{ {
"name": "symfony/var-dumper", "name": "symfony/var-dumper",
-13
View File
@@ -5,7 +5,6 @@ namespace App\Controller;
use App\ApiErrorException; use App\ApiErrorException;
use App\DTO\ApiErrorMessage; use App\DTO\ApiErrorMessage;
use App\Entity\Booking; use App\Entity\Booking;
use App\Entity\BookingCustomer;
use App\Entity\BookingRoom; use App\Entity\BookingRoom;
use App\Repository\BookingRepository; use App\Repository\BookingRepository;
use App\Repository\RoomCategoryRepository; use App\Repository\RoomCategoryRepository;
@@ -18,8 +17,6 @@ use Doctrine\ORM\EntityManagerInterface;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
@@ -51,10 +48,6 @@ class BookingController extends AbstractController
#[MapRequestPayload()] BookingDetailsRequest $detailsRequest #[MapRequestPayload()] BookingDetailsRequest $detailsRequest
): JsonResponse { ): JsonResponse {
if (\is_null($detailsRequest)) {
throw new ApiErrorException(new ApiErrorMessage(400, ApiErrorMessage::TYPE_INVALID_REQUEST_BODY_FORMAT));
}
$selectedBooking = $repoBooking->findOneBy(["refNumber" => $detailsRequest->refNumber]); $selectedBooking = $repoBooking->findOneBy(["refNumber" => $detailsRequest->refNumber]);
if (\is_null($selectedBooking)) { if (\is_null($selectedBooking)) {
throw new NotFoundHttpException('Booking not found'); throw new NotFoundHttpException('Booking not found');
@@ -87,18 +80,12 @@ class BookingController extends AbstractController
) )
)] )]
public function newBooking( public function newBooking(
Request $request,
EntityManagerInterface $entityManager, EntityManagerInterface $entityManager,
RoomCategoryRepository $repoCat, RoomCategoryRepository $repoCat,
RoomAvailabilityRepository $repoAvailability, RoomAvailabilityRepository $repoAvailability,
PricesService $priceSrv, PricesService $priceSrv,
#[MapRequestPayload()] BookingCreateRequest $createRequest #[MapRequestPayload()] BookingCreateRequest $createRequest
): JsonResponse { ): JsonResponse {
if (\is_null($createRequest)) {
throw new ApiErrorException(
new ApiErrorMessage(400, ApiErrorMessage::TYPE_INVALID_REQUEST_BODY_FORMAT)
);
}
$booking = new Booking(); $booking = new Booking();
try { try {
+4 -38
View File
@@ -4,7 +4,6 @@ namespace App\Controller;
use App\ApiErrorException; use App\ApiErrorException;
use App\DTO\ApiErrorMessage; use App\DTO\ApiErrorMessage;
use App\Repository\FareTableRepository;
use App\Repository\RoomAvailabilityRepository; use App\Repository\RoomAvailabilityRepository;
use App\Repository\RoomCategoryRepository; use App\Repository\RoomCategoryRepository;
use App\Request\SearchAvailabilityPeriodRequest; use App\Request\SearchAvailabilityPeriodRequest;
@@ -14,12 +13,10 @@ use App\Response\SearchPricesForCategoryResponse;
use App\Service\PricesService; use App\Service\PricesService;
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\HttpKernel\Exception\HttpException;
class SearchRoomsController extends AbstractController class SearchRoomsController extends AbstractController
{ {
@@ -50,15 +47,9 @@ class SearchRoomsController extends AbstractController
) )
)] )]
public function getAvailabilityForPeriod( public function getAvailabilityForPeriod(
Request $request,
RoomAvailabilityRepository $repoAvailability, RoomAvailabilityRepository $repoAvailability,
#[MapRequestPayload()] SearchAvailabilityPeriodRequest $searchRequest #[MapRequestPayload()] SearchAvailabilityPeriodRequest $searchRequest
): Response { ): JsonResponse {
if (\is_null($searchRequest)) {
throw new ApiErrorException(
new ApiErrorMessage(400, ApiErrorMessage::TYPE_INVALID_REQUEST_BODY_FORMAT)
);
}
if ($searchRequest->startDate >= $searchRequest->endDate) { if ($searchRequest->startDate >= $searchRequest->endDate) {
throw new ApiErrorException( throw new ApiErrorException(
@@ -78,13 +69,9 @@ class SearchRoomsController extends AbstractController
#[Route('/api/search/availabilityDetails', name: 'app_search_availabilityDetails', methods: ["POST"], format: "json")] #[Route('/api/search/availabilityDetails', name: 'app_search_availabilityDetails', methods: ["POST"], format: "json")]
#[OA\Tag(name: "Searchs")] #[OA\Tag(name: "Searchs")]
public function getAvailabilityForPeriodDetails( public function getAvailabilityForPeriodDetails(
Request $request,
RoomAvailabilityRepository $repoAvailability, RoomAvailabilityRepository $repoAvailability,
#[MapRequestPayload()] SearchAvailabilityPeriodRequest $searchRequest #[MapRequestPayload()] SearchAvailabilityPeriodRequest $searchRequest
): Response { ): JsonResponse {
if (\is_null($searchRequest)) {
throw new \Exception('Invalid Payload', 400);
}
$rs = $repoAvailability->getAvailabilityForPeriodDetails($searchRequest->startDate, $searchRequest->endDate); $rs = $repoAvailability->getAvailabilityForPeriodDetails($searchRequest->startDate, $searchRequest->endDate);
@@ -119,16 +106,10 @@ class SearchRoomsController extends AbstractController
) )
)] )]
public function getPricesForCategoryOnPeriod( public function getPricesForCategoryOnPeriod(
Request $request,
RoomCategoryRepository $repoCat, RoomCategoryRepository $repoCat,
PricesService $priceService, PricesService $priceService,
#[MapRequestPayload()] SearchPricesForCategoryRequest $searchRequest #[MapRequestPayload()] SearchPricesForCategoryRequest $searchRequest
): Response { ): JsonResponse {
if (\is_null($searchRequest)) {
throw new ApiErrorException(
new ApiErrorMessage(400, ApiErrorMessage::TYPE_INVALID_REQUEST_BODY_FORMAT)
);
}
$roomCategory = $repoCat->find($searchRequest->roomCategory); $roomCategory = $repoCat->find($searchRequest->roomCategory);
if ($roomCategory === null) { if ($roomCategory === null) {
@@ -153,19 +134,4 @@ class SearchRoomsController extends AbstractController
return $this->json(["results" => $rs]); return $this->json(["results" => $rs]);
} }
#[Route('/debug/search', name: 'app_search_faresDebug')]
public function faresDebug(Request $request, PricesService $priceService, FareTableRepository $repoFareTable, RoomCategoryRepository $repoCat): Response
{
$fareTable[] = $repoFareTable->find(1);
$fareTable[] = $repoFareTable->find(2);
$cat = $repoCat->find(4);
$startDate = new \DateTime("2024-04-27");
$endDate = new \DateTime("2024-05-02");
$rs = $priceService->getFinalPricesForSearch($cat, $startDate, $endDate);
return $this->json($rs);
}
} }
+6
View File
@@ -5,6 +5,7 @@ namespace App\Entity;
use App\Repository\BookingCustomerRepository; use App\Repository\BookingCustomerRepository;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Ignore; use Symfony\Component\Serializer\Annotation\Ignore;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: BookingCustomerRepository::class)] #[ORM\Entity(repositoryClass: BookingCustomerRepository::class)]
class BookingCustomer class BookingCustomer
@@ -21,15 +22,20 @@ class BookingCustomer
private ?Booking $booking = null; private ?Booking $booking = null;
#[ORM\Column(length: 64)] #[ORM\Column(length: 64)]
#[Assert\NotBlank]
private ?string $name = null; private ?string $name = null;
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
#[Assert\NotBlank]
private ?string $surname = null; private ?string $surname = null;
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
#[Assert\NotBlank]
#[Assert\Email]
private ?string $email = null; private ?string $email = null;
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
#[Assert\NotBlank]
private ?string $phoneNumber = null; private ?string $phoneNumber = null;
public function getId(): ?int public function getId(): ?int
+20 -1
View File
@@ -10,6 +10,7 @@ use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException;
class ApiExceptionSubscriber implements EventSubscriberInterface class ApiExceptionSubscriber implements EventSubscriberInterface
{ {
@@ -34,7 +35,25 @@ class ApiExceptionSubscriber implements EventSubscriberInterface
* low-level exception, which should *not* be exposed * low-level exception, which should *not* be exposed
*/ */
if ($e instanceof HttpExceptionInterface) { if ($e instanceof HttpExceptionInterface) {
$apiErrorMsg->setExtraData('detail', $e->getMessage()); //Check if it's a Validation error.
// When using MapRequestPayload() ValidationErrors throw HttpExceptions.
$prev = $e->getPrevious();
if ($prev instanceof ValidationFailedException) {
//Get validation error details.
$errorList = [];
$violationList = $prev->getViolations();
foreach ($violationList as $violation) {
$errorList[] = [
'field' => $violation->getPropertyPath(),
'message' => $violation->getMessage(),
];
}
$apiErrorMsg->setExtraData('detail', "Validation Error. See errorList for details");
$apiErrorMsg->setExtraData('errorList', $errorList);
} else {
//Get exception error message.
$apiErrorMsg->setExtraData('detail', $e->getMessage());
}
} }
} }
+11 -2
View File
@@ -3,19 +3,26 @@
namespace App\Request; namespace App\Request;
use App\Entity\BookingCustomer; use App\Entity\BookingCustomer;
use DateTimeInterface;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;
class BookingCreateRequest class BookingCreateRequest
{ {
#[OA\Property(description: 'Booking Arrival date')] #[OA\Property(description: 'Booking Arrival date')]
public \DateTime $arrivalDate; #[Assert\NotBlank]
#[Assert\GreaterThanOrEqual(value: 'today')]
public DateTimeInterface $arrivalDate;
#[OA\Property(description: 'Booking departure date')] #[OA\Property(description: 'Booking departure date')]
public \DateTime $departureDate; #[Assert\NotBlank]
public DateTimeInterface $departureDate;
#[OA\Property( #[OA\Property(
description: 'Booking customer details', description: 'Booking customer details',
)] )]
#[Assert\NotBlank]
#[Assert\Valid]
public BookingCustomer $customerDetails; public BookingCustomer $customerDetails;
/** /**
@@ -24,5 +31,7 @@ class BookingCreateRequest
#[OA\Property( #[OA\Property(
description: 'Rooms to book', description: 'Rooms to book',
)] )]
#[Assert\NotBlank]
#[Assert\Valid]
public array $rooms; public array $rooms;
} }
+6
View File
@@ -3,15 +3,21 @@
namespace App\Request; namespace App\Request;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;
class BookingCreateRoomItem class BookingCreateRoomItem
{ {
#[OA\Property(description: 'Room category ID')] #[OA\Property(description: 'Room category ID')]
#[Assert\NotBlank]
public int $roomCategory; public int $roomCategory;
#[OA\Property(description: 'Guest number for the room')] #[OA\Property(description: 'Guest number for the room')]
#[Assert\NotBlank]
#[Assert\GreaterThan(value: 0)]
public int $guestNumber; public int $guestNumber;
#[OA\Property(description: 'Room amount')] #[OA\Property(description: 'Room amount')]
#[Assert\NotBlank]
#[Assert\GreaterThan(value: 0)]
public int $amount; public int $amount;
} }
+4
View File
@@ -3,12 +3,16 @@
namespace App\Request; namespace App\Request;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;
class BookingDetailsRequest class BookingDetailsRequest
{ {
#[OA\Property(description: 'Booking reference number')] #[OA\Property(description: 'Booking reference number')]
#[Assert\NotBlank]
public string $refNumber; public string $refNumber;
#[OA\Property(maxLength: 255, description: 'Email associated to the booking')] #[OA\Property(maxLength: 255, description: 'Email associated to the booking')]
#[Assert\NotBlank]
#[Assert\Email]
public string $email; public string $email;
} }
@@ -2,13 +2,17 @@
namespace App\Request; namespace App\Request;
use DateTimeInterface;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;
class SearchAvailabilityPeriodRequest class SearchAvailabilityPeriodRequest
{ {
#[OA\Property(description: 'Period start date')] #[OA\Property(description: 'Period start date')]
public \DateTime $startDate; #[Assert\NotBlank]
public DateTimeInterface $startDate;
#[OA\Property(description: 'Period end date')] #[OA\Property(description: 'Period end date')]
public \DateTime $endDate; #[Assert\NotBlank]
public DateTimeInterface $endDate;
} }
@@ -2,16 +2,21 @@
namespace App\Request; namespace App\Request;
use DateTimeInterface;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Symfony\Component\Validator\Constraints as Assert;
class SearchPricesForCategoryRequest class SearchPricesForCategoryRequest
{ {
#[OA\Property(description: 'Room category ID')] #[OA\Property(description: 'Room category ID')]
#[Assert\NotBlank]
public int $roomCategory; public int $roomCategory;
#[OA\Property(description: 'Period start date')] #[OA\Property(description: 'Period start date')]
public \DateTime $startDate; #[Assert\NotBlank]
public DateTimeInterface $startDate;
#[OA\Property(description: 'Period end date')] #[OA\Property(description: 'Period end date')]
public \DateTime $endDate; #[Assert\NotBlank]
public DateTimeInterface $endDate;
} }