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
37 changes: 36 additions & 1 deletion doc/8-disable-caching-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,43 @@ To achieve this, you can use the `CacheActivator` to disable tagging and invalid
self::getContainer()->get(CacheActivator::class)->deactivateCaching();

// Your test code here

self::assertSame('this is amazing!', $result);
}
```

## Suppress automatic tagging for a specific code block

Sometimes you need to load Pimcore elements for business logic without tagging the current response with those elements. For example, loading related products to calculate a price should not cause the response to be tagged with all those products.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this isn’t the best example..


Use `CacheActivator::withoutAutomaticTagging()` to run a block of code with automatic tagging suppressed:

```php
$result = $cacheActivator->withoutAutomaticTagging(function () use ($id) {
// Automatic tagging is disabled here — loading this element
// will not tag the current response.
return $this->repository->find($id);
});
```

### Selectively tagging within the block

If you still want to tag the response with specific tags inside the block, use a generator and `yield` the tags you need:

```php
$result = $cacheActivator->withoutAutomaticTagging(function () use ($id) {
$element = $this->repository->find($id);

// Yield only the tags you explicitly want applied to the response.
yield CacheTag::fromElement($element);

return $element;
});
```

You can yield both `CacheTag` and `CacheTags` objects. All yielded tags are collected and applied to the response after the block completes. The return value of the generator is returned by `withoutAutomaticTagging()`.

### State restoration

The previous caching state is always restored after the block completes, even if an exception is thrown. If caching was already disabled before calling `withoutAutomaticTagging()`, it remains disabled afterwards.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use “afterward” for US English consistency.

Replace “afterwards” with “afterward”.

✏️ Proposed wording fix
-...it remains disabled afterwards.
+...it remains disabled afterward.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The previous caching state is always restored after the block completes, even if an exception is thrown. If caching was already disabled before calling `withoutAutomaticTagging()`, it remains disabled afterwards.
The previous caching state is always restored after the block completes, even if an exception is thrown. If caching was already disabled before calling `withoutAutomaticTagging()`, it remains disabled afterward.
🧰 Tools
🪛 LanguageTool

[locale-violation] ~55-~55: In American English, ‘afterward’ is the preferred variant. ‘Afterwards’ is more commonly used in British English and other dialects.
Context: ...utomaticTagging()`, it remains disabled afterwards.

