From d8859ca751459b6622b1e078dd614202b5796629 Mon Sep 17 00:00:00 2001 From: firdaous elhalafi Date: Wed, 3 Jan 2024 22:28:49 +0100 Subject: [PATCH] gestion de caches au cas ou ces les memes requettes qui se font avec les memes inputs --- api/composer.json | 4 +- api/composer.lock | 77 +------- api/config/routes.yaml | 9 - api/src/Controller/SaleController.php | 27 ++- api/src/Entity/Sale.php | 13 +- api/src/Service/SaleService.php | 258 +++++++++++++++++--------- 6 files changed, 205 insertions(+), 183 deletions(-) diff --git a/api/composer.json b/api/composer.json index 992dbcf..272b903 100644 --- a/api/composer.json +++ b/api/composer.json @@ -6,9 +6,8 @@ "ext-ctype": "*", "ext-iconv": "*", "api-platform/core": "^3.2", - "beberlei/doctrineextensions": "dev-master", - "doctrine/cache": "*", "beberlei/doctrineextensions": "^1.3", + "doctrine/cache": "*", "doctrine/doctrine-bundle": "^2.7", "doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/orm": "^2.12", @@ -16,6 +15,7 @@ "phpstan/phpdoc-parser": "^1.16", "runtime/frankenphp-symfony": "^0.1.1", "symfony/asset": "6.4.*", + "symfony/cache": "6.4.*", "symfony/console": "6.4.*", "symfony/dotenv": "6.4.*", "symfony/expression-language": "6.4.*", diff --git a/api/composer.lock b/api/composer.lock index ed564cf..3baddaf 100644 --- a/api/composer.lock +++ b/api/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": "03c3d0b50d73e40d0c29a385e97c5c6c", + "content-hash": "830b9da35a09ffc162e5f9fda1fbaf62", "packages": [ { "name": "api-platform/core", @@ -228,65 +228,6 @@ }, "time": "2020-11-29T07:37:23+00:00" }, - { - "name": "beberlei/doctrineextensions", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/beberlei/DoctrineExtensions.git", - "reference": "67f32c184e80085e170b6e4e8f4f94bc92394c72" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/beberlei/DoctrineExtensions/zipball/67f32c184e80085e170b6e4e8f4f94bc92394c72", - "reference": "67f32c184e80085e170b6e4e8f4f94bc92394c72", - "shasum": "" - }, - "require": { - "doctrine/orm": "^2.7", - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "doctrine/cache": "^1.11", - "friendsofphp/php-cs-fixer": "^2.14", - "nesbot/carbon": "*", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", - "symfony/yaml": "^4.4 || ^5.3 || ^6.0", - "zf1/zend-date": "^1.12", - "zf1/zend-registry": "^1.12" - }, - "default-branch": true, - "type": "library", - "autoload": { - "psr-4": { - "DoctrineExtensions\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Steve Lacey", - "email": "steve@steve.ly" - } - ], - "description": "A set of extensions to Doctrine 2 that add support for additional query functions available in MySQL, Oracle, PostgreSQL and SQLite.", - "keywords": [ - "database", - "doctrine", - "orm" - ], - "support": { - "source": "https://github.com/beberlei/DoctrineExtensions/tree/master" - }, - "time": "2023-02-25T19:57:36+00:00" - }, { "name": "doctrine/cache", "version": "2.2.0", @@ -2322,16 +2263,16 @@ }, { "name": "symfony/cache", - "version": "v6.4.0", + "version": "v6.4.2", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "ac2d25f97b17eec6e19760b6b9962a4f7c44356a" + "reference": "14a75869bbb41cb35bc5d9d322473928c6f3f978" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/ac2d25f97b17eec6e19760b6b9962a4f7c44356a", - "reference": "ac2d25f97b17eec6e19760b6b9962a4f7c44356a", + "url": "https://api.github.com/repos/symfony/cache/zipball/14a75869bbb41cb35bc5d9d322473928c6f3f978", + "reference": "14a75869bbb41cb35bc5d9d322473928c6f3f978", "shasum": "" }, "require": { @@ -2398,7 +2339,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.4.0" + "source": "https://github.com/symfony/cache/tree/v6.4.2" }, "funding": [ { @@ -2414,7 +2355,7 @@ "type": "tidelift" } ], - "time": "2023-11-24T19:28:07+00:00" + "time": "2023-12-29T15:34:34+00:00" }, { "name": "symfony/cache-contracts", @@ -8325,9 +8266,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "beberlei/doctrineextensions": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/api/config/routes.yaml b/api/config/routes.yaml index 031ef7c..41ef814 100644 --- a/api/config/routes.yaml +++ b/api/config/routes.yaml @@ -3,12 +3,3 @@ controllers: path: ../src/Controller/ namespace: App\Controller type: attribute - -bar_chart_route: - path: '/sales/bar-chart/{startDate}/{endDate}/{granularity}' - controller: 'BarChartController::getChartData' - methods: ['GET'] - requirements: - startDate: '\d{4}-\d{2}-\d{2}' - endDate: '\d{4}-\d{2}-\d{2}' - granularity: 'day|month|year' diff --git a/api/src/Controller/SaleController.php b/api/src/Controller/SaleController.php index 35b8dd1..2e14a2d 100644 --- a/api/src/Controller/SaleController.php +++ b/api/src/Controller/SaleController.php @@ -7,15 +7,17 @@ use App\Service\SaleService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Validation; +use ApiPlatform\Metadata\ApiResource; -#[AsController] class SaleController extends AbstractController { + public const GRANULARITY_DAY = 'day'; + public const GRANULARITY_MONTH = 'month'; + public const GRANULARITY_YEAR = 'year'; private SaleService $saleService; public function __construct(SaleService $saleService) @@ -36,8 +38,27 @@ class SaleController extends AbstractController 'endDate' => '\d{4}-\d{2}-\d{2}', // YYYY-MM-DD 'granularity' => 'day|month|year', // day, month, ou year ])] - public function getChartData(string $startDate, string $endDate, string $granularity): JsonResponse + public function getChartData( + string $startDate, + string $endDate, + string $granularity + ): JsonResponse { + $validator = Validation::createValidator(); + $violations = $validator->validate([ + 'startDate' => $startDate, + 'endDate' => $endDate, + 'granularity' => $granularity, + ], new Assert\Collection([ + 'startDate' => new Assert\Date(), + 'endDate' => new Assert\Date(), + 'granularity' => new Assert\Choice(choices: [self::GRANULARITY_DAY, self::GRANULARITY_MONTH, self::GRANULARITY_YEAR]), + ])); + + if (count($violations) > 0) { + return new JsonResponse(['error' => 'Invalid input'], Response::HTTP_BAD_REQUEST); + } + $input = new BarChartInput(); $input->start = $startDate; $input->end = $endDate; diff --git a/api/src/Entity/Sale.php b/api/src/Entity/Sale.php index 869d610..589a094 100644 --- a/api/src/Entity/Sale.php +++ b/api/src/Entity/Sale.php @@ -6,11 +6,14 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use App\Controller\SaleController; use Doctrine\ORM\Mapping as ORM; -use ApiPlatform\Metadata\Get; -use App\Controller\ChartController; #[ApiResource( operations: [ + new GetCollection( + uriTemplate: '/api/timeseries', + controller: SaleController::class, + name: 'timeseries' + ), new GetCollection( uriTemplate: '/api/bar-chart/{startDate}/{endDate}/{granularity}', requirements: [ @@ -28,12 +31,6 @@ use App\Controller\ChartController; ], controller: SaleController::class, name: 'donut-chart', - ), - new GetCollection( - uriTemplate: '/api/timeseries', - requirements: [], - controller: SaleController::class, - name: 'timeseries' ) ] )] diff --git a/api/src/Service/SaleService.php b/api/src/Service/SaleService.php index aeebcb0..cbd5d64 100644 --- a/api/src/Service/SaleService.php +++ b/api/src/Service/SaleService.php @@ -7,122 +7,196 @@ use App\Dto\BarChartOutput; use App\Dto\MonthlyAveragePriceEvolution; use App\Entity\Sale; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Query; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; class SaleService { private EntityManagerInterface $entityManager; + private CacheInterface $cache; - public function __construct(EntityManagerInterface $entityManager) + public function __construct(EntityManagerInterface $entityManager, CacheInterface $cache) { $this->entityManager = $entityManager; + $this->cache = $cache; } - public function getTimeSeries(): array + public function updateTimeSerisCache(): void { - $queryBuilder = $this->entityManager->createQueryBuilder(); - - $result = $queryBuilder - ->select('MONTH(s.date) as month', 'YEAR(s.date) as year', 'AVG(CASE WHEN s.surface <> 0 THEN s.amount / s.surface ELSE 0 END) as average_price') - ->from(Sale::class, 's') - ->groupBy('year, month') - ->orderBy('year, month') - ->getQuery() - ->getResult(); - - $monthlyAveragePriceEvolutions = []; - foreach ($result as $row) { - $monthlyAveragePriceEvolutions[] = new MonthlyAveragePriceEvolution( - (int)$row['month'], - (int)$row['year'], - (float)$row['average_price'] - ); - } + $this->cache->delete('time_series'); + } + + public function updateBartChartCache($input): void + { + $cacheKey = 'bar-chart' . $input; - return $monthlyAveragePriceEvolutions; + if ($this->cache->has($cacheKey)) { + $this->cache->delete($cacheKey); + } } - public function getBarChartData(BarChartInput $input): array + public function updateDonutChartCache($year): void { - $startDate = new \DateTime($input->start); - $endDate = new \DateTime($input->end); - - $queryBuilder = $this->entityManager->createQueryBuilder(); - $output = []; - - if ($input->granularity == 'month') { - $result = $queryBuilder - ->select('YEAR(s.date) as year', 'MONTH(s.date) as month', 'COUNT(s.id) as occurrences') - ->from(Sale::class, 's') - ->where('s.date BETWEEN :start AND :end') - ->setParameter('start', $startDate) - ->setParameter('end', $endDate) - ->groupBy('year', 'month') - ->orderBy('year') - ->getQuery() - ->getResult(); - } else { - switch ($input->granularity) { - case 'day': - $groupByExpression = 's.date'; - $groupByAlias = 'date'; - $dateFormat = 'Y-m-d'; - break; - case 'year': - $groupByExpression = 'YEAR(s.date)'; - $groupByAlias = 'year'; - $dateFormat = 'Y'; - break; - default: - throw new \InvalidArgumentException('Invalid granularity'); - } + $cacheKey = 'donut-chart' . $year; - $result = $queryBuilder - ->select("{$groupByExpression} as {$groupByAlias}", 'COUNT(s.id) as occurrences') - ->from(Sale::class, 's') - ->where('s.date BETWEEN :start AND :end') - ->setParameter('start', $startDate) - ->setParameter('end', $endDate) - ->groupBy("{$groupByAlias}") - ->orderBy("{$groupByAlias}") - ->getQuery() - ->getResult(); + if ($this->cache->has($cacheKey)) { + $this->cache->delete($cacheKey); } + } + - foreach ($result as $row) { - if ($input->granularity == 'month') { - $month = $row['month']; - $year = $row['year']; - $dateString = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT); - } else { - $dateString = $row[$groupByAlias] instanceof \DateTimeInterface ? $row[$groupByAlias]->format($dateFormat) : $row[$groupByAlias]; + + public function getTimeSeries(): array + { + return $this->cache->get('time_series', function (ItemInterface $item) { + try { + $queryBuilder = $this->entityManager->createQueryBuilder(); + + $result = $queryBuilder + ->select('MONTH(s.date) as month', 'YEAR(s.date) as year', 'AVG(CASE WHEN s.surface <> 0 THEN s.amount / s.surface ELSE 0 END) as average_price') + ->from(Sale::class, 's') + ->groupBy('year, month') + ->orderBy('year, month') + ->getQuery() + ->getResult(Query::HYDRATE_ARRAY); + + $monthlyAveragePriceEvolutions = []; + foreach ($result as $row) { + $monthlyAveragePriceEvolutions[] = new MonthlyAveragePriceEvolution( + (int)$row['month'], + (int)$row['year'], + (float)$row['average_price'] + ); + } + + $item->expiresAfter(600); + + return $monthlyAveragePriceEvolutions; + } catch (\Exception $exception){ + throw $exception; } + }); + } - $output[] = new BarChartOutput($dateString, (int)$row['occurrences']); - } + private function generateCacheKey(BarChartInput $input): string + { + $inputArray = [ + 'start' => $input->start, + 'end' => $input->end, + 'granularity' => $input->granularity, + ]; + + $jsonEncodedInput = json_encode($inputArray); + + $cleanedKey = preg_replace('/[^A-Za-z0-9]/', '_', $jsonEncodedInput); + return $cleanedKey; + } - return $output; + public function getBarChartData(BarChartInput $input): array + { + $cacheKey = 'bar-chart' . $this->generateCacheKey($input); + + return $this->cache->get($cacheKey, function (ItemInterface $item) use ($input) { + try { + $startDate = new \DateTime($input->start); + $endDate = new \DateTime($input->end); + + $queryBuilder = $this->entityManager->createQueryBuilder(); + $output = []; + + if ($input->granularity == 'month') { + $result = $queryBuilder + ->select('YEAR(s.date) as year', 'MONTH(s.date) as month', 'COUNT(s.id) as occurrences') + ->from(Sale::class, 's') + ->where('s.date BETWEEN :start AND :end') + ->setParameter('start', $startDate) + ->setParameter('end', $endDate) + ->groupBy('year', 'month') + ->orderBy('year') + ->getQuery() + ->getResult(); + } else { + switch ($input->granularity) { + case 'day': + $groupByExpression = 's.date'; + $groupByAlias = 'date'; + $dateFormat = 'Y-m-d'; + break; + case 'year': + $groupByExpression = 'YEAR(s.date)'; + $groupByAlias = 'year'; + $dateFormat = 'Y'; + break; + default: + throw new \InvalidArgumentException('Invalid granularity'); + } + + $result = $queryBuilder + ->select("{$groupByExpression} as {$groupByAlias}", 'COUNT(s.id) as occurrences') + ->from(Sale::class, 's') + ->where('s.date BETWEEN :start AND :end') + ->setParameter('start', $startDate) + ->setParameter('end', $endDate) + ->groupBy("{$groupByAlias}") + ->orderBy("{$groupByAlias}") + ->getQuery() + ->getResult(); + } + + foreach ($result as $row) { + if ($input->granularity == 'month') { + $month = $row['month']; + $year = $row['year']; + $dateString = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT); + } else { + $dateString = $row[$groupByAlias] instanceof \DateTimeInterface ? $row[$groupByAlias]->format($dateFormat) : $row[$groupByAlias]; + } + + $output[] = new BarChartOutput($dateString, (int)$row['occurrences']); + } + + $item->expiresAfter(600); + + return $output; + } catch (\Exception $exception){ + throw $exception; + } + }); } public function getDonutChartData(string $year): array { - $repository = $this->entityManager->getRepository(Sale::class); - - $result = $repository->createQueryBuilder('s') - ->select('s.region', 'COUNT(s.id) as occurrences') - ->andWhere('YEAR(s.date) = :year') - ->setParameter('year', $year) - ->groupBy('s.region') - ->getQuery() - ->getResult(); - - $data = []; - foreach ($result as $row) { - $data[] = [ - 'region' => $row['region'], - 'occurrences' => (int)$row['occurrences'], - ]; - } - return $data; + $cacheKey = 'donut-chart' . $year; + + return $this->cache->get($cacheKey, function (ItemInterface $item) use ($year) { + + try { + $repository = $this->entityManager->getRepository(Sale::class); + + $result = $repository->createQueryBuilder('s') + ->select('s.region', 'COUNT(s.id) as occurrences') + ->andWhere('YEAR(s.date) = :year') + ->setParameter('year', $year) + ->groupBy('s.region') + ->getQuery() + ->getResult(); + + $data = []; + foreach ($result as $row) { + $data[] = [ + 'region' => $row['region'], + 'occurrences' => (int)$row['occurrences'], + ]; + } + + $item->expiresAfter(600); + + return $data; + }catch (\Exception $exception){ + throw $exception; + } + }); } } -- GitLab