Skip to content

Commit

Permalink
feat: add cache invalidation by tag (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrien-chinour authored Apr 7, 2024
1 parent 1b427fd commit 0b91e27
Show file tree
Hide file tree
Showing 38 changed files with 396 additions and 211 deletions.
2 changes: 0 additions & 2 deletions compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3.7'

services:
web:
container_name: blog.nginx
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion config/packages/framework.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -132,7 +135,7 @@
'middleware' => array_filter([
$container->env() === 'dev' ? StopwatchMiddleware::class : null,
LoggerMiddleware::class,
$container->env() === 'dev' ? null : CacheMiddleware::class,
CacheMiddleware::class,
]),
]);

Expand Down
4 changes: 2 additions & 2 deletions config/packages/security.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
->security(false);

$adminFirewall = $security->firewall('admin')
->pattern('^/admin/')
->pattern('^/(admin|webhook)/')
->security(true)
->stateless(true)
->provider(ADMIN_USER_PROVIDER);
Expand All @@ -34,6 +34,6 @@
->lazy(true);

$security->accessControl()
->path('^/admin')
->path('^/(admin|webhook)')
->roles(['ROLE_ADMIN']);
};
4 changes: 4 additions & 0 deletions config/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
58 changes: 22 additions & 36 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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).
Expand All @@ -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)
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -233,7 +220,6 @@ window.addEventListener('turbo:load', () => {
Countly.track_sessions();
Countly.track_pageview();
Countly.track_errors();
});
```

Expand Down
1 change: 1 addition & 0 deletions http/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
http-client.private.env.json
6 changes: 6 additions & 0 deletions http/admin_cache_invalidation.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# @name Cache Invalidation by tag
GET {{host}}/admin/cache-invalidation?tag[]=article
Content-Type: application/json
Authorization: Bearer {{token}}

###
12 changes: 12 additions & 0 deletions http/api_article.http
Original file line number Diff line number Diff line change
@@ -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

###
107 changes: 107 additions & 0 deletions http/webhook_contentful.http
Original file line number Diff line number Diff line change
@@ -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"
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace App\Application\Command\TagCacheInvalidation;

final readonly class TagCacheInvalidationCommand
{
public function __construct(
public array $tags
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace App\Application\Command\TagCacheInvalidation;

use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Contracts\Cache\TagAwareCacheInterface;

#[AsMessageHandler]
final readonly class TagCacheInvalidationCommandHandler
{
public function __construct(
private TagAwareCacheInterface $messengerCache
) {}

public function __invoke(TagCacheInvalidationCommand $command): void
{
$this->messengerCache->invalidateTags($command->tags);
}
}
12 changes: 0 additions & 12 deletions src/Application/Query/CacheableQueryInterface.php

This file was deleted.

Loading

0 comments on commit 0b91e27

Please sign in to comment.