diff --git a/composer.json b/composer.json index 0eed7ea..2643ad1 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "symfony/dotenv": "6.2.*", "symfony/flex": "^2", "symfony/framework-bundle": "6.2.*", + "symfony/monolog-bundle": "^3.8", "symfony/runtime": "6.2.*", "symfony/security-bundle": "6.2.*", "symfony/serializer": "6.2.*", diff --git a/composer.lock b/composer.lock index 1a13657..7f315ea 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "76dd7c4a30ce3f2d4cc6681213a99fb6", + "content-hash": "4f698c4d36137de0d1bb86faad2c23e3", "packages": [ { "name": "doctrine/annotations", @@ -1636,6 +1636,107 @@ ], "time": "2022-12-08T02:08:23+00:00" }, + { + "name": "monolog/monolog", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "305444bc6fb6c89e490f4b34fa6e979584d7fa81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/305444bc6fb6c89e490f4b34fa6e979584d7fa81", + "reference": "305444bc6fb6c89e490f4b34fa6e979584d7fa81", + "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", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^9.5.16", + "predis/predis": "^1.1", + "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": "j.boggiano@seld.be", + "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.2.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2022-07-24T12:00:55+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -3551,6 +3652,170 @@ ], "time": "2022-11-02T09:08:04+00:00" }, + { + "name": "symfony/monolog-bridge", + "version": "v6.2.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "56172b511312a7ea9759311109df060d14b55e08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/56172b511312a7ea9759311109df060d14b55e08", + "reference": "56172b511312a7ea9759311109df060d14b55e08", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1|^2|^3", + "php": ">=8.1", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "symfony/console": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/security-core": "<6.0" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/mailer": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/security-core": "^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "suggest": { + "symfony/console": "For the possibility to show log messages in console commands depending on verbosity settings.", + "symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.", + "symfony/var-dumper": "For using the debugging handlers like the console handler or the log server handler." + }, + "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": "fabien@symfony.com" + }, + { + "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/v6.2.2" + }, + "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": "2022-12-14T16:11:27+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.8.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d", + "reference": "a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.22 || ^2.0 || ^3.0", + "php": ">=7.1.3", + "symfony/config": "~4.4 || ^5.0 || ^6.0", + "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0", + "symfony/http-kernel": "~4.4 || ^5.0 || ^6.0", + "symfony/monolog-bridge": "~4.4 || ^5.0 || ^6.0" + }, + "require-dev": { + "symfony/console": "~4.4 || ^5.0 || ^6.0", + "symfony/phpunit-bridge": "^5.2 || ^6.0", + "symfony/yaml": "~4.4 || ^5.0 || ^6.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": "fabien@symfony.com" + }, + { + "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.8.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": "2022-05-10T14:24:36+00:00" + }, { "name": "symfony/options-resolver", "version": "v6.2.0", diff --git a/config/bundles.php b/config/bundles.php index bb17505..8293de5 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -9,4 +9,5 @@ Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], ]; diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml new file mode 100644 index 0000000..587dc13 --- /dev/null +++ b/config/packages/monolog.yaml @@ -0,0 +1,54 @@ +monolog: + channels: + - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists + +when@dev: + monolog: + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + channels: [ "!event" ] + console: + type: console + level: debug + 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 diff --git a/src/Action/Region/Get/GetRegionsAction.php b/src/Action/Region/Get/GetRegionsAction.php new file mode 100644 index 0000000..2cd0fee --- /dev/null +++ b/src/Action/Region/Get/GetRegionsAction.php @@ -0,0 +1,22 @@ +regionRepository->list($request->offset, $request->limit, $request->title) + ); + } + +} \ No newline at end of file diff --git a/src/Action/Region/Get/GetRegionsActionInterface.php b/src/Action/Region/Get/GetRegionsActionInterface.php new file mode 100644 index 0000000..ff42c04 --- /dev/null +++ b/src/Action/Region/Get/GetRegionsActionInterface.php @@ -0,0 +1,10 @@ +limit = array_key_exists('limit', $params) && is_numeric($params['limit']) + ? (int)$params['limit'] + : self::DEFAULT_LIMIT; + + $req->offset = array_key_exists('offset', $params) && is_numeric($params['offset']) + ? (int)$params['offset'] + : self::DEFAULT_OFFSET; + + $req->title = $params['title'] ?? null; + + return $req; + } +} \ No newline at end of file diff --git a/src/Action/Region/Get/GetRegionsActionResponse.php b/src/Action/Region/Get/GetRegionsActionResponse.php new file mode 100644 index 0000000..76d6292 --- /dev/null +++ b/src/Action/Region/Get/GetRegionsActionResponse.php @@ -0,0 +1,20 @@ + $regions + */ + public function __construct(array $regions) + { + $this->regions = $regions; + } +} \ No newline at end of file diff --git a/src/Controller/Api/V1/RegionController.php b/src/Controller/Api/V1/RegionController.php index ec9b7a2..9fe0f0b 100644 --- a/src/Controller/Api/V1/RegionController.php +++ b/src/Controller/Api/V1/RegionController.php @@ -8,6 +8,8 @@ use App\Action\Region\Create\CreateRegionActionRequest; use App\Action\Region\Delete\DeleteRegionActionInterface; use App\Action\Region\Delete\DeleteRegionActionRequest; +use App\Action\Region\Get\GetRegionsAction; +use App\Action\Region\Get\GetRegionsActionRequest; use App\Controller\Api\ApiController; use App\Controller\HttpMethod; use App\Exception\ValidationException; @@ -37,8 +39,19 @@ public function delete(string $title, DeleteRegionActionInterface $action): Json { $req = new DeleteRegionActionRequest($title); $this->validateRequest($req); - return $this->json($action->run($req)); } + /** + * @throws ValidationException + */ + #[Route(self::API_ROUTE, name: 'app_api_v1_region_list', methods: HttpMethod::GET)] + public function list(Request $request, GetRegionsAction $action): JsonResponse + { + $req = GetRegionsActionRequest::fromArray($request->query->all()); + $this->validateRequest($req); + + return $this->json($action->run($req)->regions); + } + } \ No newline at end of file diff --git a/src/Controller/HttpMethod.php b/src/Controller/HttpMethod.php index 3f6b039..b28ce8f 100644 --- a/src/Controller/HttpMethod.php +++ b/src/Controller/HttpMethod.php @@ -8,4 +8,5 @@ class HttpMethod { public const POST = 'POST'; public const PUT = 'PUT'; public const DELETE = 'DELETE'; + public const GET = 'GET'; } \ No newline at end of file diff --git a/src/EventListener/ExceptionListener.php b/src/EventListener/ExceptionListener.php index 5bfe01e..689fd82 100644 --- a/src/EventListener/ExceptionListener.php +++ b/src/EventListener/ExceptionListener.php @@ -9,10 +9,14 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; +use Symfony\Component\HttpKernel\KernelInterface; readonly class ExceptionListener { - public function __construct(private LoggerInterface $logger) + public function __construct( + private LoggerInterface $logger, + private KernelInterface $kernel, + ) { } @@ -20,8 +24,8 @@ public function onKernelException(ExceptionEvent $event): void { $e = $event->getThrowable(); - if ($e instanceof ApplicationException) { - $code = $e->getCode(); + if ($e instanceof ApplicationException || $this->kernel->getEnvironment() === 'dev') { + $code = $e->getCode() === 0 ? Response::HTTP_INTERNAL_SERVER_ERROR : $e->getCode(); $message = $e->getMessage(); } else { $code = Response::HTTP_INTERNAL_SERVER_ERROR; diff --git a/src/Repository/RegionRepository.php b/src/Repository/RegionRepository.php index dcdbfa1..eed88f8 100644 --- a/src/Repository/RegionRepository.php +++ b/src/Repository/RegionRepository.php @@ -38,4 +38,17 @@ public function findByTitle(string $title): ?Region return $this->findOneBy(['title' => $title]); } + public function list(int $offset, int $limit, ?string $title): array + { + $qb = $this->createQueryBuilder('r') + ->setMaxResults($limit) + ->setFirstResult($offset); + + if ($title) { + $qb->where('r.title LIKE :title') + ->setParameter('title', "%$title%"); + } + + return $qb->getQuery()->getResult(); + } } diff --git a/src/Repository/RegionRepositoryInterface.php b/src/Repository/RegionRepositoryInterface.php index ce3c3d9..2558861 100644 --- a/src/Repository/RegionRepositoryInterface.php +++ b/src/Repository/RegionRepositoryInterface.php @@ -11,4 +11,5 @@ interface RegionRepositoryInterface public function save(Region $region): void; public function remove(Region $region): void; public function findByTitle(string $title): ?Region; + public function list(int $offset, int $limit, ?string $title): array; } \ No newline at end of file diff --git a/symfony.lock b/symfony.lock index 3bbf813..4b705f9 100644 --- a/symfony.lock +++ b/symfony.lock @@ -110,6 +110,18 @@ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" } }, + "symfony/monolog-bundle": { + "version": "3.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.7", + "ref": "213676c4ec929f046dfde5ea8e97625b81bc0578" + }, + "files": [ + "config/packages/monolog.yaml" + ] + }, "symfony/phpunit-bridge": { "version": "6.2", "recipe": { diff --git a/tests/Unit/Action/Region/Get/GetRegionsActionRequestTest.php b/tests/Unit/Action/Region/Get/GetRegionsActionRequestTest.php new file mode 100644 index 0000000..e5309a3 --- /dev/null +++ b/tests/Unit/Action/Region/Get/GetRegionsActionRequestTest.php @@ -0,0 +1,43 @@ + $limit, + 'offset' => $offset, + 'title' => $title, + ]; + + $actual = GetRegionsActionRequest::fromArray($request); + + $this->assertEquals($limit, $actual->limit); + $this->assertEquals($offset, $actual->offset); + $this->assertEquals($title, $actual->title); + } + + public function testShouldSetDefaultValuesOnNonNumericRequest(): void + { + $request = [ + 'offset' => 'o', + 'limit' => '1o' + ]; + + $actual = GetRegionsActionRequest::fromArray($request); + + $this->assertEquals(GetRegionsActionRequest::DEFAULT_LIMIT, $actual->limit); + $this->assertEquals(GetRegionsActionRequest::DEFAULT_OFFSET, $actual->offset); + } +} \ No newline at end of file diff --git a/tests/Unit/Action/Region/Get/GetRegionsActionResponseTest.php b/tests/Unit/Action/Region/Get/GetRegionsActionResponseTest.php new file mode 100644 index 0000000..4e3277e --- /dev/null +++ b/tests/Unit/Action/Region/Get/GetRegionsActionResponseTest.php @@ -0,0 +1,28 @@ +assertCount(2, $actual->regions); + $this->assertEquals($asia, $actual->regions[0]); + $this->assertEquals($europe, $actual->regions[1]); + } +} \ No newline at end of file diff --git a/tests/Unit/Action/Region/Get/GetRegionsActionTest.php b/tests/Unit/Action/Region/Get/GetRegionsActionTest.php new file mode 100644 index 0000000..51a11b2 --- /dev/null +++ b/tests/Unit/Action/Region/Get/GetRegionsActionTest.php @@ -0,0 +1,61 @@ +repository = $this->getMockBuilder(RegionRepositoryInterface::class)->getMock(); + + $id1 = Uuid::v1(); + $id2 = Uuid::v1(); + + $this->europe = new Region($id1); + $this->europe->setTitle('Europe'); + $this->europe->setCreatedAt(); + + $this->asia = new Region($id2); + $this->asia->setTitle('Asia'); + $this->asia->setCreatedAt(); + } + + public function testShouldReturnResponseArrayOfRegions(): void + { + $limit = 10; + $offset = 0; + + $regions = [$this->europe, $this->asia]; + + $this->repository + ->expects($this->once()) + ->method('list') + ->with($offset, $limit) + ->willReturn($regions); + + $req = new GetRegionsActionRequest(); + $req->limit = $limit; + $req->offset = $offset; + + $action = new GetRegionsAction($this->repository); + $actual = $action->run($req); + + $this->assertCount(2, $actual->regions); + $this->assertEquals($this->europe, $actual->regions[0]); + $this->assertEquals($this->asia, $actual->regions[1]); + } + +} \ No newline at end of file