diff --git a/.gitignore b/.gitignore
index fca9e04..6cffc53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-templates/frontOffice/default/header_highlights/assets/node_modules
\ No newline at end of file
+templates/frontOffice/default/header_highlights/assets/node_modules
+/.phpunit.cache/
diff --git a/Config/module.xml b/Config/module.xml
index d1c63fb..fc1e106 100644
--- a/Config/module.xml
+++ b/Config/module.xml
@@ -13,7 +13,7 @@
en_US
fr_FR
- 2.1.4
+ 2.2.0
Loïc MO
diff --git a/HeaderHighlights.php b/HeaderHighlights.php
index cffec0a..7e2ebf7 100644
--- a/HeaderHighlights.php
+++ b/HeaderHighlights.php
@@ -4,12 +4,15 @@
use HeaderHighlights\Model\HeaderHighlights as HeaderHighlightsModel;
use HeaderHighlights\Model\HeaderHighlightsImage;
+use HeaderHighlights\SharedCache\HeaderHighlightsPurgeListener;
+use HeaderHighlights\SharedCache\HeaderHighlightsSharedCache;
use Propel\Runtime\Connection\ConnectionInterface;
use Propel\Runtime\Propel;
use Symfony\Component\DependencyInjection\Loader\Configurator\ServicesConfigurator;
use Symfony\Component\Finder\Finder;
use Thelia\Core\Template\TemplateDefinition;
use Thelia\Core\Install\Database;
+use Thelia\Core\SharedCache\SharedCacheContributorInterface;
use Thelia\Model\Base\CategoryQuery;
use Thelia\Model\ConfigQuery;
use Thelia\Model\LangQuery;
@@ -30,9 +33,21 @@ class HeaderHighlights extends BaseModule
public static function configureServices(ServicesConfigurator $servicesConfigurator): void
{
$servicesConfigurator->load(self::getModuleCode().'\\', __DIR__)
- ->exclude([__DIR__.'/I18n/*', __DIR__.'/Config/**/*.php', __DIR__.'/Tests/*', __DIR__.'/HeaderHighlights.php'])
+ ->exclude([__DIR__.'/I18n/*', __DIR__.'/Config/**/*.php', __DIR__.'/Tests/*', __DIR__.'/SharedCache/*', __DIR__.'/HeaderHighlights.php'])
->autowire(true)
->autoconfigure(true);
+
+ // The shared cache of the public pages is a soft dependency: on a core that
+ // predates it the interface does not exist, and reflecting a class that
+ // implements it would be a fatal error, not a skip.
+ if (interface_exists(SharedCacheContributorInterface::class)) {
+ $servicesConfigurator->set(HeaderHighlightsSharedCache::class)
+ ->autoconfigure(true);
+
+ $servicesConfigurator->set(HeaderHighlightsPurgeListener::class)
+ ->autowire(true)
+ ->autoconfigure(true);
+ }
}
/**
diff --git a/Readme.md b/Readme.md
index 40ddcd6..6ac9e4a 100644
--- a/Readme.md
+++ b/Readme.md
@@ -56,6 +56,16 @@ unstyled band.
The call to action is the theme's `Molecules:Button:Base` component, `size` `large` on the lead
block and `small` on the short ones.
+## Shared cache of the public pages
+
+On a Thelia core that brings the shared cache of the public pages
+(`shared_cache_mode`), the module declares `/api/header-highlights` as a
+resource every visitor reads the same: a page that shows the highlights can
+still be kept by the proxy, tagged with `header_highlights`, and saving or
+deleting a highlight or its image purges that key. Uploading a new picture
+purges every page, like any image of a module. On an older core the module
+behaves as before.
+
## Hook
You must define 3 hooks to render (one for html, one for css and one for js)
diff --git a/SharedCache/HeaderHighlightsPurgeListener.php b/SharedCache/HeaderHighlightsPurgeListener.php
new file mode 100644
index 0000000..737f177
--- /dev/null
+++ b/SharedCache/HeaderHighlightsPurgeListener.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace HeaderHighlights\SharedCache;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Thelia\Core\SharedCache\SharedCacheInvalidator;
+
+/**
+ * Purges the pages that show the highlights when one of their rows is written or deleted.
+ *
+ * The configuration screen saves the models directly, so the Propel events of the four
+ * tables are the only thing every write goes through. They are named by their string: the
+ * classes Propel generates for them are not loaded before the kernel boots.
+ */
+final readonly class HeaderHighlightsPurgeListener implements EventSubscriberInterface
+{
+ private const TABLES = ['header_highlights', 'header_highlights_i18n', 'header_highlights_image', 'header_highlights_image_i18n'];
+
+ public function __construct(
+ private SharedCacheInvalidator $invalidator,
+ ) {
+ }
+
+ public static function getSubscribedEvents(): array
+ {
+ $events = [];
+
+ foreach (self::TABLES as $table) {
+ $events['propel.post.save.'.$table] = 'purge';
+ $events['propel.post.delete.'.$table] = 'purge';
+ }
+
+ return $events;
+ }
+
+ public function purge(): void
+ {
+ $this->invalidator->purge([HeaderHighlightsSharedCache::KEY]);
+ }
+}
diff --git a/SharedCache/HeaderHighlightsSharedCache.php b/SharedCache/HeaderHighlightsSharedCache.php
new file mode 100644
index 0000000..d652cf8
--- /dev/null
+++ b/SharedCache/HeaderHighlightsSharedCache.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace HeaderHighlights\SharedCache;
+
+use Thelia\Core\SharedCache\SharedCacheContributorInterface;
+
+/**
+ * The highlights are the same for every visitor: a page that reads them through
+ * `/api/header-highlights` may still be kept by the shared cache of the public pages,
+ * tagged with the key a change of the highlights purges.
+ */
+final readonly class HeaderHighlightsSharedCache implements SharedCacheContributorInterface
+{
+ public const KEY = 'header_highlights';
+
+ public const RESOURCE = '/api/header-highlights';
+
+ public function readableResources(): array
+ {
+ return [self::RESOURCE => [self::KEY]];
+ }
+
+ public function shareableViews(): array
+ {
+ return [];
+ }
+}
diff --git a/Tests/Integration/SharedCache/HeaderHighlightsSharedCacheTest.php b/Tests/Integration/SharedCache/HeaderHighlightsSharedCacheTest.php
new file mode 100644
index 0000000..9c12f41
--- /dev/null
+++ b/Tests/Integration/SharedCache/HeaderHighlightsSharedCacheTest.php
@@ -0,0 +1,99 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace HeaderHighlights\Tests\Integration\SharedCache;
+
+use HeaderHighlights\Model\HeaderHighlights as HeaderHighlightsModel;
+use HeaderHighlights\Model\HeaderHighlightsImage;
+use HeaderHighlights\SharedCache\HeaderHighlightsSharedCache;
+use Thelia\Core\SharedCache\SharedCacheContributorInterface;
+use Thelia\Core\SharedCache\SharedCacheInvalidationListener;
+use Thelia\Core\SharedCache\SharedCacheKeyCollector;
+use Thelia\Core\SharedCache\SharedCachePurgerInterface;
+use Thelia\Test\IntegrationTestCase;
+
+/**
+ * The highlights do not keep a page out of the shared cache of the public pages, and a
+ * change of the highlights purges the pages that show them.
+ */
+final class HeaderHighlightsSharedCacheTest extends IntegrationTestCase
+{
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ if (!interface_exists(SharedCacheContributorInterface::class)) {
+ self::markTestSkipped('The core of this shop predates the shared cache of the public pages.');
+ }
+ }
+
+ public function testAPageThatReadsTheHighlightsCanStillBeSharedAndIsTaggedWithThem(): void
+ {
+ $collector = static::getContainer()->get(SharedCacheKeyCollector::class);
+ $collector->reset();
+
+ $collector->record(HeaderHighlightsSharedCache::RESOURCE);
+
+ self::assertTrue($collector->isComplete());
+ self::assertSame([HeaderHighlightsSharedCache::KEY], $collector->keys());
+ }
+
+ public function testSavingAHighlightPurgesThePagesThatShowIt(): void
+ {
+ $purger = $this->recordingPurger();
+
+ $highlight = (new HeaderHighlightsModel())->setDisplayType('desktop')->setImageBlock('1');
+ $highlight->setLocale('en_US')->setTitle('Autumn sale');
+ $highlight->save($this->getPropelConnection());
+ static::getContainer()->get(SharedCacheInvalidationListener::class)->flush();
+
+ self::assertSame([[HeaderHighlightsSharedCache::KEY]], $purger->calls);
+ }
+
+ public function testDeletingTheImageOfAHighlightPurgesThePagesThatShowIt(): void
+ {
+ $purger = $this->recordingPurger();
+ $highlight = (new HeaderHighlightsModel())->setDisplayType('mobile')->setImageBlock('2');
+ $highlight->save($this->getPropelConnection());
+ $image = (new HeaderHighlightsImage())->setHeaderHighlightsId((int) $highlight->getId())->setFile('probe.webp');
+ $image->save($this->getPropelConnection());
+ static::getContainer()->get(SharedCacheInvalidationListener::class)->flush();
+ $purger->calls = [];
+
+ $image->delete($this->getPropelConnection());
+ static::getContainer()->get(SharedCacheInvalidationListener::class)->flush();
+
+ self::assertSame([[HeaderHighlightsSharedCache::KEY]], $purger->calls);
+ }
+
+ /**
+ * @return SharedCachePurgerInterface&object{calls: list>}
+ */
+ private function recordingPurger(): SharedCachePurgerInterface
+ {
+ $purger = new class implements SharedCachePurgerInterface {
+ /** @var list> */
+ public array $calls = [];
+
+ public function purge(array $keys): void
+ {
+ $this->calls[] = $keys;
+ }
+ };
+
+ static::getContainer()->set(SharedCachePurgerInterface::class, $purger);
+
+ return $purger;
+ }
+}
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 0000000..8592ed3
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,18 @@
+# Shared cache symbols of a core newer than the published one, used only when interface_exists() finds them
+# (HeaderHighlights::configureServices). Kept out of the report so that PHPStan runs against the published core;
+# an entry the core catches up with is not reported.
+parameters:
+ # PHPStan cannot ignore a class that implements an unknown interface: the class is left out of the analysis instead (still scanned, so its name resolves).
+ excludePaths:
+ analyse:
+ - SharedCache/HeaderHighlightsSharedCache.php
+ reportUnmatchedIgnoredErrors: false
+ ignoreErrors:
+ -
+ message: '#^Parameter \$invalidator of method HeaderHighlights\\SharedCache\\HeaderHighlightsPurgeListener::__construct\(\) has invalid type Thelia\\Core\\SharedCache\\SharedCacheInvalidator\.$#'
+ count: 1
+ path: SharedCache/HeaderHighlightsPurgeListener.php
+ -
+ message: '#^Property HeaderHighlights\\SharedCache\\HeaderHighlightsPurgeListener::\$invalidator has unknown class Thelia\\Core\\SharedCache\\SharedCacheInvalidator as its type\.$#'
+ count: 1
+ path: SharedCache/HeaderHighlightsPurgeListener.php
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..1ed8f2d
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tests/Integration
+
+
+