(AFTERWARDS_US)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@doc/8-disable-caching-behavior.md` at line 55, The sentence describing state
restoration uses "afterwards" but should use US English "afterward"; update the
wording in the doc sentence that mentions the caching state being restored after
the block completes (the line referencing withoutAutomaticTagging()) to replace
"afterwards" with "afterward" so the sentence reads "...it remains disabled
afterward."


3 changes: 1 addition & 2 deletions src/Cache/ResponseTagger/TraceableResponseTagger.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
<?php declare(strict_types=1);

namespace Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger;

Expand Down
3 changes: 1 addition & 2 deletions src/CacheActivator.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function deactivateCaching(): void
/**
* @template T
*
* @param \Closure(): (T|\Generator<int, CacheTag|CacheTags, null, T>) $fn
* @param \Closure(): (T|\Generator<array-key, CacheTag|CacheTags, null, T>) $fn
*
* @return T
*/
Expand Down Expand Up @@ -64,7 +64,6 @@ public function withoutAutomaticTagging(\Closure $fn): mixed

$tags = $tags->with($yielded);
}

$result = $result->getReturn();
}
} finally {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types=1);

use App\Controller\WithoutAutomaticTaggingController;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

return static function (RoutingConfigurator $routes) {
$routes->add('without_automatic_tagging', '/without-automatic-tagging')
->controller(WithoutAutomaticTaggingController::class);
};
63 changes: 63 additions & 0 deletions tests/Integration/Tagging/WithoutAutomaticTaggingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types=1);

namespace Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Tagging;

use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\ArrangeCacheTest;
use Neusta\Pimcore\HttpCacheBundle\Tests\Integration\Helpers\TestAssetFactory;
use Neusta\Pimcore\TestingFramework\Database\ResetDatabase;
use Neusta\Pimcore\TestingFramework\Test\Attribute\ConfigureExtension;
use Neusta\Pimcore\TestingFramework\Test\Attribute\ConfigureRoute;
use Neusta\Pimcore\TestingFramework\Test\ConfigurableWebTestcase;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;

#[ConfigureRoute(__DIR__ . '/../Fixtures/without_automatic_tagging_route.php')]
final class WithoutAutomaticTaggingTest extends ConfigurableWebTestcase
{
use ArrangeCacheTest;
use ResetDatabase;

private KernelBrowser $client;

protected function setUp(): void
{
$this->client = self::createClient();
}

/**
* @test
*/
#[ConfigureExtension('neusta_pimcore_http_cache', [
'elements' => [
'assets' => true,
],
])]
public function response_is_not_tagged_when_asset_is_loaded_without_automatic_tagging(): void
{
self::arrange(static fn () => TestAssetFactory::simpleAsset()->save());

$this->client->request('GET', '/without-automatic-tagging?id=42');

$response = $this->client->getResponse();
self::assertSame(200, $response->getStatusCode());
self::assertNull($response->headers->get('X-Cache-Tags'));
}

/**
* @test
*/
#[ConfigureExtension('neusta_pimcore_http_cache', [
'elements' => [
'assets' => true,
],
])]
public function response_is_tagged_with_manually_yielded_tag_when_loaded_without_automatic_tagging(): void
{
self::arrange(static fn () => TestAssetFactory::simpleAsset()->save());

$this->client->request('GET', '/without-automatic-tagging?id=42&manual_tag=true');

$response = $this->client->getResponse();
self::assertSame(200, $response->getStatusCode());
self::assertSame('a42', $response->headers->get('X-Cache-Tags'));
}
}
16 changes: 8 additions & 8 deletions tests/Unit/Cache/ResponseTagger/TraceableResponseTaggerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,31 @@ final class TraceableResponseTaggerTest extends TestCase
{
use ProphecyTrait;

private TraceableResponseTagger $collectTagsResponseTagger;
private TraceableResponseTagger $traceableResponseTagger;
Comment thread
jan888adams marked this conversation as resolved.

/** @var ObjectProphecy<ResponseTagger> */
private ObjectProphecy $innerTagger;

protected function setUp(): void
{
$this->innerTagger = $this->prophesize(ResponseTagger::class);
$this->collectTagsResponseTagger = new TraceableResponseTagger($this->innerTagger->reveal());
$this->traceableResponseTagger = new TraceableResponseTagger($this->innerTagger->reveal());
}

/**
* @test
*/
public function tag_should_collect_tags(): void
{
$this->collectTagsResponseTagger->tag(
$this->traceableResponseTagger->tag(
new CacheTags(
CacheTag::fromString('tag1'),
CacheTag::fromString('tag2'),
));

self::assertSame(
'tag1,tag2',
$this->collectTagsResponseTagger->recordedTags->toString(),
$this->traceableResponseTagger->recordedTags->toString(),
);
}

Expand All @@ -52,7 +52,7 @@ public function tag_should_forward_tags_to_inner_tagger(): void
CacheTag::fromString('tag2'),
);

$this->collectTagsResponseTagger->tag($tags);
$this->traceableResponseTagger->tag($tags);

$this->innerTagger->tag($tags)->shouldHaveBeenCalledOnce();
}
Expand All @@ -62,16 +62,16 @@ public function tag_should_forward_tags_to_inner_tagger(): void
*/
public function reset_should_reset_collected_tags(): void
{
$this->collectTagsResponseTagger->tag(
$this->traceableResponseTagger->tag(
new CacheTags(
CacheTag::fromString('tag1'),
CacheTag::fromString('tag2'),
));

$this->collectTagsResponseTagger->reset();
$this->traceableResponseTagger->reset();

self::assertTrue(
$this->collectTagsResponseTagger->recordedTags->isEmpty(),
$this->traceableResponseTagger->recordedTags->isEmpty(),
);
}
}
133 changes: 132 additions & 1 deletion tests/Unit/CacheActivatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,28 @@

namespace Neusta\Pimcore\HttpCacheBundle\Tests\Unit;

use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag;
use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTags;
use Neusta\Pimcore\HttpCacheBundle\Cache\ResponseTagger;
use Neusta\Pimcore\HttpCacheBundle\CacheActivator;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;

final class CacheActivatorTest extends TestCase
{
use ProphecyTrait;

private CacheActivator $cacheActivator;

/** @var ObjectProphecy<ResponseTagger> */
private ObjectProphecy $responseTagger;

protected function setUp(): void
{
$this->cacheActivator = new CacheActivator();
$this->responseTagger = $this->prophesize(ResponseTagger::class);
$this->cacheActivator = new CacheActivator(fn () => $this->responseTagger->reveal());
}

/**
Expand Down Expand Up @@ -42,4 +54,123 @@ public function it_must_be_activated_after_activateCaching_is_called(): void

self::assertTrue($this->cacheActivator->isCachingActive());
}

/**
* @test
*/
public function without_automatic_tagging_returns_closure_result(): void
{
$result = $this->cacheActivator->withoutAutomaticTagging(static fn () => 'my_result');

self::assertSame('my_result', $result);
}

/**
* @test
*/
public function without_automatic_tagging_disables_caching_during_execution(): void
{
$activator = $this->cacheActivator;
$wasCachingActive = null;

$this->cacheActivator->withoutAutomaticTagging(static function () use ($activator, &$wasCachingActive) {
$wasCachingActive = $activator->isCachingActive();
});

self::assertFalse($wasCachingActive);
}

/**
* @test
*/
public function without_automatic_tagging_restores_caching_state_after_execution(): void
{
$this->cacheActivator->withoutAutomaticTagging(static fn () => null);

self::assertTrue($this->cacheActivator->isCachingActive());
}

/**
* @test
*/
public function without_automatic_tagging_restores_inactive_state_when_caching_was_already_disabled(): void
{
$this->cacheActivator->deactivateCaching();

$this->cacheActivator->withoutAutomaticTagging(static fn () => null);

self::assertFalse($this->cacheActivator->isCachingActive());
}

/**
* @test
*/
public function without_automatic_tagging_restores_caching_state_even_when_closure_throws(): void
{
try {
$this->cacheActivator->withoutAutomaticTagging(static fn () => throw new \RuntimeException('error'));
} catch (\RuntimeException) {
}

self::assertTrue($this->cacheActivator->isCachingActive());
}

/**
* @test
*/
public function without_automatic_tagging_tags_a_yielded_cache_tag(): void
{
$tag = CacheTag::fromString('42');

$this->cacheActivator->withoutAutomaticTagging(static function () use ($tag) {
yield $tag;
});

$this->responseTagger->tag(Argument::that(
static fn (CacheTags $tags) => $tags->toString() === $tag->toString(),
))->shouldHaveBeenCalledOnce();
}

/**
* @test
*/
public function without_automatic_tagging_tags_a_yielded_cache_tags_collection(): void
{
$tags = CacheTags::fromStrings(['17', '42']);

$this->cacheActivator->withoutAutomaticTagging(static function () use ($tags) {
yield $tags;
});

$this->responseTagger->tag(Argument::that(
static fn (CacheTags $t) => $t->toString() === $tags->toString(),
))->shouldHaveBeenCalledOnce();
}

/**
* @test
*/
public function without_automatic_tagging_returns_generator_return_value(): void
{
$result = $this->cacheActivator->withoutAutomaticTagging(static function () {
yield CacheTag::fromString('42');

return 'generator_result';
});

self::assertSame('generator_result', $result);
}

/**
* @test
*/
public function without_automatic_tagging_throws_logic_exception_for_invalid_yield(): void
{
$this->expectException(\LogicException::class);
$this->expectExceptionMessageMatches('/Invalid yielded value at index 1 \(key: 0\)/');

$this->cacheActivator->withoutAutomaticTagging(static function () {
yield 'not_a_cache_tag';
});
}
}
Loading