4 Commits
Author SHA1 Message Date
Daniel-Garmig 37462e4646 Added Validation to API requests.
CI / Docker Lint (push) Successful in 21s
- Added Symfony Validation Lib.
- Updated API DTOs to include validation constraints.
2024-10-18 20:32:36 +02:00
Daniel-Garmig fbee23c022 Updated Somnia API exceptions to use the new API error handling. 2024-10-18 17:58:27 +02:00
Daniel-Garmig facd5bc9c0 New error handler for API. 2024-10-16 20:05:26 +02:00
Daniel-Garmig 1bf738be1c Added Monolog for logs. 2024-10-14 21:27:53 +02:00
20 changed files with 667 additions and 84 deletions
+11 -5
View File
@@ -89,10 +89,16 @@ docker network create somniarooms-network
Go to the app address (`https://localhost`) and it will show Symfony welcome page (or a error when running on prod). Go to the app address (`https://localhost`) and it will show Symfony welcome page (or a error when running on prod).
Go to addres `https://localhost/setup` to inicialize app users and data. Go to addres `https://localhost/setup` to inicialize app users and data.
During setup you must configure the admin account.
If you check "Add example data" you will be redirected to example data setup.
Example data can be added anytime at `https://localhost/debug/exampleData`. There is needed to use a Superadmin account.
Then, you the admin panel is at `https://localhost/admin` Then, you the admin panel is at `https://localhost/admin`
You may login using default admin user: ## Logs.
```
user: admin By default logs are saved to `/app/var/log` on an unamed container volume.
pass: admin For development, `/app/var/log` directory is mounted to `./var/log` on the local filesystem.
```
This config can be overriden setting a different mount for `/app/var/log` when creating the containers.
+2
View File
@@ -11,6 +11,8 @@ services:
# If you develop on Mac or Windows you can remove the vendor/ directory # If you develop on Mac or Windows you can remove the vendor/ directory
# from the bind-mount for better performance by enabling the next line: # from the bind-mount for better performance by enabling the next line:
- /app/vendor - /app/vendor
# On dev mount var to localfiles.
- ./var/log:/app/var/log
environment: environment:
MERCURE_EXTRA_DIRECTIVES: demo MERCURE_EXTRA_DIRECTIVES: demo
# See https://xdebug.org/docs/all_settings#mode # See https://xdebug.org/docs/all_settings#mode
+2
View File
@@ -27,12 +27,14 @@
"symfony/form": "7.0.*", "symfony/form": "7.0.*",
"symfony/framework-bundle": "7.0.*", "symfony/framework-bundle": "7.0.*",
"symfony/mime": "7.0.*", "symfony/mime": "7.0.*",
"symfony/monolog-bundle": "^3.10",
"symfony/property-access": "7.0.*", "symfony/property-access": "7.0.*",
"symfony/property-info": "7.0.*", "symfony/property-info": "7.0.*",
"symfony/runtime": "7.0.*", "symfony/runtime": "7.0.*",
"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
+269 -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": "20c33276e1944c7bb2e51459e4f90463", "content-hash": "974cb22b15d6dc64bd6dac49da5d671d",
"packages": [ "packages": [
{ {
"name": "composer/semver", "name": "composer/semver",
@@ -1401,6 +1401,107 @@
], ],
"time": "2024-04-19T18:13:15+00:00" "time": "2024-04-19T18:13:15+00:00"
}, },
{
"name": "monolog/monolog",
"version": "3.7.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/f4393b648b78a5408747de94fca38beb5f7e9ef8",
"reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8",
"shasum": ""
},
"require": {
"php": ">=8.1",
"psr/log": "^2.0 || ^3.0"
},
"provide": {
"psr/log-implementation": "3.0.0"
},
"require-dev": {
"aws/aws-sdk-php": "^3.0",
"doctrine/couchdb": "~1.0@dev",
"elasticsearch/elasticsearch": "^7 || ^8",
"ext-json": "*",
"graylog2/gelf-php": "^1.4.2 || ^2.0",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.2",
"mongodb/mongodb": "^1.8",
"php-amqplib/php-amqplib": "~2.4 || ^3",
"phpstan/phpstan": "^1.9",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-strict-rules": "^1.4",
"phpunit/phpunit": "^10.5.17",
"predis/predis": "^1.1 || ^2",
"ruflin/elastica": "^7",
"symfony/mailer": "^5.4 || ^6",
"symfony/mime": "^5.4 || ^6"
},
"suggest": {
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
"ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
"ext-mbstring": "Allow to work properly with unicode symbols",
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
"ext-openssl": "Required to send log messages using SSL",
"ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
"rollbar/rollbar": "Allow sending log messages to Rollbar",
"ruflin/elastica": "Allow sending log messages to an Elastic Search server"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Monolog\\": "src/Monolog"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "[email protected]",
"homepage": "https://seld.be"
}
],
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
"homepage": "https://github.com/Seldaek/monolog",
"keywords": [
"log",
"logging",
"psr-3"
],
"support": {
"issues": "https://github.com/Seldaek/monolog/issues",
"source": "https://github.com/Seldaek/monolog/tree/3.7.0"
},
"funding": [
{
"url": "https://github.com/Seldaek",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
"type": "tidelift"
}
],
"time": "2024-06-28T09:40:51+00:00"
},
{ {
"name": "nelmio/api-doc-bundle", "name": "nelmio/api-doc-bundle",
"version": "v4.30.0", "version": "v4.30.0",
@@ -4187,6 +4288,165 @@
], ],
"time": "2024-06-28T09:58:46+00:00" "time": "2024-06-28T09:58:46+00:00"
}, },
{
"name": "symfony/monolog-bridge",
"version": "v7.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/monolog-bridge.git",
"reference": "d80b7aeabc539538c6ae8962259ac422632d7796"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/d80b7aeabc539538c6ae8962259ac422632d7796",
"reference": "d80b7aeabc539538c6ae8962259ac422632d7796",
"shasum": ""
},
"require": {
"monolog/monolog": "^3",
"php": ">=8.2",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/service-contracts": "^2.5|^3"
},
"conflict": {
"symfony/console": "<6.4",
"symfony/http-foundation": "<6.4",
"symfony/security-core": "<6.4"
},
"require-dev": {
"symfony/console": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/mailer": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/mime": "^6.4|^7.0",
"symfony/security-core": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0"
},
"type": "symfony-bridge",
"autoload": {
"psr-4": {
"Symfony\\Bridge\\Monolog\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "[email protected]"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides integration for Monolog with various Symfony components",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/monolog-bridge/tree/v7.0.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T14:55:39+00:00"
},
{
"name": "symfony/monolog-bundle",
"version": "v3.10.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/monolog-bundle.git",
"reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181",
"reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181",
"shasum": ""
},
"require": {
"monolog/monolog": "^1.25.1 || ^2.0 || ^3.0",
"php": ">=7.2.5",
"symfony/config": "^5.4 || ^6.0 || ^7.0",
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0",
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
"symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0"
},
"require-dev": {
"symfony/console": "^5.4 || ^6.0 || ^7.0",
"symfony/phpunit-bridge": "^6.3 || ^7.0",
"symfony/yaml": "^5.4 || ^6.0 || ^7.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Bundle\\MonologBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "[email protected]"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony MonologBundle",
"homepage": "https://symfony.com",
"keywords": [
"log",
"logging"
],
"support": {
"issues": "https://github.com/symfony/monolog-bundle/issues",
"source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-11-06T17:08:13+00:00"
},
{ {
"name": "symfony/options-resolver", "name": "symfony/options-resolver",
"version": "v7.0.0", "version": "v7.0.0",
@@ -6322,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": {
@@ -6376,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/",
@@ -6396,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": [
{ {
@@ -6412,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",
+1
View File
@@ -12,4 +12,5 @@ return [
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
]; ];
+75
View File
@@ -0,0 +1,75 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev:
monolog:
handlers:
main:
type: rotating_file
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event", "!doctrine"]
max_files: 0
doctrine:
type: rotating_file
path: "%kernel.logs_dir%/db_%kernel.environment%.log"
level: debug
channels: ["doctrine", "request"]
max_files: 10
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
when@test:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
when@prod:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested:
type: stream
path: php://stderr
level: debug
formatter: monolog.formatter.json
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
deprecation:
type: stream
channels: [deprecation]
path: php://stderr
formatter: monolog.formatter.json
logfile:
type: rotating_file
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: info
channels: ["!event"]
max_files: 60
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App;
use App\DTO\ApiErrorMessage;
use Symfony\Component\HttpKernel\Exception\HttpException;
class ApiErrorException extends HttpException
{
private ApiErrorMessage $apiErrorMsg;
public function __construct(ApiErrorMessage $apiErrorMsg, $detailsData = null, \Exception $previous = null, array $headers = array(), $code = 0)
{
$this->apiErrorMsg = $apiErrorMsg;
if (isset($detailsData)) {
$this->apiErrorMsg->setDetails($detailsData);
}
$statusCode = $apiErrorMsg->getStatusCode();
$message = $apiErrorMsg->getTitle();
parent::__construct($statusCode, $message, $previous, $headers, $code);
}
public function getApiErrorMsg(): ApiErrorMessage
{
return $this->apiErrorMsg;
}
}
+5
View File
@@ -11,6 +11,8 @@ use App\Repository\RoomCategoryRepository;
use App\Service\AppSetupService; use App\Service\AppSetupService;
use App\Service\ExampleDataService; use App\Service\ExampleDataService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -20,6 +22,7 @@ class AppSetupController extends AbstractController
{ {
#[Route(path: '/setup', name: 'app_setup_setup')] #[Route(path: '/setup', name: 'app_setup_setup')]
public function addAdmin( public function addAdmin(
LoggerInterface $logger,
AppSetupService $setupService, AppSetupService $setupService,
Request $request, Request $request,
EntityManagerInterface $entityManager EntityManagerInterface $entityManager
@@ -29,6 +32,8 @@ class AppSetupController extends AbstractController
$setupStatus = $configRepo->findBy(["key" => "SETUP_STATUS"]); $setupStatus = $configRepo->findBy(["key" => "SETUP_STATUS"]);
if ($setupStatus != null && $setupStatus[0] != null && $setupStatus[0]->getValue() == 1) { if ($setupStatus != null && $setupStatus[0] != null && $setupStatus[0]->getValue() == 1) {
$logger->warning("SomniaRooms was already setup");
return $this->GoToResultPage( return $this->GoToResultPage(
isError: false, isError: false,
message: "SomniaRooms already setup.", message: "SomniaRooms already setup.",
+29 -26
View File
@@ -2,8 +2,9 @@
namespace App\Controller; namespace App\Controller;
use App\ApiErrorException;
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;
@@ -16,11 +17,10 @@ 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;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class BookingController extends AbstractController class BookingController extends AbstractController
{ {
@@ -48,21 +48,16 @@ class BookingController extends AbstractController
#[MapRequestPayload()] BookingDetailsRequest $detailsRequest #[MapRequestPayload()] BookingDetailsRequest $detailsRequest
): JsonResponse { ): JsonResponse {
if (\is_null($detailsRequest)) { $selectedBooking = $repoBooking->findOneBy(["refNumber" => $detailsRequest->refNumber]);
throw new \Exception('Invalid Payload', 400); if (\is_null($selectedBooking)) {
throw new NotFoundHttpException('Booking not found');
} }
$bookingList = $repoBooking->findBy(["refNumber" => $detailsRequest->refNumber]); if ($selectedBooking->getBookingCustomer()->getEmail() != $detailsRequest->email) {
if (\is_null($bookingList) || \count($bookingList) == 0) { throw new NotFoundHttpException('Booking not found');
throw new HttpException(404, 'Invalid Booking');
} }
$customerBooking = $bookingList[0]; return $this->json($selectedBooking);
if ($customerBooking->getBookingCustomer()->getEmail() != $detailsRequest->email) {
throw new HttpException(404, 'Invalid Booking');
}
return $this->json($customerBooking);
} }
/** /**
@@ -85,26 +80,25 @@ 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 HttpException('Invalid Payload', 400);
}
$booking = new Booking(); $booking = new Booking();
try { try {
if ($createRequest->arrivalDate >= $createRequest->departureDate) { if ($createRequest->arrivalDate >= $createRequest->departureDate) {
throw new HttpException(400, "Arrival cant be before departure"); throw new ApiErrorException(
new ApiErrorMessage(422, ApiErrorMessage::TYPE_INVALID_DATE),
'Arrival cant be before departure',
);
} }
$entityManager->beginTransaction(); $entityManager->beginTransaction();
$currentTime = new \DateTime(); $currentTime = new \DateTime("now");
// Create booking from request data. // Create booking from request data.
$booking->setBookingTime($currentTime); $booking->setBookingTime($currentTime);
@@ -124,7 +118,10 @@ class BookingController extends AbstractController
$entityManager->persist($booking); $entityManager->persist($booking);
if (\count($createRequest->rooms) == 0) { if (\count($createRequest->rooms) == 0) {
throw new HttpException(400, "Se debe indicar al menos una habitación"); throw new ApiErrorException(
new ApiErrorMessage(422, ApiErrorMessage::TYPE_VALIDATION_ERROR),
'Booking must contain rooms',
);
} }
// Create each room. // Create each room.
@@ -135,7 +132,10 @@ class BookingController extends AbstractController
//Get Room category. //Get Room category.
$roomCategory = $repoCat->find($roomItem->roomCategory); $roomCategory = $repoCat->find($roomItem->roomCategory);
if ($roomCategory == null) { if ($roomCategory == null) {
throw new HttpException(400, "La RoomCategory no es válida."); throw new ApiErrorException(
new ApiErrorMessage(422, ApiErrorMessage::TYPE_VALIDATION_ERROR),
'Invalid RoomCategory: ' . $roomItem->roomCategory,
);
} }
//Check room availability. //Check room availability.
@@ -144,7 +144,10 @@ class BookingController extends AbstractController
$selectedGuestAmount = $roomItem->guestNumber; $selectedGuestAmount = $roomItem->guestNumber;
if ($amountAvailable == null || $selectedAmount > $amountAvailable) { if ($amountAvailable == null || $selectedAmount > $amountAvailable) {
throw new HttpException(400, "No se pueden reservar más habitaciones de las disponibles."); throw new ApiErrorException(
new ApiErrorMessage(400, ApiErrorMessage::TYPE_LOGICAL_ERROR),
'Not enought rooms available for category: ' . $roomItem->roomCategory,
);
} }
$room->setRoomCategory($roomCategory); $room->setRoomCategory($roomCategory);
@@ -163,9 +166,9 @@ class BookingController extends AbstractController
} }
if (!$priceFound) { if (!$priceFound) {
throw new HttpException( throw new ApiErrorException(
500, new ApiErrorMessage(422, ApiErrorMessage::TYPE_LOGICAL_ERROR),
"No se puedo obtener el precio para la categoría: " . $roomCategory->getId() . " , guests: " . $selectedGuestAmount 'Price not found for category: ' . $roomCategory->getId() . ', guests: ' . $selectedGuestAmount,
); );
} }
+5 -1
View File
@@ -2,6 +2,8 @@
namespace App\Controller; namespace App\Controller;
use App\ApiErrorException;
use App\DTO\ApiErrorMessage;
use App\Entity\ConfigVariable; use App\Entity\ConfigVariable;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -91,7 +93,9 @@ class ConfigVariableController extends AbstractController
public function getVariablesBySection(ConfigVariableRepository $repo, string $section): Response public function getVariablesBySection(ConfigVariableRepository $repo, string $section): Response
{ {
if(\is_null($section) || $section == '') { if(\is_null($section) || $section == '') {
return new Response("Invalid section value", 404); throw new ApiErrorException(
new ApiErrorMessage(400, ApiErrorMessage::TYPE_INVALID_REQUEST_BODY_FORMAT)
);
} }
$varList = $repo->findAllBySection($section); $varList = $repo->findAllBySection($section);
+18 -37
View File
@@ -2,7 +2,8 @@
namespace App\Controller; namespace App\Controller;
use App\Repository\FareTableRepository; use App\ApiErrorException;
use App\DTO\ApiErrorMessage;
use App\Repository\RoomAvailabilityRepository; use App\Repository\RoomAvailabilityRepository;
use App\Repository\RoomCategoryRepository; use App\Repository\RoomCategoryRepository;
use App\Request\SearchAvailabilityPeriodRequest; use App\Request\SearchAvailabilityPeriodRequest;
@@ -12,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
{ {
@@ -48,16 +47,15 @@ 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 HttpException('Invalid Payload', 400);
}
if ($searchRequest->startDate >= $searchRequest->endDate) { if ($searchRequest->startDate >= $searchRequest->endDate) {
throw new HttpException(400, "startDate cant be before endDate"); throw new ApiErrorException(
new ApiErrorMessage(422, ApiErrorMessage::TYPE_INVALID_DATE),
'startDate cant be before endDate',
);
} }
$rs = $repoAvailability->getAvailabilityForPeriod($searchRequest->startDate, $searchRequest->endDate); $rs = $repoAvailability->getAvailabilityForPeriod($searchRequest->startDate, $searchRequest->endDate);
@@ -71,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);
@@ -112,22 +106,24 @@ 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 \Exception('Invalid Payload', 400);
}
$roomCategory = $repoCat->find($searchRequest->roomCategory); $roomCategory = $repoCat->find($searchRequest->roomCategory);
if ($roomCategory === null) { if ($roomCategory === null) {
throw new \Exception('Invalid Category', 400); throw new ApiErrorException(
new ApiErrorMessage(422, ApiErrorMessage::TYPE_VALIDATION_ERROR),
'Invalid RoomCategory: ' . $searchRequest->roomCategory,
);
} }
if ($searchRequest->startDate >= $searchRequest->endDate) { if ($searchRequest->startDate >= $searchRequest->endDate) {
throw new \Exception('Invalid time interval', 400); throw new ApiErrorException(
new ApiErrorMessage(422, ApiErrorMessage::TYPE_INVALID_DATE),
'startDate cant be before endDate',
);
} }
$fareList = $priceService->getFinalPricesForSearch($roomCategory, $searchRequest->startDate, $searchRequest->endDate); $fareList = $priceService->getFinalPricesForSearch($roomCategory, $searchRequest->startDate, $searchRequest->endDate);
@@ -138,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);
}
} }
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace App\DTO;
use Symfony\Component\HttpFoundation\Response;
class ApiErrorMessage
{
const TYPE_VALIDATION_ERROR = 'validation_error';
const TYPE_INVALID_REQUEST_BODY_FORMAT = 'invalid_body_format';
const TYPE_LOGICAL_ERROR = 'logical_error';
const TYPE_SERVER_ERROR = 'server_error';
const TYPE_INVALID_DATE = 'invalid_date';
private static $titles = array(
self::TYPE_VALIDATION_ERROR => 'There was a validation error',
self::TYPE_INVALID_REQUEST_BODY_FORMAT => 'Invalid JSON format sent',
self::TYPE_LOGICAL_ERROR => 'Invalid request due to a logic error',
self::TYPE_SERVER_ERROR => 'Error on server side',
self::TYPE_INVALID_DATE => 'Invalid date on request',
);
private int $statusCode;
private string $type;
private string $title;
private array $extraData = array();
public function __construct($statusCode, $type = null)
{
if ($type === null) {
// no type? The default is about:blank and the title should
// be the standard status code message
$type = 'about:blank';
$title = isset(Response::$statusTexts[$statusCode]) ?
Response::$statusTexts[$statusCode] : 'Unknown status code :(';
} else {
if (!isset(self::$titles[$type])) {
throw new \InvalidArgumentException('No title for type ' . $type);
}
$title = self::$titles[$type];
}
$this->statusCode = $statusCode;
$this->type = $type;
$this->title = $title;
}
public function getStatusCode(): int
{
return $this->statusCode;
}
public function getTitle(): string
{
return $this->title;
}
public function setExtraData($name, $value)
{
$this->extraData[$name] = $value;
}
public function setDetails($value)
{
$this->extraData['details'] = $value;
}
public function toArray(): array
{
return array_merge(
$this->extraData,
array(
'status' => $this->statusCode,
'type' => $this->type,
'title' => $this->title,
)
);
}
}
+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
@@ -0,0 +1,86 @@
<?php
namespace App\EventSubscriber;
use App\ApiErrorException;
use App\DTO\ApiErrorMessage;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException;
class ApiExceptionSubscriber implements EventSubscriberInterface
{
public function __construct(
private LoggerInterface $logger,
) {}
public function onKernelException(ExceptionEvent $event): void
{
$e = $event->getThrowable();
if ($e instanceof ApiErrorException) {
$apiErrorMsg = $e->getApiErrorMsg();
} else {
$statusCode = $e instanceof HttpExceptionInterface ? $e->getStatusCode() : 500;
$apiErrorMsg = new ApiErrorMessage($statusCode);
/*
* If it's an HttpException message (e.g. for 404, 403),
* we'll say as a rule that the exception message is safe
* for the client. Otherwise, it could be some sensitive
* low-level exception, which should *not* be exposed
*/
if ($e instanceof HttpExceptionInterface) {
//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());
}
}
}
//Send error to logs.
$exceptionError = [
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTrace()
];
$errorContext = array_merge($apiErrorMsg->toArray(), $exceptionError);
$this->logger->error("API ERROR Exception:", $errorContext);
//Send error to client.
$response = new JsonResponse(
$apiErrorMsg->toArray(),
$apiErrorMsg->getStatusCode()
);
$response->headers->set('Content-Type', 'application/problem+json');
$event->setResponse($response);
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::EXCEPTION => 'onKernelException',
];
}
}
+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;
} }
+12
View File
@@ -127,6 +127,18 @@
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
} }
}, },
"symfony/monolog-bundle": {
"version": "3.10",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.7",
"ref": "aff23899c4440dd995907613c1dd709b6f59503f"
},
"files": [
"./config/packages/monolog.yaml"
]
},
"symfony/routing": { "symfony/routing": {
"version": "7.0", "version": "7.0",
"recipe": { "recipe": {