Skip to content
Open
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
46 changes: 12 additions & 34 deletions .github/workflows/backend-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,12 @@
php:
- '8.1'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6

- name: Setup PHP Action
uses: shivammathur/setup-php@v2
- uses: ibexa/gh-workflows/actions/composer-install@main

Check failure on line 21 in .github/workflows/backend-ci.yaml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use full commit SHA hash for this dependency.

See more on https://sonarcloud.io/project/issues?id=ibexa_http-cache&issues=AZ7fbA7s92W6J5I6dTdS&open=AZ7fbA7s92W6J5I6dTdS&pullRequest=80
with:
php-version: ${{ matrix.php }}
coverage: none
extensions: 'pdo_sqlite, gd'
tools: cs2pr

- uses: ramsey/composer-install@v3
with:
dependency-versions: highest
gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }}
gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }}

- name: Run code style check
run: composer run-script check-cs -- --format=checkstyle | cs2pr
Expand All @@ -43,19 +36,12 @@
- '8.3'
- '8.4'
steps:
- uses: actions/checkout@v5

- name: Setup PHP Action
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
extensions: 'pdo_sqlite, gd'
tools: cs2pr
- uses: actions/checkout@v6

- uses: ramsey/composer-install@v3
- uses: ibexa/gh-workflows/actions/composer-install@main

Check failure on line 41 in .github/workflows/backend-ci.yaml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use full commit SHA hash for this dependency.

See more on https://sonarcloud.io/project/issues?id=ibexa_http-cache&issues=AZ7fbA7s92W6J5I6dTdT&open=AZ7fbA7s92W6J5I6dTdT&pullRequest=80
with:
dependency-versions: highest
gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }}
gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }}

- name: Run PHPStan analysis
run: composer run-script phpstan
Expand All @@ -75,20 +61,12 @@
composer_options: [ "" ]

steps:
- uses: actions/checkout@v5

- name: Setup PHP Action
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
extensions: pdo_sqlite, gd
tools: cs2pr
- uses: actions/checkout@v6

- uses: ramsey/composer-install@v3
- uses: ibexa/gh-workflows/actions/composer-install@main

Check failure on line 66 in .github/workflows/backend-ci.yaml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use full commit SHA hash for this dependency.

See more on https://sonarcloud.io/project/issues?id=ibexa_http-cache&issues=AZ7fbA7s92W6J5I6dTdU&open=AZ7fbA7s92W6J5I6dTdU&pullRequest=80
with:
dependency-versions: highest
composer-options: "${{ matrix.composer_options }}"
gh-client-id: ${{ secrets.AUTOMATION_CLIENT_ID }}
gh-client-secret: ${{ secrets.AUTOMATION_CLIENT_SECRET }}

- name: Setup problem matchers for PHPUnit
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
Expand Down
2 changes: 1 addition & 1 deletion features/setup/symfonyCache.feature
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ index 9982c21..03ac40a 100644

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

