Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
templates/frontOffice/default/header_highlights/assets/node_modules
templates/frontOffice/default/header_highlights/assets/node_modules
/.phpunit.cache/
2 changes: 1 addition & 1 deletion Config/module.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<language>en_US</language>
<language>fr_FR</language>
</languages>
<version>2.1.4</version>
<version>2.2.0</version>
<authors>
<author>
<name>Loïc MO</name>
Expand Down
17 changes: 16 additions & 1 deletion HeaderHighlights.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}

/**
Expand Down
10 changes: 10 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
52 changes: 52 additions & 0 deletions SharedCache/HeaderHighlightsPurgeListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* 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]);
}
}
39 changes: 39 additions & 0 deletions SharedCache/HeaderHighlightsSharedCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* 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 [];
}
}
99 changes: 99 additions & 0 deletions Tests/Integration/SharedCache/HeaderHighlightsSharedCacheTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Thelia package.
* http://www.thelia.net
*
* (c) OpenStudio <info@thelia.net>
*
* 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<list<string>>}
*/
private function recordingPurger(): SharedCachePurgerInterface
{
$purger = new class implements SharedCachePurgerInterface {
/** @var list<list<string>> */
public array $calls = [];

public function purge(array $keys): void
{
$this->calls[] = $keys;
}
};

static::getContainer()->set(SharedCachePurgerInterface::class, $purger);

return $purger;
}
}
18 changes: 18 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>

<!-- Integration tests of the module, run from the root of a Thelia shop that has
it installed and active, against the prepared test database:

php bin/test-prepare
vendor/bin/phpunit -c local/modules/HeaderHighlights

Each test runs inside a transaction rolled back afterwards
(Thelia\Test\IntegrationTestCase). -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.5/phpunit.xsd"
bootstrap="../../../tests/bootstrap.php"
colors="true"
failOnWarning="true"
failOnRisky="true"
cacheDirectory=".phpunit.cache">
<php>
<ini name="error_reporting" value="-1"/>
<server name="SHELL_VERBOSITY" value="-1"/>
<server name="KERNEL_CLASS" value="App\Kernel"/>
<server name="APP_ENV" value="test" force="true"/>
<server name="APP_DEBUG" value="0" force="true"/>
<server name="SYMFONY_DEPRECATIONS_HELPER" value="weak"/>
<env name="APP_SECRET" value="287f2178af9e0c250e8219a9df833504"/>
</php>

<testsuites>
<testsuite name="integration">
<directory>Tests/Integration</directory>
</testsuite>
</testsuites>
</phpunit>
Loading