diff --git a/compose.yml b/compose.yml index 5db6358..9afac82 100644 --- a/compose.yml +++ b/compose.yml @@ -1,5 +1,3 @@ -version: '3.7' - services: web: container_name: blog.nginx diff --git a/composer.json b/composer.json index fb2944b..04d6172 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "ext-iconv": "*", "ext-intl": "*", "league/commonmark": "2.4.*", + "symfony/config": "7.0.*", "symfony/console": "7.0.*", "symfony/dotenv": "7.0.*", "symfony/flex": "^2", diff --git a/composer.lock b/composer.lock index c6b0d9d..94c3879 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": "61a00d4110d12aae947ae95e3fcff4b4", + "content-hash": "9c927b3044f44948de6b6bab3cab2603", "packages": [ { "name": "dflydev/dot-access-data", diff --git a/config/packages/framework.php b/config/packages/framework.php index 02d7fc1..6dc9b73 100644 --- a/config/packages/framework.php +++ b/config/packages/framework.php @@ -52,6 +52,9 @@ ->app('cache.adapter.filesystem') ->system('cache.adapter.system'); + $framework->cache()->pool('messenger.cache') + ->tags(true); + /** * Router Configuration * @see \Symfony\Config\Framework\RouterConfig @@ -132,7 +135,7 @@ 'middleware' => array_filter([ $container->env() === 'dev' ? StopwatchMiddleware::class : null, LoggerMiddleware::class, - $container->env() === 'dev' ? null : CacheMiddleware::class, + CacheMiddleware::class, ]), ]); diff --git a/config/packages/security.php b/config/packages/security.php index e5cd808..6f10e91 100644 --- a/config/packages/security.php +++ b/config/packages/security.php @@ -17,7 +17,7 @@ ->security(false); $adminFirewall = $security->firewall('admin') - ->pattern('^/admin/') + ->pattern('^/(admin|webhook)/') ->security(true) ->stateless(true) ->provider(ADMIN_USER_PROVIDER); @@ -34,6 +34,6 @@ ->lazy(true); $security->accessControl() - ->path('^/admin') + ->path('^/(admin|webhook)') ->roles(['ROLE_ADMIN']); }; diff --git a/config/routes.php b/config/routes.php index 426c676..083c657 100644 --- a/config/routes.php +++ b/config/routes.php @@ -13,6 +13,10 @@ ->prefix('/api') ->namePrefix('api_'); + $routes->import('../src/Presentation/Webhook/', 'attribute') + ->prefix('/webhook') + ->namePrefix('webhook_'); + if ($routes->env() === 'dev') { $routes->import('@WebProfilerBundle/Resources/config/routing/wdt.xml')->prefix('/_wdt'); $routes->import('@WebProfilerBundle/Resources/config/routing/profiler.xml')->prefix('/_profiler'); diff --git a/docs/README.md b/docs/README.md index 4c451e8..be60e7e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,13 +23,6 @@ make npm c="run dev" make watch ``` -# Roadmap πŸ—ΊοΈ - -- Add Webhook support on Contentful -- Add Comment system on Articles -- Add tag pages -- _(Add more tests)_ - # Project Architecture πŸ—οΈ ![PHP 8.3](https://img.shields.io/badge/php_8.3-brightgreen?logo=php&logoColor=white) @@ -43,20 +36,23 @@ make watch ### Git -Commit **MUST** respect [Conventional Commits specifications](https://www.conventionalcommits.org/en/v1.0.0/) +Commit **MUST** respect [Conventional Commits specifications](https://www.conventionalcommits.org/en/v1.0.0/) + +Allowed types are : -Allowed types are : - **feat** – a new feature is introduced with the changes - **fix** – a bug fix has occurred -- **chore** – changes that do not relate to a fix or feature and don't modify src or test files (for example updating dependencies) +- **chore** – changes that do not relate to a fix or feature and don't modify src or test files (for example updating + dependencies) - **refactor** – refactored code that neither fixes a bug nor adds a feature - **docs** – updates to documentation such as a the README or other markdown files -- **style** – changes that do not affect the meaning of the code, likely related to code formatting such as white-space, missing semi-colons, and so on. +- **style** – changes that do not affect the meaning of the code, likely related to code formatting such as white-space, + missing semi-colons, and so on. - **test** – including new or correcting previous tests - **perf** – performance improvements - **ci** – continuous integration related - **build** – changes that affect the build system or external dependencies -- **revert** – reverts a previous commit +- **revert** – reverts a previous commit ## Layers @@ -65,7 +61,7 @@ Project not use default Symfony structure but use a multi layer organisation. Th - **Domain** : contain business logic, in our case Models and Repositories Interface. - **Infrastructure** : make link with framework (Symfony) and External services (Contentful, GitHub, etc.). - **Application** : define actions on application, implement CQRS pattern. -- **UI** : in charge of http request/response handling. +- **Presentation** : in charge of http request/response handling. > See [Domain-driven design](https://en.wikipedia.org/wiki/Domain-driven_design). @@ -81,39 +77,30 @@ default `sync` Transport for Query and Command. ## Query Caching -Using `CacheableQueryInterface` on query allow to cache query result. +Using `QueryCache` attribute on query allow to cache query result. -For exemple, `GetArticleQuery` is cached for 1h _(60 * 60 = 3600)_. +For exemple, `GetArticleQuery` is cached for 1h _(3600s)_. ```php +#[QueryCache( + ttl: 3600, + tags: ['get_article', 'article'], +)] final readonly class GetArticleQuery implements CacheableQueryInterface { - public function __construct(public string $identifier, public bool $preview = false) {} - - public function getCacheKey(): string - { - return sprintf('article_%s', $this->identifier); - } - - public function getCacheTtl(): int - { - return $this->preview ? 0 : 3600; - } + // ... } ``` -- `getCacheKey` : key used in cache system to store query result. -- `getCacheTtl` : time-to-live for cache entry in seconds. +- ttl : cache duration +- tags : allow to invalidate multiple cache entries by tag -> **✨ Improvement** : Use an attribute instead of interface for cache metadata. +> To see more, cache implementation is made on `App\Infrastructure\Cache` and used by a Symfony Messenger +> middleware : `App\Infrastructure\Symfony\Messenger\Middleware\CacheMiddleware` ## Cache invalidation -```shell -curl -H "Authorization: Bearer {{token}} https://www.udfn.fr/admin/cache-invalidation?cacheKey={{cacheKey}} -``` - -Cache can be purged from `/admin/cache-invalidation` with `cacheKey` defined in query. +Cache can be purged from `/admin/cache-invalidation` with `tag[]` defined in query. > Routes from /admin/* need a security token to be accessed : see [admin section](#Secure-routes) @@ -188,7 +175,7 @@ and `App\Infrastructure\Symfony\Security\AccessTokenHandler`. **Usage (send a cache invalidation request):** ```shell -curl -H "Authorization: Bearer {{token}}" https://www.udfn.fr/admin/cache-invalidation?cacheKey=articles +curl -H "Authorization: Bearer {{token}}" https://www.udfn.fr/admin/cache-invalidation?tag[]=article ``` > 3 bad login attempt will ban IP for 1 hour. (Configuration from SecurityBundle using RateLimiter component). @@ -233,7 +220,6 @@ window.addEventListener('turbo:load', () => { Countly.track_sessions(); Countly.track_pageview(); Countly.track_errors(); - }); ``` diff --git a/http/.gitignore b/http/.gitignore new file mode 100644 index 0000000..95914b9 --- /dev/null +++ b/http/.gitignore @@ -0,0 +1 @@ +http-client.private.env.json diff --git a/http/admin_cache_invalidation.http b/http/admin_cache_invalidation.http new file mode 100644 index 0000000..f77e81f --- /dev/null +++ b/http/admin_cache_invalidation.http @@ -0,0 +1,6 @@ +# @name Cache Invalidation by tag +GET {{host}}/admin/cache-invalidation?tag[]=article +Content-Type: application/json +Authorization: Bearer {{token}} + +### diff --git a/http/api_article.http b/http/api_article.http new file mode 100644 index 0000000..c9cf22d --- /dev/null +++ b/http/api_article.http @@ -0,0 +1,12 @@ +# @name Get Articles +GET {{host}}/api/articles +Content-Type: application/json + +### + +# @name Get Article By ID +@id = 6J0hWoFCuiuWQtjp3K1mXn +GET {{host}}/api/articles/{{id}} +Content-Type: application/json + +### diff --git a/http/webhook_contentful.http b/http/webhook_contentful.http new file mode 100644 index 0000000..a3f8428 --- /dev/null +++ b/http/webhook_contentful.http @@ -0,0 +1,107 @@ +# @name Contentful Webhook : Publish BlogPage +POST {{host}}/webhook/contentful/publish +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "metadata": { + "tags": [] + }, + "fields": { + "title": { + "fr": "DΓ©buter avec Varnish" + }, + "slug": { + "fr": "debuter-avec-varnish" + }, + "description": { + "fr": "TODO" + }, + "content": { + "fr": "TODO" + }, + "categories": { + "fr": [] + } + }, + "sys": { + "type": "Entry", + "id": "1234", + "space": { + "sys": { + "type": "Link", + "linkType": "Space", + "id": "1234" + } + }, + "environment": { + "sys": { + "id": "master", + "type": "Link", + "linkType": "Environment" + } + }, + "contentType": { + "sys": { + "type": "Link", + "linkType": "ContentType", + "id": "blogPage" + } + }, + "createdBy": { + "sys": { + "type": "Link", + "linkType": "User", + "id": "1234" + } + }, + "updatedBy": { + "sys": { + "type": "Link", + "linkType": "User", + "id": "1234" + } + }, + "revision": 3, + "createdAt": "2023-12-18T19:15:54.485Z", + "updatedAt": "2024-01-14T14:48:10.656Z" + } +} + +### + +# @name Contentful Webhook : Unpublsih BlogPage +POST {{host}}/webhook/contentful/publish +Content-Type: application/json + +{ + "sys": { + "type": "DeletedEntry", + "id": "1234", + "space": { + "sys": { + "type": "Link", + "linkType": "Space", + "id": "1234" + } + }, + "environment": { + "sys": { + "id": "master", + "type": "Link", + "linkType": "Environment" + } + }, + "contentType": { + "sys": { + "type": "Link", + "linkType": "ContentType", + "id": "blogPage" + } + }, + "revision": 3, + "createdAt": "2024-01-14T14:48:15.803Z", + "updatedAt": "2024-01-14T14:48:15.803Z", + "deletedAt": "2024-01-14T14:48:15.803Z" + } +} diff --git a/src/Application/Command/CacheInvalidation/CacheInvalidationCommand.php b/src/Application/Command/CacheInvalidation/CacheInvalidationCommand.php deleted file mode 100644 index f5fbf29..0000000 --- a/src/Application/Command/CacheInvalidation/CacheInvalidationCommand.php +++ /dev/null @@ -1,10 +0,0 @@ -adapter->deleteItem($command->cacheKey); - } -} diff --git a/src/Application/Command/TagCacheInvalidation/TagCacheInvalidationCommand.php b/src/Application/Command/TagCacheInvalidation/TagCacheInvalidationCommand.php new file mode 100644 index 0000000..9ac5e5b --- /dev/null +++ b/src/Application/Command/TagCacheInvalidation/TagCacheInvalidationCommand.php @@ -0,0 +1,12 @@ +messengerCache->invalidateTags($command->tags); + } +} diff --git a/src/Application/Query/CacheableQueryInterface.php b/src/Application/Query/CacheableQueryInterface.php deleted file mode 100644 index 61f6ac9..0000000 --- a/src/Application/Query/CacheableQueryInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -identifier); - } - - public function getCacheTtl(): int - { - return $this->preview ? 0 : 3600; - } + public function __construct( + public string $identifier, + ) {} } diff --git a/src/Application/Query/GetArticle/GetArticleQueryHandler.php b/src/Application/Query/GetArticle/GetArticleQueryHandler.php index ffba828..c2d42ea 100644 --- a/src/Application/Query/GetArticle/GetArticleQueryHandler.php +++ b/src/Application/Query/GetArticle/GetArticleQueryHandler.php @@ -11,10 +11,12 @@ #[AsMessageHandler] final readonly class GetArticleQueryHandler { - public function __construct(private BlogArticleRepository $blogArticleRepository) {} + public function __construct( + private BlogArticleRepository $blogArticleRepository, + ) {} public function __invoke(GetArticleQuery $query): ?BlogArticle { - return $this->blogArticleRepository->getById($query->identifier, $query->preview); + return $this->blogArticleRepository->getById($query->identifier); } } diff --git a/src/Application/Query/GetArticleByFilter/GetArticleByFilterQuery.php b/src/Application/Query/GetArticleByFilter/GetArticleByFilterQuery.php index b781343..364d7e5 100644 --- a/src/Application/Query/GetArticleByFilter/GetArticleByFilterQuery.php +++ b/src/Application/Query/GetArticleByFilter/GetArticleByFilterQuery.php @@ -4,19 +4,12 @@ namespace App\Application\Query\GetArticleByFilter; -use App\Application\Query\CacheableQueryInterface; +use App\Application\Query\QueryCache; -final readonly class GetArticleByFilterQuery implements CacheableQueryInterface +#[QueryCache(ttl: 3600, tags: ['get_article', 'article'])] +final readonly class GetArticleByFilterQuery { - public function __construct(public array $filters) {} - - public function getCacheKey(): string - { - return sprintf('article_filters_%s', md5((string)json_encode($this->filters))); - } - - public function getCacheTtl(): int - { - return 3600; - } + public function __construct( + public array $filters, + ) {} } diff --git a/src/Application/Query/GetArticleByFilter/GetArticleByFilterQueryHandler.php b/src/Application/Query/GetArticleByFilter/GetArticleByFilterQueryHandler.php index 5926b75..fd9290e 100644 --- a/src/Application/Query/GetArticleByFilter/GetArticleByFilterQueryHandler.php +++ b/src/Application/Query/GetArticleByFilter/GetArticleByFilterQueryHandler.php @@ -11,7 +11,9 @@ #[AsMessageHandler] final readonly class GetArticleByFilterQueryHandler { - public function __construct(private BlogArticleRepository $articleRepository) {} + public function __construct( + private BlogArticleRepository $articleRepository, + ) {} public function __invoke(GetArticleByFilterQuery $query): ?BlogArticle { diff --git a/src/Application/Query/GetArticleList/GetArticleListQuery.php b/src/Application/Query/GetArticleList/GetArticleListQuery.php index 9134f82..702ae90 100644 --- a/src/Application/Query/GetArticleList/GetArticleListQuery.php +++ b/src/Application/Query/GetArticleList/GetArticleListQuery.php @@ -4,21 +4,12 @@ namespace App\Application\Query\GetArticleList; -use App\Application\Query\CacheableQueryInterface; +use App\Application\Query\QueryCache; -final readonly class GetArticleListQuery implements CacheableQueryInterface +#[QueryCache(ttl: 3600, tags: ['list_article', 'article'])] +final readonly class GetArticleListQuery { public function __construct( public ?int $limit = null, ) {} - - public function getCacheKey(): string - { - return sprintf('articles_%s', $this->limit); - } - - public function getCacheTtl(): int - { - return 3600; - } } diff --git a/src/Application/Query/GetArticleList/GetArticleListQueryHandler.php b/src/Application/Query/GetArticleList/GetArticleListQueryHandler.php index 66483d3..a0a9440 100644 --- a/src/Application/Query/GetArticleList/GetArticleListQueryHandler.php +++ b/src/Application/Query/GetArticleList/GetArticleListQueryHandler.php @@ -11,7 +11,9 @@ #[AsMessageHandler] final readonly class GetArticleListQueryHandler { - public function __construct(private BlogArticleRepository $articleRepository) {} + public function __construct( + private BlogArticleRepository $articleRepository, + ) {} /** * @return BlogArticle[] diff --git a/src/Application/Query/GetLatestArticles/GetLatestArticlesQuery.php b/src/Application/Query/GetLatestArticles/GetLatestArticlesQuery.php deleted file mode 100644 index d179b30..0000000 --- a/src/Application/Query/GetLatestArticles/GetLatestArticlesQuery.php +++ /dev/null @@ -1,22 +0,0 @@ -limit); - } - - public function getCacheTtl(): int - { - return 3600; - } -} diff --git a/src/Application/Query/GetLatestArticles/GetLatestArticlesQueryHandler.php b/src/Application/Query/GetLatestArticles/GetLatestArticlesQueryHandler.php deleted file mode 100644 index e79cf52..0000000 --- a/src/Application/Query/GetLatestArticles/GetLatestArticlesQueryHandler.php +++ /dev/null @@ -1,24 +0,0 @@ -blogArticleRepository->getLatestArticles($query->limit); - } -} diff --git a/src/Application/Query/GetPreviewArticle/GetPreviewArticleQuery.php b/src/Application/Query/GetPreviewArticle/GetPreviewArticleQuery.php new file mode 100644 index 0000000..91ca7e1 --- /dev/null +++ b/src/Application/Query/GetPreviewArticle/GetPreviewArticleQuery.php @@ -0,0 +1,12 @@ +blogArticleRepository->getById($query->identifier, true); + } +} diff --git a/src/Application/Query/GetProjectList/GetProjectListQuery.php b/src/Application/Query/GetProjectList/GetProjectListQuery.php index daf1b89..7d46b54 100644 --- a/src/Application/Query/GetProjectList/GetProjectListQuery.php +++ b/src/Application/Query/GetProjectList/GetProjectListQuery.php @@ -4,19 +4,12 @@ namespace App\Application\Query\GetProjectList; -use App\Application\Query\CacheableQueryInterface; +use App\Application\Query\QueryCache; -final readonly class GetProjectListQuery implements CacheableQueryInterface +#[QueryCache(ttl: 3600, tags: ['list_project', 'project'])] +final readonly class GetProjectListQuery { - public function __construct(public int $limit = 10) {} - - public function getCacheKey(): string - { - return sprintf('project_list_limit_%s', $this->limit); - } - - public function getCacheTtl(): int - { - return 3600; - } + public function __construct( + public int $limit = 10, + ) {} } diff --git a/src/Application/Query/GetProjectList/GetProjectListQueryHandler.php b/src/Application/Query/GetProjectList/GetProjectListQueryHandler.php index 0504e67..c6a68a8 100644 --- a/src/Application/Query/GetProjectList/GetProjectListQueryHandler.php +++ b/src/Application/Query/GetProjectList/GetProjectListQueryHandler.php @@ -11,10 +11,11 @@ #[AsMessageHandler] final readonly class GetProjectListQueryHandler { - public function __construct(private ProjectRepository $projectRepository) {} + public function __construct( + private ProjectRepository $projectRepository, + ) {} /** - * @param GetProjectListQuery $query * @return Project[] */ public function __invoke(GetProjectListQuery $query): array diff --git a/src/Application/Query/QueryCache.php b/src/Application/Query/QueryCache.php new file mode 100644 index 0000000..b95267f --- /dev/null +++ b/src/Application/Query/QueryCache.php @@ -0,0 +1,14 @@ +logger = new NullLogger(); + } + + public function resolve(object $query): ?QueryCacheConfig + { + $reflectionClass = new \ReflectionClass($query); + if (null === ($cacheAttribute = $reflectionClass->getAttributes(QueryCache::class)[0] ?? null)) { + return null; + } + + return new QueryCacheConfig( + $this->buildCacheKey($reflectionClass, $query), + $cacheAttribute->newInstance()->ttl, + $cacheAttribute->newInstance()->tags, + ); + } + + private function buildCacheKey(\ReflectionClass $class, object $query): string + { + $properties = array_map( + fn (\ReflectionProperty $property) => $property->getValue($query), + $class->getProperties() + ); + + $key = md5(sprintf('%s/%s', $class->getShortName(), json_encode($properties))); + + $this->logger?->info( + sprintf('Cache key for %s with %s is %s', $class->getShortName(), json_encode($properties), $key) + ); + + return $key; + } +} diff --git a/src/Infrastructure/Contentful/Repository/ContentfulBlogArticleRepository.php b/src/Infrastructure/Contentful/Repository/ContentfulBlogArticleRepository.php index ad121bd..1321697 100644 --- a/src/Infrastructure/Contentful/Repository/ContentfulBlogArticleRepository.php +++ b/src/Infrastructure/Contentful/Repository/ContentfulBlogArticleRepository.php @@ -24,14 +24,6 @@ public function __construct( parent::__construct($apiClient, $serializer, $queryBuilder); } - public function getLatestArticles(int $length): array - { - /** @var BlogPageCollection $data */ - $data = $this->query(BlogPageCollection::class, ['limit' => $length]); - - return $this->blogArticleFactory->fromBlogPageCollection($data); - } - public function getById(string $identifier, bool $preview = false): ?BlogArticle { /** @var BlogPage $data */ diff --git a/src/Infrastructure/Symfony/Messenger/Middleware/CacheMiddleware.php b/src/Infrastructure/Symfony/Messenger/Middleware/CacheMiddleware.php index 95f7af4..c66d489 100644 --- a/src/Infrastructure/Symfony/Messenger/Middleware/CacheMiddleware.php +++ b/src/Infrastructure/Symfony/Messenger/Middleware/CacheMiddleware.php @@ -4,10 +4,13 @@ namespace App\Infrastructure\Symfony\Messenger\Middleware; -use App\Application\Query\CacheableQueryInterface; +use App\Infrastructure\Cache\QueryCacheConfig; +use App\Infrastructure\Cache\QueryCacheResolver; +use Psr\Cache\CacheException; +use Psr\Cache\InvalidArgumentException; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerAwareTrait; -use Symfony\Component\Cache\Adapter\AdapterInterface; +use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Middleware\MiddlewareInterface; use Symfony\Component\Messenger\Middleware\StackInterface; @@ -19,12 +22,22 @@ final class CacheMiddleware implements MiddlewareInterface, LoggerAwareInterface { use LoggerAwareTrait; - public function __construct(private readonly AdapterInterface $cache) {} + public function __construct( + private readonly TagAwareAdapterInterface $messengerCache, + private readonly QueryCacheResolver $resolver, + ) {} + /** + * @throws CacheException + * @throws InvalidArgumentException + */ public function handle(Envelope $envelope, StackInterface $stack): Envelope { $message = $envelope->getMessage(); - if (!$message instanceof CacheableQueryInterface) { + + $config = $this->resolver->resolve($message); + + if (!($config instanceof QueryCacheConfig)) { $this->logger?->info( 'Message of {message_class} is not cacheable, skipping', ['message_class' => $message::class] @@ -33,7 +46,7 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope return $this->continue($envelope, $stack); } - $item = $this->cache->getItem($message->getCacheKey()); + $item = $this->messengerCache->getItem($config->key); $this->logger?->info( 'Cache {key} result in hit={hit}', @@ -41,9 +54,12 @@ public function handle(Envelope $envelope, StackInterface $stack): Envelope ); if (!$item->isHit()) { - $item->set($this->continue($envelope, $stack)); - $item->expiresAfter($message->getCacheTtl()); - $this->cache->save($item); + $item + ->set($this->continue($envelope, $stack)) + ->expiresAfter($config->ttl) + ->tag($config->tags); + + $this->messengerCache->save($item); } return $item->get(); diff --git a/src/Presentation/Admin/CacheInvalidationController.php b/src/Presentation/Admin/CacheInvalidationController.php index 2af1233..a5e5c08 100644 --- a/src/Presentation/Admin/CacheInvalidationController.php +++ b/src/Presentation/Admin/CacheInvalidationController.php @@ -4,7 +4,7 @@ namespace App\Presentation\Admin; -use App\Application\Command\CacheInvalidation\CacheInvalidationCommand; +use App\Application\Command\TagCacheInvalidation\TagCacheInvalidationCommand; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\Cache; @@ -19,9 +19,9 @@ { public function __construct(private MessageBusInterface $bus) {} - public function __invoke(#[MapQueryParameter('cacheKey')] string $cacheKey): JsonResponse + public function __invoke(#[MapQueryParameter('tag')] array $tag): JsonResponse { - $this->bus->dispatch(new CacheInvalidationCommand($cacheKey)); + $this->bus->dispatch(new TagCacheInvalidationCommand($tag)); return new JsonResponse(); } diff --git a/src/Presentation/Component/Article/LatestArticleListComponent.php b/src/Presentation/Component/Article/LatestArticleListComponent.php index a0ee89d..573a4ed 100644 --- a/src/Presentation/Component/Article/LatestArticleListComponent.php +++ b/src/Presentation/Component/Article/LatestArticleListComponent.php @@ -4,7 +4,7 @@ namespace App\Presentation\Component\Article; -use App\Application\Query\GetLatestArticles\GetLatestArticlesQuery; +use App\Application\Query\GetArticleList\GetArticleListQuery; use App\Domain\Blogging\BlogArticle; use Symfony\Component\Messenger\HandleTrait; use Symfony\Component\Messenger\MessageBusInterface; @@ -29,6 +29,6 @@ public function __construct(MessageBusInterface $messageBus) */ public function articles(): array { - return $this->handle(new GetLatestArticlesQuery($this->size)); + return $this->handle(new GetArticleListQuery($this->size)); } } diff --git a/src/Presentation/Public/ArticlePreviewController.php b/src/Presentation/Public/ArticlePreviewController.php index 1f9e453..dfc874a 100644 --- a/src/Presentation/Public/ArticlePreviewController.php +++ b/src/Presentation/Public/ArticlePreviewController.php @@ -4,7 +4,7 @@ namespace App\Presentation\Public; -use App\Application\Query\GetArticle\GetArticleQuery; +use App\Application\Query\GetPreviewArticle\GetPreviewArticleQuery; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\AsController; @@ -27,7 +27,7 @@ public function __construct(MessageBusInterface $messageBus) public function __invoke(string $identifier): Response { - if (null === ($article = $this->handle(new GetArticleQuery($identifier, true)))) { + if (null === ($article = $this->handle(new GetPreviewArticleQuery($identifier)))) { throw $this->createNotFoundException(); } diff --git a/src/Presentation/Webhook/ContentfulPublishWebhook.php b/src/Presentation/Webhook/ContentfulPublishWebhook.php new file mode 100644 index 0000000..b90c9b7 --- /dev/null +++ b/src/Presentation/Webhook/ContentfulPublishWebhook.php @@ -0,0 +1,27 @@ +bus->dispatch(new TagCacheInvalidationCommand(['article'])); + + return new JsonResponse(status: Response::HTTP_OK); + } +}