return function (array $context) {
return static function (array $context) {
- return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
+ $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
+ Request::enableHttpMethodParameterOverride();
Expand Down
8 changes: 7 additions & 1 deletion src/bundle/Resources/config/event.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ services:
$contentHandler: '@Ibexa\Core\Persistence\Cache\ContentHandler'
$isTranslationAware: '%ibexa.http_cache.translation_aware.enabled%'

Ibexa\HttpCache\EventSubscriber\CachePurge\BinaryFileHttpCachePurgeSubscriber:
arguments:
$cacheManager: '@fos_http_cache.cache_manager'

Ibexa\HttpCache\EventSubscriber\CachePurge\:
resource: '../../../lib/EventSubscriber/CachePurge/*'
exclude: '../../../lib/EventSubscriber/CachePurge/ContentEventsSubscriber.php'
exclude:
- '../../../lib/EventSubscriber/CachePurge/ContentEventsSubscriber.php'
- '../../../lib/EventSubscriber/CachePurge/BinaryFileHttpCachePurgeSubscriber.php'
arguments:
$purgeClient: '@ibexa.http_cache.purge_client'
$locationHandler: '@Ibexa\Core\Persistence\Cache\LocationHandler'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\HttpCache\EventSubscriber\CachePurge;

use FOS\HttpCacheBundle\CacheManager;
use Ibexa\Contracts\Core\Repository\Events\Content\PublishVersionEvent;
use Ibexa\Core\FieldType\BinaryBase\Value as BinaryBaseValue;
use Ibexa\Core\FieldType\Image\Value as ImageValue;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class BinaryFileHttpCachePurgeSubscriber implements EventSubscriberInterface
{
private CacheManager $cacheManager;

public function __construct(CacheManager $cacheManager)
{
$this->cacheManager = $cacheManager;
}

public static function getSubscribedEvents(): array
{
return [
PublishVersionEvent::class => 'onPublishVersion',
];
}

public function onPublishVersion(PublishVersionEvent $event): void
{
$content = $event->getContent();
$purged = [];

foreach ($content->getFields() as $field) {
$value = $field->getValue();

if (!$value instanceof ImageValue && !$value instanceof BinaryBaseValue) {
continue;
}

$uri = $value->uri;

if ($uri === null || $uri === '' || isset($purged[$uri])) {
continue;
}

$this->cacheManager->invalidatePath($uri);
Comment thread
konradoboza marked this conversation as resolved.
$purged[$uri] = true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\HttpCache\EventSubscriber\CachePurge;

use FOS\HttpCache\ProxyClient\Invalidation\PurgeCapable;
use FOS\HttpCacheBundle\CacheManager;
use Ibexa\Contracts\Core\Repository\Events\Content\PublishVersionEvent;
use Ibexa\Contracts\Core\Repository\Values\Content\Content;
use Ibexa\Contracts\Core\Repository\Values\Content\Field;
use Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo;
use Ibexa\Core\FieldType\BinaryFile\Value as BinaryFileValue;
use Ibexa\Core\FieldType\Image\Value as ImageValue;
use Ibexa\HttpCache\EventSubscriber\CachePurge\BinaryFileHttpCachePurgeSubscriber;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

final class BinaryFileHttpCachePurgeSubscriberTest extends TestCase
{
private PurgeCapable $proxyClient;

private BinaryFileHttpCachePurgeSubscriber $subscriber;

protected function setUp(): void
{
$this->proxyClient = $this->createMock(PurgeCapable::class);
$this->subscriber = new BinaryFileHttpCachePurgeSubscriber(
new CacheManager(
$this->proxyClient,
$this->createMock(UrlGeneratorInterface::class)
),
);
}

public function testGetSubscribedEvents(): void
{
self::assertArrayHasKey(
PublishVersionEvent::class,
BinaryFileHttpCachePurgeSubscriber::getSubscribedEvents()
);
}

/**
* @param \Ibexa\Contracts\Core\Repository\Values\Content\Field[] $fields
*/
private function buildEvent(array $fields): PublishVersionEvent
{
$content = $this->createMock(Content::class);
$content->method('getFields')->willReturn($fields);

return new PublishVersionEvent(
$content,
$this->createMock(VersionInfo::class),
[],
);
}

/**
* @return iterable<string, array{\Ibexa\Contracts\Core\Repository\Values\Content\Field[]}>
*/
public function fieldsThatDoNotTriggerPurgeProvider(): iterable
{
yield 'no fields' => [[]];

yield 'non-binary field value' => [[new Field(['value' => new \stdClass()])]];

$imageWithNullUri = new ImageValue();
$imageWithNullUri->uri = null;
yield 'image value with null URI' => [[new Field(['value' => $imageWithNullUri])]];

$imageWithEmptyUri = new ImageValue();
$imageWithEmptyUri->uri = '';
yield 'image value with empty URI' => [[new Field(['value' => $imageWithEmptyUri])]];
}

/**
* @dataProvider fieldsThatDoNotTriggerPurgeProvider
*
* @param \Ibexa\Contracts\Core\Repository\Values\Content\Field[] $fields
*/
public function testFieldsThatDoNotTriggerPurge(array $fields): void
{
$this->proxyClient->expects(self::never())->method('purge');

$this->subscriber->onPublishVersion($this->buildEvent($fields));
}

/**
* @return iterable<string, array{\Ibexa\Contracts\Core\Repository\Values\Content\Field[], string}>
*/
public function fieldsThatTriggerPurgeProvider(): iterable
{
$imageValue = new ImageValue();
$imageValue->uri = '/var/site/storage/images/foo.jpg';
yield 'image value with URI' => [[new Field(['value' => $imageValue])], '/var/site/storage/images/foo.jpg'];

$binaryValue = new BinaryFileValue();
$binaryValue->uri = '/var/site/storage/original/application/foo.pdf';
yield 'binary file value with URI' => [[new Field(['value' => $binaryValue])], '/var/site/storage/original/application/foo.pdf'];
}

/**
* @dataProvider fieldsThatTriggerPurgeProvider
*
* @param \Ibexa\Contracts\Core\Repository\Values\Content\Field[] $fields
* @param string $expectedUri
*/
public function testFieldsThatTriggerPurge(array $fields, string $expectedUri): void
{
$this->proxyClient
->expects(self::once())
->method('purge')
->with($expectedUri, []);

$this->subscriber->onPublishVersion($this->buildEvent($fields));
}

public function testDuplicateUriIsInvalidatedOnlyOnce(): void
{
$uri = '/var/site/storage/images/same.jpg';

$imageValue1 = new ImageValue();
$imageValue1->uri = $uri;

$imageValue2 = new ImageValue();
$imageValue2->uri = $uri;

$this->proxyClient
->expects(self::once())
->method('purge')
->with($uri, []);

$this->subscriber->onPublishVersion($this->buildEvent([
new Field(['value' => $imageValue1]),
new Field(['value' => $imageValue2]),
]));
}

public function testMultipleDistinctUrisAreEachInvalidated(): void
{
$imageValue = new ImageValue();
$imageValue->uri = '/var/site/storage/images/a.jpg';

$binaryValue = new BinaryFileValue();
$binaryValue->uri = '/var/site/storage/original/application/b.pdf';

$this->proxyClient
->expects(self::exactly(2))
->method('purge')
->withConsecutive(
['/var/site/storage/images/a.jpg', []],
['/var/site/storage/original/application/b.pdf', []],
);

$this->subscriber->onPublishVersion($this->buildEvent([
new Field(['value' => $imageValue]),
new Field(['value' => $binaryValue]),
]));
}

public function testMixedFieldsOnlyInvalidatesBinaryAndImageUris(): void
{
$imageValue = new ImageValue();
$imageValue->uri = '/var/site/storage/images/photo.jpg';

$this->proxyClient
->expects(self::once())
->method('purge')
->with('/var/site/storage/images/photo.jpg', []);

$this->subscriber->onPublishVersion($this->buildEvent([
new Field(['value' => 'plain text value']),
new Field(['value' => $imageValue]),
new Field(['value' => 42]),
]));
}
}
Loading