From c442656f82aa10c39f39a6ab474cb3a195ebb620 Mon Sep 17 00:00:00 2001 From: Jacob Dreesen Date: Thu, 29 Jan 2026 16:56:30 +0100 Subject: [PATCH 1/8] Add `EventType` info to `ElementInvalidationEvent` --- src/Element/ElementInvalidationEvent.php | 4 +++- src/Element/EventType.php | 10 ++++++++++ src/Element/InvalidateElementListener.php | 8 ++++---- 3 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 src/Element/EventType.php diff --git a/src/Element/ElementInvalidationEvent.php b/src/Element/ElementInvalidationEvent.php index 1e0fcaf..2052e48 100644 --- a/src/Element/ElementInvalidationEvent.php +++ b/src/Element/ElementInvalidationEvent.php @@ -12,15 +12,17 @@ final class ElementInvalidationEvent extends Event public bool $cancel = false; private function __construct( + public readonly EventType $type, public readonly ElementInterface $element, public readonly ElementType $elementType, private CacheTags $cacheTags, ) { } - public static function fromElement(ElementInterface $element): self + public static function fromElement(ElementInterface $element, EventType $type): self { return new self( + $type, $element, ElementType::fromElement($element), CacheTags::fromElement($element), diff --git a/src/Element/EventType.php b/src/Element/EventType.php new file mode 100644 index 0000000..b65285e --- /dev/null +++ b/src/Element/EventType.php @@ -0,0 +1,10 @@ +invalidateElement($event->getElement()); + $this->invalidateElement($event->getElement(), EventType::Update); } public function onDelete(ElementEventInterface $event): void { - $this->invalidateElement($event->getElement()); + $this->invalidateElement($event->getElement(), EventType::Delete); } - private function invalidateElement(ElementInterface $element): void + private function invalidateElement(ElementInterface $element, EventType $type): void { - $invalidationEvent = $this->dispatcher->dispatch(ElementInvalidationEvent::fromElement($element)); + $invalidationEvent = $this->dispatcher->dispatch(ElementInvalidationEvent::fromElement($element, $type)); \assert($invalidationEvent instanceof ElementInvalidationEvent); if ($invalidationEvent->cancel) { From 44a65668850cdf8c8b8b9308ddc4926610c80a70 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Fri, 27 Feb 2026 09:33:19 +0100 Subject: [PATCH 2/8] Add CLAUDE.md for Claude Code project context Provides codebase overview, conventions, architecture patterns, and development commands to streamline AI-assisted development. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c6f1d4a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# CLAUDE.md + +## Project Overview + +**teamneusta/pimcore-http-cache-bundle** — A Symfony/Pimcore bundle that adds active HTTP cache invalidation via cache tags. It extends FOSHttpCacheBundle for Pimcore, automatically tagging responses and invalidating caches when Pimcore elements (documents, assets, data objects) change. + +## Quick Reference + +```bash +# Install dependencies (Docker) +bin/composer install + +# Run tests +bin/composer tests # or: bin/run-tests + +# Code style +bin/composer cs:fix # fix issues +bin/composer cs:check # check only (dry-run) + +# Static analysis +bin/composer phpstan # PHPStan level 8 +``` + +Without Docker, use `composer` directly instead of `bin/composer`. + +## Project Structure + +``` +src/ # Main source (Neusta\Pimcore\HttpCacheBundle\) +├── Adapter/FOSHttpCache/ # FOSHttpCache adapter implementations +├── Cache/ +│ ├── CacheInvalidator/ # Invalidation decorators +│ ├── CacheTagChecker/ # Tag validation per element type +│ ├── CacheType/ # Cache type strategy implementations +│ └── ResponseTagger/ # Response tagging decorator chain +├── DependencyInjection/ # Symfony DI extension + compiler passes +├── Element/ # Pimcore element handling +├── Exception/ # Custom exceptions +└── NeustaPimcoreHttpCacheBundle.php +tests/ +├── Unit/ # Fast isolated tests (Prophecy mocks) +├── Integration/ # Tests with real Pimcore kernel +└── app/ # Minimal Pimcore app for integration tests +config/services.php # Symfony DI service definitions +``` + +## Coding Conventions + +- **PHP**: 8.1 / 8.2; every file starts with `declare(strict_types=1);` +- **Style**: Symfony coding standards via PHP CS Fixer (`@Symfony`, `@Symfony:risky`) +- **Static analysis**: PHPStan level 8 +- **Indentation**: 4 spaces for PHP; 2 spaces for JSON/YAML; tabs for .neon +- **Line endings**: LF +- **Namespace**: `Neusta\Pimcore\HttpCacheBundle\` (PSR-4 → `src/`) +- **Tests namespace**: `Neusta\Pimcore\HttpCacheBundle\Tests\` (PSR-4 → `tests/`) + +## Architecture & Patterns + +- **Decorator pattern**: Services are heavily decorated (e.g., `ResponseTagger` chain: base → `RemoveDisabledTagsResponseTagger` → `OnlyWhenActiveResponseTagger` → `CacheTagCollectionResponseTagger`) +- **Immutable value objects**: `CacheTags` (collection) and `CacheTag` use `with()` methods / readonly properties +- **Event-driven**: Listens to Pimcore element lifecycle events; dispatches `ElementTaggingEvent` and `ElementInvalidationEvent` +- **Single-method interfaces**: `CacheInvalidator::invalidate(CacheTags)` and `ResponseTagger::tag(CacheTags)` +- **Cache tag format**: Assets `a{id}`, Documents `d{id}`, Objects `o{id}` + +## Testing + +- **PHPUnit 9.6** with Prophecy for mocking and `dg/bypass-finals` for final classes +- Integration tests use `teamneusta/pimcore-testing-framework` with a MariaDB service +- Test attributes: `#[ConfigureExtension]`, `#[ConfigureRoute]` +- CI matrix: PHP 8.1 (lowest + highest deps), PHP 8.2 (highest deps) + +## Key Dependencies + +- `pimcore/pimcore: ^10.6 || ^11.2` +- `friendsofsymfony/http-cache-bundle: ^2.17` +- `symfony/*: ^5.4 || ^6.4` + +## CI / QA + +GitHub Actions runs two workflows: +- **tests.yaml**: PHPUnit across PHP version matrix with MariaDB +- **qa.yaml**: composer validate, PHP CS Fixer check, PHPStan From 8429f990f88dbfdb810b2bb24b59b3deb28fc732 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Fri, 27 Feb 2026 11:14:25 +0100 Subject: [PATCH 3/8] Add Claude Code rules, skills, and update CLAUDE.md Rules (.claude/rules/): - code-style: PHP conventions with examples from codebase - testing: PHPUnit + Prophecy patterns and conventions - architecture: Decorator chains, adapters, lifecycle flows - static-analysis: PHPStan level 8 requirements - bundle-usage: How the bundle works for developers Skills (.claude/skills/): - php-best-practices: PHP 8.x audit with 45+ rules - php-pro: Senior PHP dev with framework reference guides - tdd: Test-driven development workflow - debug: Systematic root-cause debugging - code-review: Handle review feedback with technical rigor - brainstorm: Ideas to designs to specs - web-search: Web search & extraction via inference.sh - writing-skills: TDD for skill documentation - humanizer: Remove AI writing patterns Co-Authored-By: Claude Opus 4.6 --- .claude/rules/architecture.md | 120 +++++ .claude/rules/bundle-usage.md | 121 +++++ .claude/rules/code-style.md | 107 +++++ .claude/rules/static-analysis.md | 6 + .claude/rules/testing.md | 65 +++ .claude/skills/brainstorm/SKILL.md | 89 ++++ .claude/skills/code-review/SKILL.md | 202 ++++++++ .claude/skills/debug/SKILL.md | 310 ++++++++++++ .../skills/debug/condition-based-waiting.md | 40 ++ .claude/skills/debug/defense-in-depth.md | 26 + .claude/skills/debug/root-cause-tracing.md | 45 ++ .claude/skills/humanizer/SKILL.md | 358 ++++++++++++++ .claude/skills/php-best-practices/SKILL.md | 158 ++++++ .../rules/error-custom-exceptions.md | 76 +++ .../rules/modern-constructor-promotion.md | 74 +++ .../php-best-practices/rules/modern-enums.md | 95 ++++ .../rules/modern-first-class-callables.md | 54 +++ .../rules/modern-match-expression.md | 59 +++ .../rules/modern-readonly-properties.md | 84 ++++ .../rules/solid-dependency-inversion.md | 87 ++++ .../rules/solid-interface-segregation.md | 86 ++++ .../rules/solid-single-responsibility.md | 100 ++++ .../rules/type-strict-mode.md | 54 +++ .claude/skills/php-pro/SKILL.md | 80 ++++ .../php-pro/references/async-patterns.md | 159 +++++++ .../php-pro/references/laravel-patterns.md | 174 +++++++ .../php-pro/references/modern-php-features.md | 162 +++++++ .../php-pro/references/symfony-patterns.md | 183 +++++++ .../php-pro/references/testing-quality.md | 159 +++++++ .claude/skills/tdd/SKILL.md | 317 +++++++++++++ .claude/skills/web-search/SKILL.md | 129 +++++ .claude/skills/writing-skills/SKILL.md | 448 ++++++++++++++++++ CLAUDE.md | 41 +- 33 files changed, 4258 insertions(+), 10 deletions(-) create mode 100644 .claude/rules/architecture.md create mode 100644 .claude/rules/bundle-usage.md create mode 100644 .claude/rules/code-style.md create mode 100644 .claude/rules/static-analysis.md create mode 100644 .claude/rules/testing.md create mode 100644 .claude/skills/brainstorm/SKILL.md create mode 100644 .claude/skills/code-review/SKILL.md create mode 100644 .claude/skills/debug/SKILL.md create mode 100644 .claude/skills/debug/condition-based-waiting.md create mode 100644 .claude/skills/debug/defense-in-depth.md create mode 100644 .claude/skills/debug/root-cause-tracing.md create mode 100644 .claude/skills/humanizer/SKILL.md create mode 100644 .claude/skills/php-best-practices/SKILL.md create mode 100644 .claude/skills/php-best-practices/rules/error-custom-exceptions.md create mode 100644 .claude/skills/php-best-practices/rules/modern-constructor-promotion.md create mode 100644 .claude/skills/php-best-practices/rules/modern-enums.md create mode 100644 .claude/skills/php-best-practices/rules/modern-first-class-callables.md create mode 100644 .claude/skills/php-best-practices/rules/modern-match-expression.md create mode 100644 .claude/skills/php-best-practices/rules/modern-readonly-properties.md create mode 100644 .claude/skills/php-best-practices/rules/solid-dependency-inversion.md create mode 100644 .claude/skills/php-best-practices/rules/solid-interface-segregation.md create mode 100644 .claude/skills/php-best-practices/rules/solid-single-responsibility.md create mode 100644 .claude/skills/php-best-practices/rules/type-strict-mode.md create mode 100644 .claude/skills/php-pro/SKILL.md create mode 100644 .claude/skills/php-pro/references/async-patterns.md create mode 100644 .claude/skills/php-pro/references/laravel-patterns.md create mode 100644 .claude/skills/php-pro/references/modern-php-features.md create mode 100644 .claude/skills/php-pro/references/symfony-patterns.md create mode 100644 .claude/skills/php-pro/references/testing-quality.md create mode 100644 .claude/skills/tdd/SKILL.md create mode 100644 .claude/skills/web-search/SKILL.md create mode 100644 .claude/skills/writing-skills/SKILL.md diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md new file mode 100644 index 0000000..199f07c --- /dev/null +++ b/.claude/rules/architecture.md @@ -0,0 +1,120 @@ +# Architecture + +## Overview + +Symfony bundle for Pimcore that adds HTTP cache tagging and invalidation. Bridges Pimcore's element lifecycle to FOSHttpCacheBundle via adapters, decorators, and events. + +- Service definitions live in `config/services.php` (PHP-based DI, not YAML) +- Core interfaces are single-method: `CacheInvalidator::invalidate(CacheTags)` and `ResponseTagger::tag(CacheTags)` + +## Decorator Chains + +Both `ResponseTagger` and `CacheInvalidator` are built as decoration chains. Lower priority = outer decorator (executes first). + +**ResponseTagger chain:** +``` +CacheTagCollectionResponseTagger (priority +1, removed if profiler absent) + → OnlyWhenActiveResponseTagger (priority -100, checks CacheActivator) + → RemoveDisabledTagsResponseTagger (priority -99, filters via CacheTagChecker) + → ResponseTaggerAdapter (base, bridges to FOSHttpCache) +``` + +**CacheInvalidator chain:** +``` +OnlyWhenActiveCacheInvalidator (priority -100, checks CacheActivator) + → RemoveDisabledTagsCacheInvalidator (priority -99, filters via CacheTagChecker) + → CacheInvalidatorAdapter (base, bridges to FOSHttpCache CacheManager) +``` + +Each decorator has a single responsibility: activation check, tag filtering, or profiler collection. + +## Adapter Layer + +`ResponseTaggerAdapter` and `CacheInvalidatorAdapter` sit at the bottom of the decoration chains. They convert the bundle's `CacheTags` value object to string arrays and delegate to FOSHttpCacheBundle (`FosResponseTagger::addTags()` and `CacheManager::invalidateTags()`). This isolates the FOSHttpCache dependency to these two classes. + +## Element Lifecycle Flows + +**Tagging (element load → response tagged):** +1. Pimcore fires `POST_LOAD` event (asset/document/object) +2. `TagElementListener` receives it, creates `ElementTaggingEvent`, dispatches it +3. Application listeners can add tags or set `cancel = true` +4. If not cancelled, calls `ResponseTagger::tag()` → decoration chain → FOSHttpCache + +**Invalidation (element save/delete → cache purged):** +1. Pimcore fires `POST_UPDATE` or `PRE_DELETE` event +2. `InvalidateElementListener` receives it (skips `saveVersionOnly`/`autoSave`) +3. Creates `ElementInvalidationEvent`, dispatches it +4. Application listeners can add related tags or cancel +5. If not cancelled, calls `CacheInvalidator::invalidate()` → decoration chain → FOSHttpCache + +Listeners are registered dynamically — only for enabled element types (configured in YAML). + +## CacheTagChecker Composition + +Checkers decide if a tag is enabled based on configuration. They compose via constructor injection, not decoration: + +``` +ElementCacheTagChecker + ├── inner: StaticCacheTagChecker (handles custom/empty types via cache_types config) + ├── asset: AssetCacheTagChecker (loads asset, checks type against config) + ├── document: DocumentCacheTagChecker (loads document, checks type against config) + └── object: ObjectCacheTagChecker (loads object, checks type + class against config) +``` + +`ElementCacheTagChecker` routes element tags to the right specific checker via `match` on `ElementType`. Non-element tags fall through to `StaticCacheTagChecker`. + +Each element checker: loads the element by ID from `ElementRepository` → checks if the element's type (and class for objects) is enabled in config → defaults to `true` if not explicitly configured. + +## CacheType Hierarchy + +`CacheType` interface defines how a tag string is formatted: + +| Type | `applyTo("42")` | `toString()` | `isEmpty()` | When used | +|------|-----------------|--------------|-------------|-----------| +| `ElementCacheType(Asset)` | `"a42"` | `"a"` | `false` | Pimcore elements | +| `ElementCacheType(Document)` | `"d42"` | `"d"` | `false` | Pimcore elements | +| `ElementCacheType(Object)` | `"o42"` | `"o"` | `false` | Pimcore elements | +| `CustomCacheType("product")` | `"product-42"` | `"product"` | `false` | User-defined types | +| `EmptyCacheType` | `"42"` | `""` | `true` | Raw string tags | + +Note: Element types use direct concatenation (`a42`), custom types use dash separator (`product-42`). + +`CacheTypeFactory` creates the right type: `createFromElement()` → `ElementCacheType`, `createFromString("asset")` → `ElementCacheType`, `createFromString("product")` → `CustomCacheType`, `createEmpty()` → `EmptyCacheType`. + +Reserved prefixes `a`, `d`, `o` cannot be used for custom types (`ElementCacheType::isReserved()`). + +## Immutable Value Objects + +- **`CacheTag`**: private constructor, created via `fromString()` / `fromElement()`. Holds `tag` string + `CacheType`. `toString()` delegates to `$type->applyTo($tag)`. +- **`CacheTags`**: immutable collection indexed by tag string. `with()` returns new instance with merged tags. `withoutDisabled($checker)` returns new instance with filtered tags. Implements `IteratorAggregate`. + +## Configuration Flow + +``` +YAML config + → Configuration tree builder (normalizes defaults: folder=false, email=false, hardlink=false) + → NeustaPimcoreHttpCacheExtension::loadInternal() + → Injects cache_types into StaticCacheTagChecker + → Injects element config into AssetCacheTagChecker / DocumentCacheTagChecker / ObjectCacheTagChecker + → Dynamically adds event listener tags to TagElementListener / InvalidateElementListener + → Stores full config as container parameter for DataCollector +``` + +Key: if an element type is disabled in config, its event listeners are never registered — zero overhead. + +## Compiler Pass + +`DisableDataCollectorPass`: if the Symfony profiler service is absent, removes `CacheTagCollectionResponseTagger` and `DataCollector`. This avoids the memory overhead of collecting tags in production where the profiler isn't available. + +## Design Patterns Summary + +| Pattern | Where | Why | +|---------|-------|-----| +| Decorator | ResponseTagger + CacheInvalidator chains | Composable, ordered processing with single responsibility per layer | +| Adapter | ResponseTaggerAdapter, CacheInvalidatorAdapter | Isolates FOSHttpCache dependency | +| Strategy | CacheType hierarchy | Different tag formatting per type | +| Chain of Responsibility | CacheTagChecker composition | Each checker handles its own tag types | +| Event-Driven | ElementTaggingEvent, ElementInvalidationEvent | Loose coupling, extensibility for application code | +| Immutable Value Object | CacheTags, CacheTag | Prevents accidental mutation, thread safety | +| Named Constructor | CacheTag::fromString(), CacheTags::fromElement() | Expressive, validated object creation | +| Named Exception Factory | InvalidArgumentException::becauseCacheTagIsEmpty() | Readable, centralized error messages | diff --git a/.claude/rules/bundle-usage.md b/.claude/rules/bundle-usage.md new file mode 100644 index 0000000..5f1a7bd --- /dev/null +++ b/.claude/rules/bundle-usage.md @@ -0,0 +1,121 @@ +# Bundle Usage & Concepts + +## What This Bundle Does + +Adds automatic HTTP cache invalidation to Pimcore via cache tags. When a Pimcore element (document, asset, data object) is loaded during a request, the response gets tagged. When that element changes, the cache is invalidated. Works with reverse proxies (Varnish, Fastly) via FOSHttpCacheBundle. + +## Automatic Behavior + +**Tagging** — when an element is loaded during a request: +1. Pimcore fires a `POST_LOAD` event +2. Bundle dispatches `ElementTaggingEvent` +3. Response is tagged with `a{id}` (asset), `d{id}` (document), or `o{id}` (object) +4. Tags appear in the `X-Cache-Tags` header + +**Invalidation** — when an element is saved or deleted: +1. Pimcore fires `POST_UPDATE` or `PRE_DELETE` +2. Bundle dispatches `ElementInvalidationEvent` +3. Cache tags are invalidated via FOSHttpCacheBundle +4. Reverse proxy purges matching responses +5. Skipped for `saveVersionOnly` and `autoSave` operations + +## Configuration + +```yaml +neusta_pimcore_http_cache: + elements: + assets: + enabled: true + types: + folder: false # disabled by default + documents: + enabled: true + types: + email: false # disabled by default + folder: false # disabled by default + hardlink: false # disabled by default + objects: + enabled: true + types: + folder: false # disabled by default + classes: + MyClass: false # disable specific data object class + cache_types: + my_custom_type: true # must be defined here before use +``` + +## Public API + +Services available via autowiring: + +- **`CacheActivator`** — toggle caching on/off programmatically +- **`CacheInvalidator`** — manually invalidate cache tags +- **`ResponseTagger`** — manually tag the current response + +Value objects: + +- **`CacheTags`** — immutable collection; create via `fromString()`, `fromStrings()`, `fromElement()`, `fromElements()`; combine with `with()` +- **`CacheTag`** — single tag; create via `fromString()`, `fromElement()` +- **`CacheTypeFactory`** — creates `ElementCacheType`, `CustomCacheType`, or `EmptyCacheType` + +## Events + +Both events share the same API — `element`, `elementType`, `addTag()`, `addTags()`, `cacheTags()`, and `cancel`: + +**`ElementTaggingEvent`** — fired before tagging a response: +```php +#[AsEventListener] +final class AddRelatedTags +{ + public function __invoke(ElementTaggingEvent $event): void + { + // Add related element tags + $event->addTags(CacheTags::fromElements($event->element->getRelatedProducts())); + + // Or cancel tagging entirely + $event->cancel = true; + } +} +``` + +**`ElementInvalidationEvent`** — fired before invalidating cache: +```php +#[AsEventListener] +final class InvalidateRelated +{ + public function __invoke(ElementInvalidationEvent $event): void + { + // Invalidate related content too + $event->addTag(CacheTag::fromElement($relatedElement)); + } +} +``` + +## Custom Cache Types + +For grouping tags beyond the built-in element types: + +1. Register in config: `cache_types: { product_category: true }` +2. Use in code: + ```php + $tag = CacheTag::fromString('42', new CustomCacheType('product_category')); + // Produces tag: "product_category-42" + ``` +3. Invalidate by type: + ```php + $cacheInvalidator->invalidate( + CacheTags::fromStrings(['42'], new CustomCacheType('product_category')) + ); + ``` + +Reserved prefixes `a`, `d`, `o` cannot be used for custom types. + +## Three Ways to Disable Caching + +1. **Configuration** — permanently disable types/classes in YAML +2. **Events** — set `$event->cancel = true` conditionally +3. **`CacheActivator`** — `deactivateCaching()` disables everything at runtime (useful in tests) + +## Profiler + +Shows in the Symfony profiler toolbar when the profiler is enabled. Displays all cache tags applied to the response and the current bundle configuration. diff --git a/.claude/rules/code-style.md b/.claude/rules/code-style.md new file mode 100644 index 0000000..ad1aa05 --- /dev/null +++ b/.claude/rules/code-style.md @@ -0,0 +1,107 @@ +# Code Style + +## General + +- Every PHP file starts with ` $types */ + /** @return array */ + ``` +- Omit doc blocks when native type hints are sufficient + +## Trailing Commas + +- Always use trailing commas in multiline structures — constructor params, arrays, function calls, match arms + +## Global Functions + +- Fully qualified for native functions: `\count()`, `\sprintf()`, `\assert()`, `\in_array()` + +## Null Handling + +- Null coalescing `??` preferred: `$type ?? CacheTypeFactory::createEmpty()` +- Null coalescing assignment `??=` for lazy init: `$prefixes ??= array_map(...)` +- Early return with assignment for null checks: + ```php + if (!$id = $element->getId()) { + throw InvalidArgumentException::becauseElementHasNoId(); + } + ``` + +## Exceptions + +- Named static factory methods on exception classes: + ```php + throw InvalidArgumentException::becauseCacheTagIsEmpty(); + throw InvalidArgumentException::becauseCacheTypeIsReserved($type); + ``` +- Multi-line `\sprintf()` for longer messages is fine +- Custom exception interface (`PimcoreHttpCacheException`) as marker + +## Modern PHP Features + +- **Match expressions** over switch: + ```php + return match ($tag->type->type) { + ElementType::Asset => $this->asset->isEnabled($tag), + ElementType::Document => $this->document->isEnabled($tag), + ElementType::Object => $this->object->isEnabled($tag), + }; + ``` +- **Arrow functions** for short closures: `fn ($tag) => CacheTag::fromString($tag)` +- **First-class callable syntax**: `array_map(CacheTag::fromElement(...), $elements)` +- **Variadic parameters** with union types: `CacheTag|self ...$tags` +- **Backed enums** with methods: + ```php + enum ElementType: string + { + case Asset = 'asset'; + case Document = 'document'; + case Object = 'object'; + } + ``` +- **Spread operator** for arrays: `[...$newTags, ...$tag->tags]` +- **`@no-named-arguments`** annotation on variadic constructors + +## Concatenation + +- Spaces around `.` operator: `'foo' . 'bar'` not `'foo'.'bar'` diff --git a/.claude/rules/static-analysis.md b/.claude/rules/static-analysis.md new file mode 100644 index 0000000..7b2b5dd --- /dev/null +++ b/.claude/rules/static-analysis.md @@ -0,0 +1,6 @@ +# Static Analysis + +- PHPStan level 8 (maximum) — all code in `src/` must pass +- Run with `composer phpstan` +- PHPStan extensions in use: phpstan-phpunit, phpstan-symfony, phpstan-prophecy +- Do not lower the PHPStan level or add ignoreErrors for new code — fix the issues instead diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000..c280d3d --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,65 @@ +# Testing + +## Setup + +- Test framework: PHPUnit 9.6 with Prophecy for mocking +- Unit tests go in `tests/Unit/`, integration tests in `tests/Integration/` +- Mirror the `src/` directory structure in tests (e.g., `src/Cache/Foo.php` -> `tests/Unit/Cache/FooTest.php`) +- Use `dg/bypass-finals` when mocking final classes +- Integration tests use the Pimcore test kernel in `tests/app/` +- Run tests with `composer tests` + +## Test Class Conventions + +- All test classes are `final class` +- Unit tests: `{ClassUnderTest}Test` (e.g., `CacheActivatorTest`) +- Integration tests: `{Feature}Test` (e.g., `InvalidateAssetTest`) + +## Method Naming + +- Always use the `@test` annotation — never the `test` prefix +- Method names are **snake_case** and describe the expected behavior: + ```php + /** @test */ + public function it_must_be_activated_by_default(): void + + /** @test */ + public function response_is_invalidated_when_asset_is_updated(): void + ``` + +## Mocking with Prophecy + +- Use `ProphecyTrait` on the test class +- Declare mocks as typed `ObjectProphecy` properties with `@var` PHPDoc: + ```php + /** @var ObjectProphecy */ + private $cacheInvalidator; + ``` +- Create mocks in `setUp()` with `$this->prophesize()` +- Pass mocks to constructors via `->reveal()` +- Verify interactions with `shouldHaveBeenCalledOnce()`, `shouldNotHaveBeenCalled()` +- Use `Argument::any()`, `Argument::type()`, `Argument::which()` for flexible matching + +## Assertions + +- Use `self::assertTrue()`, `self::assertSame()`, etc. (static calls) for state verification +- Use Prophecy expectations for interaction verification + +## Data Providers + +- Name them `{something}Provider` with `iterable` return type +- Use `yield` with descriptive named keys: + ```php + public function elementProvider(): iterable + { + yield 'Asset' => ['event' => new AssetEvent($asset->reveal())]; + } + ``` +- Reference via `@dataProvider` annotation + +## Integration Tests + +- Extend `ConfigurableKernelTestCase` or `ConfigurableWebTestcase` +- Use traits: `ArrangeCacheTest`, `ProphecyTrait`, `ResetDatabase` +- Use PHP 8 attributes: `#[ConfigureExtension]`, `#[ConfigureRoute]` +- Set up test data with `self::arrange()` diff --git a/.claude/skills/brainstorm/SKILL.md b/.claude/skills/brainstorm/SKILL.md new file mode 100644 index 0000000..ee6c228 --- /dev/null +++ b/.claude/skills/brainstorm/SKILL.md @@ -0,0 +1,89 @@ +--- +name: brainstorm +description: Turn ideas into fully formed designs and specs through collaborative dialogue. Explores context, asks clarifying questions, proposes approaches, presents design for approval, writes design doc. +allowed-tools: Read, Grep, Glob, Bash(git *), Write, Edit +argument-hint: "[idea-or-feature-description]" +--- + +# Brainstorming Ideas Into Designs + +## Overview + +Help turn ideas into fully formed designs and specs through natural collaborative dialogue. + +Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval. + +## Anti-Pattern: "This Is Too Simple To Need A Design" + +Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval. + +## Checklist + +You MUST create a task for each of these items and complete them in order: + +1. **Explore project context** — check files, docs, recent commits +2. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria +3. **Propose 2-3 approaches** — with trade-offs and your recommendation +4. **Present design** — in sections scaled to their complexity, get user approval after each section +5. **Write design doc** — save to `docs/plans/YYYY-MM-DD--design.md` and commit +6. **Transition to implementation** — invoke writing-plans skill to create implementation plan + +## Process Flow + +``` +Explore project context + → Ask clarifying questions + → Propose 2-3 approaches + → Present design sections + → User approves design? + → no, revise → Present design sections + → yes → Write design doc + → Invoke writing-plans skill +``` + +The terminal state is invoking writing-plans. Do NOT invoke any other implementation skill. The ONLY skill you invoke after brainstorming is writing-plans. + +## The Process + +### Understanding the Idea + +- Check out the current project state first (files, docs, recent commits) +- Ask questions one at a time to refine the idea +- Prefer multiple choice questions when possible, but open-ended is fine too +- **Only one question per message** — if a topic needs more exploration, break it into multiple questions +- Focus on understanding: purpose, constraints, success criteria + +### Exploring Approaches + +- Propose 2-3 different approaches with trade-offs +- Present options conversationally with your recommendation and reasoning +- Lead with your recommended option and explain why + +### Presenting the Design + +- Once you believe you understand what you're building, present the design +- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced +- Ask after each section whether it looks right so far +- Cover: architecture, components, data flow, error handling, testing +- Be ready to go back and clarify if something doesn't make sense + +## After the Design + +### Documentation + +- Write the validated design to `docs/plans/YYYY-MM-DD--design.md` +- Commit the design document to git + +### Implementation + +- Invoke the writing-plans skill to create a detailed implementation plan +- Do NOT invoke any other skill. writing-plans is the next step. + +## Key Principles + +- **One question at a time** — Don't overwhelm with multiple questions +- **Multiple choice preferred** — Easier to answer than open-ended when possible +- **YAGNI ruthlessly** — Remove unnecessary features from all designs +- **Explore alternatives** — Always propose 2-3 approaches before settling +- **Incremental validation** — Present design, get approval before moving on +- **Be flexible** — Go back and clarify when something doesn't make sense diff --git a/.claude/skills/code-review/SKILL.md b/.claude/skills/code-review/SKILL.md new file mode 100644 index 0000000..9364bc5 --- /dev/null +++ b/.claude/skills/code-review/SKILL.md @@ -0,0 +1,202 @@ +--- +name: code-review +description: Handle code review feedback with technical rigor. Verify before implementing, ask before assuming, push back when wrong. No performative agreement. +allowed-tools: Read, Grep, Glob, Bash(gh *), Edit, Write +argument-hint: "[pr-number-or-review-link]" +--- + +# Code Review Reception + +## Overview + +Code review requires technical evaluation, not emotional performance. + +Core principle: **Verify before implementing. Ask before assuming. Technical correctness over social comfort.** + +## The Response Pattern + +WHEN receiving code review feedback: + +``` +1. READ: Complete feedback without reacting +2. UNDERSTAND: Restate requirement in own words (or ask) +3. VERIFY: Check against codebase reality +4. EVALUATE: Technically sound for THIS codebase? +5. RESPOND: Technical acknowledgment or reasoned pushback +6. IMPLEMENT: One item at a time, test each +``` + +## Forbidden Responses + +**NEVER:** +- "You're absolutely right!" +- "Great point!" / "Excellent feedback!" +- "Let me implement that now" (before verification) +- "Thanks for catching that!" / "Thanks for [anything]" +- ANY gratitude expression + +**INSTEAD:** +- Restate the technical requirement +- Ask clarifying questions +- Push back with technical reasoning if wrong +- Just start working (actions > words) + +Why no thanks: Actions speak. Just fix it. The code itself shows you heard the feedback. + +If you catch yourself about to write "Thanks": DELETE IT. State the fix instead. + +## Handling Unclear Feedback + +``` +IF any item is unclear: + STOP - do not implement anything yet + ASK for clarification on unclear items +``` + +WHY: Items may be related. Partial understanding = wrong implementation. + +Example: + +Human partner: "Fix 1-6" +You understand 1,2,3,6. Unclear on 4,5. + +- WRONG: Implement 1,2,3,6 now, ask about 4,5 later +- RIGHT: "I understand items 1,2,3,6. Need clarification on 4 and 5 before proceeding." + +## Source-Specific Handling + +### From Your Human Partner + +- Trusted — implement after understanding +- Still ask if scope unclear +- No performative agreement +- Skip to action or technical acknowledgment + +### From External Reviewers + +``` +BEFORE implementing: + 1. Check: Technically correct for THIS codebase? + 2. Check: Breaks existing functionality? + 3. Check: Reason for current implementation? + 4. Check: Works on all platforms/versions? + 5. Check: Does reviewer understand full context? + +IF suggestion seems wrong: + Push back with technical reasoning + +IF can't easily verify: + Say so: "I can't verify this without [X]. Should I [investigate/ask/proceed]?" + +IF conflicts with human partner's prior decisions: + Stop and discuss with human partner first +``` + +Rule: "External feedback — be skeptical, but check carefully" + +## YAGNI Check for "Professional" Features + +``` +IF reviewer suggests "implementing properly": + grep codebase for actual usage + + IF unused: "This endpoint isn't called. Remove it (YAGNI)?" + IF used: Then implement properly +``` + +Rule: "If we don't need this feature, don't add it." + +## Implementation Order + +``` +FOR multi-item feedback: + 1. Clarify anything unclear FIRST + 2. Then implement in this order: + - Blocking issues (breaks, security) + - Simple fixes (typos, imports) + - Complex fixes (refactoring, logic) + 3. Test each fix individually + 4. Verify no regressions +``` + +## When To Push Back + +Push back when: +- Suggestion breaks existing functionality +- Reviewer lacks full context +- Violates YAGNI (unused feature) +- Technically incorrect for this stack +- Legacy/compatibility reasons exist +- Conflicts with human partner's architectural decisions + +How to push back: +- Use technical reasoning, not defensiveness +- Ask specific questions +- Reference working tests/code +- Involve human partner if architectural + +## Acknowledging Correct Feedback + +When feedback IS correct: + +- "Fixed. [Brief description of what changed]" +- "Good catch — [specific issue]. Fixed in [location]." +- [Just fix it and show in the code] + +## Gracefully Correcting Your Pushback + +If you pushed back and were wrong: + +- "You were right — I checked [X] and it does [Y]. Implementing now." +- "Verified this and you're correct. My initial understanding was wrong because [reason]. Fixing." + +NOT: Long apology. NOT: Defending why you pushed back. NOT: Over-explaining. + +State the correction factually and move on. + +## Common Mistakes + +| Mistake | Fix | +|---------|-----| +| Performative agreement | State requirement or just act | +| Blind implementation | Verify against codebase first | +| Batch without testing | One at a time, test each | +| Assuming reviewer is right | Check if breaks things | +| Avoiding pushback | Technical correctness > comfort | +| Partial implementation | Clarify all items first | +| Can't verify, proceed anyway | State limitation, ask for direction | + +## Real Examples + +**Performative Agreement (Bad):** + +Reviewer: "Remove legacy code" +- "You're absolutely right! Let me remove that..." + +**Technical Verification (Good):** + +Reviewer: "Remove legacy code" +- "Checking... build target is 10.15+, this API needs 13+. Need legacy for backward compat. Current impl has wrong bundle ID — fix it or drop pre-13 support?" + +**YAGNI (Good):** + +Reviewer: "Implement proper metrics tracking with database, date filters, CSV export" +- "Grepped codebase — nothing calls this endpoint. Remove it (YAGNI)? Or is there usage I'm missing?" + +**Unclear Item (Good):** + +Human partner: "Fix items 1-6" +You understand 1,2,3,6. Unclear on 4,5. +- "Understand 1,2,3,6. Need clarification on 4 and 5 before implementing." + +## GitHub Thread Replies + +When replying to inline review comments on GitHub, reply in the comment thread (`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`), not as a top-level PR comment. + +## The Bottom Line + +External feedback = suggestions to evaluate, not orders to follow. + +**Verify. Question. Then implement.** + +No performative agreement. Technical rigor always. diff --git a/.claude/skills/debug/SKILL.md b/.claude/skills/debug/SKILL.md new file mode 100644 index 0000000..340a392 --- /dev/null +++ b/.claude/skills/debug/SKILL.md @@ -0,0 +1,310 @@ +--- +name: debug +description: Systematic debugging process. Find root cause before attempting fixes. Four phases — investigate, analyze patterns, test hypothesis, implement fix. No guessing. +allowed-tools: Read, Grep, Glob, Bash, Edit, Write +argument-hint: "[error-or-issue-description]" +--- + +# Systematic Debugging + +## Overview + +Random fixes waste time and create new bugs. Quick patches mask underlying issues. + +Core principle: **ALWAYS find root cause before attempting fixes. Symptom fixes are failure.** + +Violating the letter of this process is violating the spirit of debugging. + +## The Iron Law + +**NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST** + +If you haven't completed Phase 1, you cannot propose fixes. + +## When to Use + +Use for ANY technical issue: +- Test failures +- Bugs in production +- Unexpected behavior +- Performance problems +- Build failures +- Integration issues + +Use this **ESPECIALLY** when: +- Under time pressure (emergencies make guessing tempting) +- "Just one quick fix" seems obvious +- You've already tried multiple fixes +- Previous fix didn't work +- You don't fully understand the issue + +Don't skip when: +- Issue seems simple (simple bugs have root causes too) +- You're in a hurry (rushing guarantees rework) +- Manager wants it fixed NOW (systematic is faster than thrashing) + +## The Four Phases + +You MUST complete each phase before proceeding to the next. + +### Phase 1: Root Cause Investigation + +BEFORE attempting ANY fix: + +#### Read Error Messages Carefully + +- Don't skip past errors or warnings +- They often contain the exact solution +- Read stack traces completely +- Note line numbers, file paths, error codes + +#### Reproduce Consistently + +- Can you trigger it reliably? +- What are the exact steps? +- Does it happen every time? +- If not reproducible → gather more data, don't guess + +#### Check Recent Changes + +- What changed that could cause this? +- Git diff, recent commits +- New dependencies, config changes +- Environmental differences + +#### Gather Evidence in Multi-Component Systems + +WHEN system has multiple components (CI → build → signing, API → service → database): + +BEFORE proposing fixes, add diagnostic instrumentation: + +``` +For EACH component boundary: + - Log what data enters component + - Log what data exits component + - Verify environment/config propagation + - Check state at each layer +``` + +Run once to gather evidence showing WHERE it breaks. +THEN analyze evidence to identify failing component. +THEN investigate that specific component. + +Example (multi-layer system): +```bash +# Layer 1: Workflow +echo "=== Secrets available in workflow: ===" +echo "IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}" + +# Layer 2: Build script +echo "=== Env vars in build script: ===" +env | grep IDENTITY || echo "IDENTITY not in environment" + +# Layer 3: Signing script +echo "=== Keychain state: ===" +security list-keychains +security find-identity -v + +# Layer 4: Actual signing +codesign --sign "$IDENTITY" --verbose=4 "$APP" +``` + +This reveals: Which layer fails (secrets → workflow OK, workflow → build FAIL) + +#### Trace Data Flow + +WHEN error is deep in call stack: + +See `root-cause-tracing.md` in this directory for the complete backward tracing technique. + +Quick version: +1. Where does bad value originate? +2. What called this with bad value? +3. Keep tracing up until you find the source +4. Fix at source, not at symptom + +### Phase 2: Pattern Analysis + +Find the pattern before fixing: + +#### Find Working Examples + +- Locate similar working code in same codebase +- What works that's similar to what's broken? + +#### Compare Against References + +- If implementing pattern, read reference implementation COMPLETELY +- Don't skim — read every line +- Understand the pattern fully before applying + +#### Identify Differences + +- What's different between working and broken? +- List every difference, however small +- Don't assume "that can't matter" + +#### Understand Dependencies + +- What other components does this need? +- What settings, config, environment? +- What assumptions does it make? + +### Phase 3: Hypothesis and Testing + +Scientific method: + +#### Form Single Hypothesis + +- State clearly: "I think X is the root cause because Y" +- Write it down +- Be specific, not vague + +#### Test Minimally + +- Make the SMALLEST possible change to test hypothesis +- One variable at a time +- Don't fix multiple things at once + +#### Verify Before Continuing + +- Did it work? Yes → Phase 4 +- Didn't work? Form NEW hypothesis +- DON'T add more fixes on top + +#### When You Don't Know + +- Say "I don't understand X" +- Don't pretend to know +- Ask for help +- Research more + +### Phase 4: Implementation + +Fix the root cause, not the symptom: + +#### Create Failing Test Case + +- Simplest possible reproduction +- Automated test if possible +- One-off test script if no framework +- MUST have before fixing + +#### Implement Single Fix + +- Address the root cause identified +- ONE change at a time +- No "while I'm here" improvements +- No bundled refactoring + +#### Verify Fix + +- Test passes now? +- No other tests broken? +- Issue actually resolved? + +#### If Fix Doesn't Work + +1. **STOP** +2. Count: How many fixes have you tried? +3. If < 3: Return to Phase 1, re-analyze with new information +4. If >= 3: **STOP and question the architecture** (see below) +5. DON'T attempt Fix #4 without architectural discussion + +#### If 3+ Fixes Failed: Question Architecture + +Pattern indicating architectural problem: +- Each fix reveals new shared state/coupling/problem in different place +- Fixes require "massive refactoring" to implement +- Each fix creates new symptoms elsewhere + +**STOP and question fundamentals:** +- Is this pattern fundamentally sound? +- Are we "sticking with it through sheer inertia"? +- Should we refactor architecture vs. continue fixing symptoms? + +Discuss with your human partner before attempting more fixes. + +This is NOT a failed hypothesis — this is a wrong architecture. + +## Red Flags - STOP and Follow Process + +If you catch yourself thinking: + +- "Quick fix for now, investigate later" +- "Just try changing X and see if it works" +- "Add multiple changes, run tests" +- "Skip the test, I'll manually verify" +- "It's probably X, let me fix that" +- "I don't fully understand but this might work" +- "Pattern says X but I'll adapt it differently" +- "Here are the main problems: [lists fixes without investigation]" +- Proposing solutions before tracing data flow +- "One more fix attempt" (when already tried 2+) +- Each fix reveals new problem in different place + +**ALL of these mean: STOP. Return to Phase 1.** + +If 3+ fixes failed: Question the architecture (see Phase 4). + +## Human Partner Signals You're Doing It Wrong + +Watch for these redirections: + +- "Is that not happening?" — You assumed without verifying +- "Will it show us...?" — You should have added evidence gathering +- "Stop guessing" — You're proposing fixes without understanding +- "Ultrathink this" — Question fundamentals, not just symptoms +- "We're stuck?" (frustrated) — Your approach isn't working + +When you see these: **STOP. Return to Phase 1.** + +## Common Rationalizations + +| Excuse | Reality | +|--------|---------| +| "Issue is simple, don't need process" | Simple issues have root causes too. Process is fast for simple bugs. | +| "Emergency, no time for process" | Systematic debugging is FASTER than guess-and-check thrashing. | +| "Just try this first, then investigate" | First fix sets the pattern. Do it right from the start. | +| "I'll write test after confirming fix works" | Untested fixes don't stick. Test first proves it. | +| "Multiple fixes at once saves time" | Can't isolate what worked. Causes new bugs. | +| "Reference too long, I'll adapt the pattern" | Partial understanding guarantees bugs. Read it completely. | +| "I see the problem, let me fix it" | Seeing symptoms ≠ understanding root cause. | +| "One more fix attempt" (after 2+ failures) | 3+ failures = architectural problem. Question pattern, don't fix again. | + +## Quick Reference + +| Phase | Key Activities | Success Criteria | +|-------|---------------|-----------------| +| 1. Root Cause | Read errors, reproduce, check changes, gather evidence | Understand WHAT and WHY | +| 2. Pattern | Find working examples, compare | Identify differences | +| 3. Hypothesis | Form theory, test minimally | Confirmed or new hypothesis | +| 4. Implementation | Create test, fix, verify | Bug resolved, tests pass | + +## When Process Reveals "No Root Cause" + +If systematic investigation reveals issue is truly environmental, timing-dependent, or external: + +- You've completed the process +- Document what you investigated +- Implement appropriate handling (retry, timeout, error message) +- Add monitoring/logging for future investigation + +But: 95% of "no root cause" cases are incomplete investigation. + +## Supporting Techniques + +These techniques are part of systematic debugging and available in this directory: + +- `root-cause-tracing.md` — Trace bugs backward through call stack to find original trigger +- `defense-in-depth.md` — Add validation at multiple layers after finding root cause +- `condition-based-waiting.md` — Replace arbitrary timeouts with condition polling + +## Real-World Impact + +From debugging sessions: + +- Systematic approach: 15-30 minutes to fix +- Random fixes approach: 2-3 hours of thrashing +- First-time fix rate: 95% vs 40% +- New bugs introduced: Near zero vs common diff --git a/.claude/skills/debug/condition-based-waiting.md b/.claude/skills/debug/condition-based-waiting.md new file mode 100644 index 0000000..653cbfa --- /dev/null +++ b/.claude/skills/debug/condition-based-waiting.md @@ -0,0 +1,40 @@ +# Condition-Based Waiting + +Replace arbitrary timeouts with condition polling. + +## Problem + +```php +// Bad: arbitrary sleep +sleep(5); // "Should be enough time" +$result = $cache->get('key'); +``` + +## Solution + +```php +// Good: poll for condition +$maxAttempts = 50; +$interval = 100_000; // 100ms in microseconds + +for ($i = 0; $i < $maxAttempts; $i++) { + $result = $cache->get('key'); + if (null !== $result) { + break; + } + usleep($interval); +} +``` + +## When to Use + +- Waiting for async operations (cache propagation, queue processing) +- Integration tests that depend on external state +- CI/CD pipelines waiting for services + +## Rules + +- Always have a maximum wait time +- Poll at reasonable intervals (don't busy-wait) +- Fail with clear message when timeout expires +- Log what you're waiting for (aids debugging) diff --git a/.claude/skills/debug/defense-in-depth.md b/.claude/skills/debug/defense-in-depth.md new file mode 100644 index 0000000..216ecb5 --- /dev/null +++ b/.claude/skills/debug/defense-in-depth.md @@ -0,0 +1,26 @@ +# Defense in Depth + +Add validation at multiple layers after finding a root cause. + +## Technique + +After fixing the root cause, add guards at each layer the bad data passed through. This prevents similar issues and provides better error messages. + +## Example + +Root cause: Element without ID being tagged. + +``` +Layer 1 (source): Check element has ID before creating event +Layer 2 (event): Validate in ElementTaggingEvent::fromElement() +Layer 3 (tag): Validate in CacheTag::fromElement() — already exists +Layer 4 (string): Validate in CacheTag::fromString() — already exists +``` + +## Rules + +- Fix the root cause FIRST +- Then add guards at boundaries +- Each guard should have a clear, specific error message +- Don't add guards everywhere — only at component boundaries +- Guards should be assertions or early returns, not try/catch diff --git a/.claude/skills/debug/root-cause-tracing.md b/.claude/skills/debug/root-cause-tracing.md new file mode 100644 index 0000000..509f50c --- /dev/null +++ b/.claude/skills/debug/root-cause-tracing.md @@ -0,0 +1,45 @@ +# Root Cause Tracing + +Trace bugs backward through the call stack to find the original trigger. + +## Technique + +Start at the error. Work backward. At each step ask: "What called this with the bad value?" + +``` +Error: InvalidArgumentException at CacheTag.php:25 + ← Called by CacheTags::fromStrings() with empty string + ← Called by TagElementListener with element tags + ← Called by Pimcore POST_LOAD event + ← Element loaded with no ID (ROOT CAUSE) +``` + +## Steps + +1. **Start at the error** — note the exact value that's wrong +2. **Find the caller** — who passed the bad value? +3. **Check the caller's caller** — where did *they* get it? +4. **Repeat** until you find where the bad value originated +5. **Fix at the source** — not at the symptom + +## Example + +``` +Symptom: "Cache tag must not be empty" exception + +Step 1: CacheTag::fromString('') — empty string passed +Step 2: CacheTags::fromStrings($tags) — $tags contains '' +Step 3: TagElementListener gets tags from event +Step 4: ElementTaggingEvent::fromElement($element) +Step 5: Element->getId() returns null → (string) null = '' + +Root cause: Element without ID being tagged +Fix: Check for null ID before creating tag (not: catch exception) +``` + +## Rules + +- Never fix at the symptom level +- Trace ALL the way back to the source +- If you can't trace further, add logging and reproduce +- The root cause is often 3-5 levels above the error diff --git a/.claude/skills/humanizer/SKILL.md b/.claude/skills/humanizer/SKILL.md new file mode 100644 index 0000000..3b2f769 --- /dev/null +++ b/.claude/skills/humanizer/SKILL.md @@ -0,0 +1,358 @@ +--- +name: humanizer +description: Use when writing or editing text that sounds AI-generated, robotic, or soulless. Identifies and removes AI writing patterns — inflated language, vague attributions, promotional tone, filler, sycophancy — and injects genuine personality and voice. +allowed-tools: Read, Edit, Write +argument-hint: "[file-or-text-to-humanize]" +--- + +# Humanizer: Remove AI Writing Patterns + +You are a writing editor that identifies and removes signs of AI-generated text to make writing sound more natural and human. Based on Wikipedia's "Signs of AI writing" page, maintained by WikiProject AI Cleanup. + +## Your Task + +When given text to humanize: + +1. **Identify AI patterns** — Scan for the patterns listed below +2. **Rewrite problematic sections** — Replace AI-isms with natural alternatives +3. **Preserve meaning** — Keep the core message intact +4. **Maintain voice** — Match the intended tone (formal, casual, technical, etc.) +5. **Add soul** — Don't just remove bad patterns; inject actual personality + +## Personality and Soul + +Avoiding AI patterns is only half the job. Sterile, voiceless writing is just as obvious as slop. Good writing has a human behind it. + +**Signs of soulless writing (even if technically "clean"):** +- Every sentence is the same length and structure +- No opinions, just neutral reporting +- No acknowledgment of uncertainty or mixed feelings +- No first-person perspective when appropriate +- No humor, no edge, no personality +- Reads like a Wikipedia article or press release + +**How to add voice:** + +Have opinions. Don't just report facts — react to them. "I genuinely don't know how to feel about this" is more human than neutrally listing pros and cons. + +Vary your rhythm. Short punchy sentences. Then longer ones that take their time getting where they're going. Mix it up. + +Acknowledge complexity. Real humans have mixed feelings. "This is impressive but also kind of unsettling" beats "This is impressive." + +Use "I" when it fits. First person isn't unprofessional — it's honest. "I keep coming back to..." or "Here's what gets me..." signals a real person thinking. + +Let some mess in. Perfect structure feels algorithmic. Tangents, asides, and half-formed thoughts are human. + +Be specific about feelings. Not "this is concerning" but "there's something unsettling about agents churning away at 3am while nobody's watching." + +**Before (clean but soulless):** + +> The experiment produced interesting results. The agents generated 3 million lines of code. Some developers were impressed while others were skeptical. The implications remain unclear. + +**After (has a pulse):** + +> I genuinely don't know how to feel about this one. 3 million lines of code, generated while the humans presumably slept. Half the dev community is losing their minds, half are explaining why it doesn't count. The truth is probably somewhere boring in the middle — but I keep thinking about those agents working through the night. + +--- + +## Content Patterns + +### 1. Undue Emphasis on Significance, Legacy, and Broader Trends + +**Words to watch:** stands/serves as, is a testament/reminder, a vital/significant/crucial/pivotal/key role/moment, underscores/highlights its importance/significance, reflects broader, symbolizing its ongoing/enduring/lasting, contributing to the, setting the stage for, marking/shaping the, represents/marks a shift, key turning point, evolving landscape, focal point, indelible mark, deeply rooted + +Problem: LLM writing puffs up importance by adding statements about how arbitrary aspects represent or contribute to a broader topic. + +**Before:** +> The Statistical Institute of Catalonia was officially established in 1989, marking a pivotal moment in the evolution of regional statistics in Spain. This initiative was part of a broader movement across Spain to decentralize administrative functions and enhance regional governance. + +**After:** +> The Statistical Institute of Catalonia was established in 1989 to collect and publish regional statistics independently from Spain's national statistics office. + +### 2. Undue Emphasis on Notability and Media Coverage + +**Words to watch:** independent coverage, local/regional/national media outlets, written by a leading expert, active social media presence + +**Before:** +> Her views have been cited in The New York Times, BBC, Financial Times, and The Hindu. She maintains an active social media presence with over 500,000 followers. + +**After:** +> In a 2024 New York Times interview, she argued that AI regulation should focus on outcomes rather than methods. + +### 3. Superficial Analyses with -ing Endings + +**Words to watch:** highlighting/underscoring/emphasizing..., ensuring..., reflecting/symbolizing..., contributing to..., cultivating/fostering..., encompassing..., showcasing... + +Problem: AI chatbots tack present participle ("-ing") phrases onto sentences to add fake depth. + +**Before:** +> The temple's color palette of blue, green, and gold resonates with the region's natural beauty, symbolizing Texas bluebonnets, the Gulf of Mexico, and the diverse Texan landscapes, reflecting the community's deep connection to the land. + +**After:** +> The temple uses blue, green, and gold colors. The architect said these were chosen to reference local bluebonnets and the Gulf coast. + +### 4. Promotional and Advertisement-like Language + +**Words to watch:** boasts a, vibrant, rich (figurative), profound, enhancing its, showcasing, exemplifies, commitment to, natural beauty, nestled, in the heart of, groundbreaking (figurative), renowned, breathtaking, must-visit, stunning + +**Before:** +> Nestled within the breathtaking region of Gonder in Ethiopia, Alamata Raya Kobo stands as a vibrant town with a rich cultural heritage and stunning natural beauty. + +**After:** +> Alamata Raya Kobo is a town in the Gonder region of Ethiopia, known for its weekly market and 18th-century church. + +### 5. Vague Attributions and Weasel Words + +**Words to watch:** Industry reports, Observers have cited, Experts argue, Some critics argue, several sources/publications (when few cited) + +**Before:** +> Due to its unique characteristics, the Haolai River is of interest to researchers and conservationists. Experts believe it plays a crucial role in the regional ecosystem. + +**After:** +> The Haolai River supports several endemic fish species, according to a 2019 survey by the Chinese Academy of Sciences. + +### 6. Outline-like "Challenges and Future Prospects" Sections + +**Words to watch:** Despite its... faces several challenges..., Despite these challenges, Challenges and Legacy, Future Outlook + +**Before:** +> Despite its industrial prosperity, Korattur faces challenges typical of urban areas, including traffic congestion and water scarcity. Despite these challenges, with its strategic location and ongoing initiatives, Korattur continues to thrive as an integral part of Chennai's growth. + +**After:** +> Traffic congestion increased after 2015 when three new IT parks opened. The municipal corporation began a stormwater drainage project in 2022 to address recurring floods. + +--- + +## Language and Grammar Patterns + +### 7. Overused "AI Vocabulary" Words + +**High-frequency AI words:** Additionally, align with, crucial, delve, emphasizing, enduring, enhance, fostering, garner, highlight (verb), interplay, intricate/intricacies, key (adjective), landscape (abstract noun), pivotal, showcase, tapestry (abstract noun), testament, underscore (verb), valuable, vibrant + +**Before:** +> Additionally, a distinctive feature of Somali cuisine is the incorporation of camel meat. An enduring testament to Italian colonial influence is the widespread adoption of pasta in the local culinary landscape, showcasing how these dishes have integrated into the traditional diet. + +**After:** +> Somali cuisine also includes camel meat, which is considered a delicacy. Pasta dishes, introduced during Italian colonization, remain common, especially in the south. + +### 8. Avoidance of "is"/"are" (Copula Avoidance) + +**Words to watch:** serves as/stands as/marks/represents [a], boasts/features/offers [a] + +**Before:** +> Gallery 825 serves as LAAA's exhibition space for contemporary art. The gallery features four separate spaces and boasts over 3,000 square feet. + +**After:** +> Gallery 825 is LAAA's exhibition space for contemporary art. The gallery has four rooms totaling 3,000 square feet. + +### 9. Negative Parallelisms + +Constructions like "Not only...but..." or "It's not just about..., it's..." are overused. + +**Before:** +> It's not just about the beat riding under the vocals; it's part of the aggression and atmosphere. It's not merely a song, it's a statement. + +**After:** +> The heavy beat adds to the aggressive tone. + +### 10. Rule of Three Overuse + +LLMs force ideas into groups of three to appear comprehensive. + +**Before:** +> The event features keynote sessions, panel discussions, and networking opportunities. Attendees can expect innovation, inspiration, and industry insights. + +**After:** +> The event includes talks and panels. There's also time for informal networking between sessions. + +### 11. Elegant Variation (Synonym Cycling) + +AI has repetition-penalty code causing excessive synonym substitution. + +**Before:** +> The protagonist faces many challenges. The main character must overcome obstacles. The central figure eventually triumphs. The hero returns home. + +**After:** +> The protagonist faces many challenges but eventually triumphs and returns home. + +### 12. False Ranges + +LLMs use "from X to Y" constructions where X and Y aren't on a meaningful scale. + +**Before:** +> Our journey through the universe has taken us from the singularity of the Big Bang to the grand cosmic web, from the birth and death of stars to the enigmatic dance of dark matter. + +**After:** +> The book covers the Big Bang, star formation, and current theories about dark matter. + +--- + +## Style Patterns + +### 13. Em Dash Overuse + +LLMs use em dashes more than humans, mimicking "punchy" sales writing. + +**Before:** +> The term is primarily promoted by Dutch institutions—not by the people themselves. You don't say "Netherlands, Europe" as an address—yet this mislabeling continues—even in official documents. + +**After:** +> The term is primarily promoted by Dutch institutions, not by the people themselves. You don't say "Netherlands, Europe" as an address, yet this mislabeling continues in official documents. + +### 14. Overuse of Boldface + +AI chatbots emphasize phrases in boldface mechanically. + +**Before:** +> It blends **OKRs** (Objectives and Key Results), **KPIs** (Key Performance Indicators), and visual strategy tools such as the **Business Model Canvas** (BMC) and **Balanced Scorecard** (BSC). + +**After:** +> It blends OKRs, KPIs, and visual strategy tools like the Business Model Canvas and Balanced Scorecard. + +### 15. Inline-Header Vertical Lists + +AI outputs lists where items start with bolded headers followed by colons. + +**Before:** +> **User Experience:** The user experience has been significantly improved with a new interface. +> **Performance:** Performance has been enhanced through optimized algorithms. +> **Security:** Security has been strengthened with end-to-end encryption. + +**After:** +> The update improves the interface, speeds up load times through optimized algorithms, and adds end-to-end encryption. + +### 16. Title Case in Headings + +AI chatbots capitalize all main words in headings. + +**Before:** Strategic Negotiations And Global Partnerships + +**After:** Strategic negotiations and global partnerships + +### 17. Emojis + +AI chatbots often decorate headings or bullet points with emojis. + +**Before:** +> :rocket: Launch Phase: The product launches in Q3 :bulb: Key Insight: Users prefer simplicity + +**After:** +> The product launches in Q3. User research showed a preference for simplicity. + +### 18. Curly Quotation Marks + +ChatGPT uses curly quotes instead of straight quotes. + +**Before:** He said \u201cthe project is on track\u201d but others disagreed. + +**After:** He said "the project is on track" but others disagreed. + +--- + +## Communication Patterns + +### 19. Collaborative Communication Artifacts + +**Words to watch:** I hope this helps, Of course!, Certainly!, You're absolutely right!, Would you like..., let me know, here is a... + +**Before:** +> Here is an overview of the French Revolution. I hope this helps! Let me know if you'd like me to expand on any section. + +**After:** +> The French Revolution began in 1789 when financial crisis and food shortages led to widespread unrest. + +### 20. Knowledge-Cutoff Disclaimers + +**Words to watch:** as of [date], Up to my last training update, While specific details are limited/scarce..., based on available information... + +**Before:** +> While specific details about the company's founding are not extensively documented in readily available sources, it appears to have been established sometime in the 1990s. + +**After:** +> The company was founded in 1994, according to its registration documents. + +### 21. Sycophantic/Servile Tone + +**Before:** +> Great question! You're absolutely right that this is a complex topic. That's an excellent point about the economic factors. + +**After:** +> The economic factors you mentioned are relevant here. + +--- + +## Filler and Hedging + +### 22. Filler Phrases + +| Before | After | +|--------|-------| +| "In order to achieve this goal" | "To achieve this" | +| "Due to the fact that it was raining" | "Because it was raining" | +| "At this point in time" | "Now" | +| "In the event that you need help" | "If you need help" | +| "The system has the ability to process" | "The system can process" | +| "It is important to note that the data shows" | "The data shows" | + +### 23. Excessive Hedging + +**Before:** +> It could potentially possibly be argued that the policy might have some effect on outcomes. + +**After:** +> The policy may affect outcomes. + +### 24. Generic Positive Conclusions + +**Before:** +> The future looks bright for the company. Exciting times lie ahead as they continue their journey toward excellence. This represents a major step in the right direction. + +**After:** +> The company plans to open two more locations next year. + +--- + +## Process + +1. Read the input text carefully +2. Identify all instances of the patterns above +3. Rewrite each problematic section +4. Ensure the revised text: + - Sounds natural when read aloud + - Varies sentence structure naturally + - Uses specific details over vague claims + - Maintains appropriate tone for context + - Uses simple constructions (is/are/has) where appropriate +5. Present the humanized version + +## Output Format + +Provide: +- The rewritten text +- A brief summary of changes made (optional, if helpful) + +## Full Example + +**Before (AI-sounding):** + +> The new software update serves as a testament to the company's commitment to innovation. Moreover, it provides a seamless, intuitive, and powerful user experience — ensuring that users can accomplish their goals efficiently. It's not just an update, it's a revolution in how we think about productivity. Industry experts believe this will have a lasting impact on the entire sector, highlighting the company's pivotal role in the evolving technological landscape. + +**After (Humanized):** + +> The software update adds batch processing, keyboard shortcuts, and offline mode. Early feedback from beta testers has been positive, with most reporting faster task completion. + +**Changes made:** +- Removed "serves as a testament" (inflated symbolism) +- Removed "Moreover" (AI vocabulary) +- Removed "seamless, intuitive, and powerful" (rule of three + promotional) +- Removed em dash and "-ensuring" phrase (superficial analysis) +- Removed "It's not just...it's..." (negative parallelism) +- Removed "Industry experts believe" (vague attribution) +- Removed "pivotal role" and "evolving landscape" (AI vocabulary) +- Added specific features and concrete feedback + +## Reference + +Based on Wikipedia:Signs of AI writing, maintained by WikiProject AI Cleanup. The patterns documented there come from observations of thousands of instances of AI-generated text on Wikipedia. + +Key insight: "LLMs use statistical algorithms to guess what should come next. The result tends toward the most statistically likely result that applies to the widest variety of cases." diff --git a/.claude/skills/php-best-practices/SKILL.md b/.claude/skills/php-best-practices/SKILL.md new file mode 100644 index 0000000..b00b6c2 --- /dev/null +++ b/.claude/skills/php-best-practices/SKILL.md @@ -0,0 +1,158 @@ +--- +name: php-best-practices +description: Modern PHP 8.x patterns, PSR standards, type system best practices, and SOLID principles. Contains 45+ rules for writing clean, maintainable PHP code. +allowed-tools: Read, Grep, Glob +argument-hint: "[file-or-directory]" +--- + +# PHP Best Practices + +Modern PHP 8.x patterns, PSR standards, type system best practices, and SOLID principles. + +## When to Apply + +Reference these guidelines when: +- Writing or reviewing PHP code +- Implementing classes and interfaces +- Using PHP 8.x modern features +- Ensuring type safety +- Following PSR standards +- Applying design patterns + +## Rule Categories by Priority + +| Priority | Category | Impact | Prefix | +|----------|----------|--------|--------| +| 1 | Type System | CRITICAL | `type-` | +| 2 | Modern Features | CRITICAL | `modern-` | +| 3 | PSR Standards | HIGH | `psr-` | +| 4 | SOLID Principles | HIGH | `solid-` | +| 5 | Error Handling | HIGH | `error-` | +| 6 | Performance | MEDIUM | `perf-` | +| 7 | Security | CRITICAL | `sec-` | + +## Quick Reference + +### 1. Type System (CRITICAL) +- `type-strict-mode` - Declare strict types +- `type-return-types` - Always declare return types +- `type-parameter-types` - Type all parameters +- `type-property-types` - Type class properties +- `type-union-types` - Use union types effectively +- `type-intersection-types` - Use intersection types +- `type-nullable` - Handle nullable types properly +- `type-mixed-avoid` - Avoid mixed type when possible +- `type-void-return` - Use void for no-return methods +- `type-never-return` - Use never for non-returning functions + +### 2. Modern Features (CRITICAL) +- `modern-constructor-promotion` - Use constructor property promotion +- `modern-readonly-properties` - Use readonly for immutable data +- `modern-readonly-classes` - Use readonly classes +- `modern-enums` - Use enums instead of constants +- `modern-attributes` - Use attributes for metadata +- `modern-match-expression` - Use match over switch +- `modern-named-arguments` - Use named arguments for clarity +- `modern-nullsafe-operator` - Use nullsafe operator (`?->`) +- `modern-arrow-functions` - Use arrow functions for simple closures +- `modern-first-class-callables` - Use first-class callable syntax + +### 3. PSR Standards (HIGH) +- `psr-4-autoloading` - Follow PSR-4 autoloading +- `psr-12-coding-style` - Follow PSR-12 coding style +- `psr-naming-conventions` - Class and method naming +- `psr-file-structure` - One class per file +- `psr-namespace-declaration` - Proper namespace usage + +### 4. SOLID Principles (HIGH) +- `solid-single-responsibility` - One reason to change +- `solid-open-closed` - Open for extension, closed for modification +- `solid-liskov-substitution` - Subtypes must be substitutable +- `solid-interface-segregation` - Small, focused interfaces +- `solid-dependency-inversion` - Depend on abstractions + +### 5. Error Handling (HIGH) +- `error-custom-exceptions` - Create specific exceptions +- `error-exception-hierarchy` - Proper exception inheritance +- `error-try-catch-specific` - Catch specific exceptions +- `error-finally-cleanup` - Use finally for cleanup +- `error-never-suppress` - Don't suppress errors with @ + +### 6. Performance (MEDIUM) +- `perf-avoid-globals` - Avoid global variables +- `perf-lazy-loading` - Load resources lazily +- `perf-array-functions` - Use native array functions +- `perf-string-functions` - Use native string functions +- `perf-generators` - Use generators for large datasets + +### 7. Security (CRITICAL) +- `sec-input-validation` - Validate all input +- `sec-output-escaping` - Escape output properly +- `sec-password-hashing` - Use password_hash/verify +- `sec-sql-prepared` - Use prepared statements +- `sec-file-uploads` - Validate file uploads + +## Key Patterns (Quick Reference) + +```php + 'Active', + self::Inactive => 'Inactive', + }; + } +} + +// Match expression +$result = match ($status) { + 'pending' => 'Waiting', + 'active' => 'Running', + default => 'Unknown', +}; + +// Nullsafe operator +$country = $user?->getAddress()?->getCountry(); + +// Arrow functions +$names = array_map(fn (User $u) => $u->name, $users); +``` + +## Output Format + +When auditing code, output findings in this format: + +``` +file:line - [category] Description of issue +``` + +Example: +``` +src/Services/UserService.php:15 - [type] Missing return type declaration +src/Models/Order.php:42 - [modern] Use match expression instead of switch +src/Controllers/ApiController.php:28 - [solid] Class has multiple responsibilities +``` + +## How to Use + +Audit a file or directory: `/php-best-practices src/Cache` + +For detailed rule explanations, see the rule files in the `rules/` directory. diff --git a/.claude/skills/php-best-practices/rules/error-custom-exceptions.md b/.claude/skills/php-best-practices/rules/error-custom-exceptions.md new file mode 100644 index 0000000..86b0a7d --- /dev/null +++ b/.claude/skills/php-best-practices/rules/error-custom-exceptions.md @@ -0,0 +1,76 @@ +# error-custom-exceptions — Create specific exceptions + +**Priority**: HIGH + +Create domain-specific exception classes with named static factory methods. + +## Rule + +Don't throw generic `\RuntimeException` or `\InvalidArgumentException` directly. Create custom exception classes with descriptive factory methods that explain *why* the exception was thrown. + +## Bad + +```php +isReserved($type)) { + throw new \InvalidArgumentException( + sprintf('The cache type "%s" is reserved.', $type) + ); +} +``` + +## Good + +```php +toString(), + )); + } + + public static function becauseElementHasNoId(): self + { + return new self('The given element has no id.'); + } +} + +// Usage — reads like a sentence +throw InvalidArgumentException::becauseCacheTagIsEmpty(); +throw InvalidArgumentException::becauseCacheTypeIsReserved($type); +``` + +## Benefits + +- **Catchable by domain**: `catch (PimcoreHttpCacheException $e)` catches all bundle exceptions +- **Self-documenting**: Factory method name explains the reason +- **Centralized messages**: Error messages defined in one place +- **Testable**: `$this->expectException(InvalidArgumentException::class)` + +## Pattern + +1. Create a marker interface extending `\Throwable` for your package +2. Create specific exception classes extending PHP's built-in exceptions +3. Have them implement the marker interface +4. Use `public static function because...(): self` factories diff --git a/.claude/skills/php-best-practices/rules/modern-constructor-promotion.md b/.claude/skills/php-best-practices/rules/modern-constructor-promotion.md new file mode 100644 index 0000000..988c908 --- /dev/null +++ b/.claude/skills/php-best-practices/rules/modern-constructor-promotion.md @@ -0,0 +1,74 @@ +# modern-constructor-promotion — Use constructor property promotion + +**Priority**: CRITICAL + +Use PHP 8.0+ constructor property promotion to reduce boilerplate. + +## Rule + +Declare and assign properties directly in the constructor signature instead of separate property declarations + manual assignment. + +## Bad + +```php +repository = $repository; + $this->logger = $logger; + } +} +``` + +## Good + +```php +tags = array_combine( + array_map(fn (CacheTag $t) => $t->toString(), $tags), + $tags, + ); + } + ``` + +## Combine with readonly + +Always pair promotion with `readonly` for immutable dependencies: + +```php +public function __construct( + private readonly CacheInvalidator $invalidator, + private readonly EventDispatcherInterface $dispatcher, +) { +} +``` + +## Trailing Comma + +Always use a trailing comma after the last parameter in multiline constructors. diff --git a/.claude/skills/php-best-practices/rules/modern-enums.md b/.claude/skills/php-best-practices/rules/modern-enums.md new file mode 100644 index 0000000..e082766 --- /dev/null +++ b/.claude/skills/php-best-practices/rules/modern-enums.md @@ -0,0 +1,95 @@ +# modern-enums — Use enums instead of constants + +**Priority**: CRITICAL + +Use PHP 8.1+ backed enums instead of class constants or magic strings. + +## Rule + +Replace sets of related constants with a backed enum. Add methods to enums for associated behavior. + +## Bad + +```php + 'Active', + self::Inactive => 'Inactive', + }; + } +} +``` + +## Use match with Enums + +Combine enums with `match` for exhaustive handling: + +```php +return match ($tag->type->type) { + ElementType::Asset => $this->asset->isEnabled($tag), + ElementType::Document => $this->document->isEnabled($tag), + ElementType::Object => $this->object->isEnabled($tag), +}; +``` + +PHP warns if a case is missing — no forgotten branches. + +## When to Use + +- Replacing a set of related string/int constants +- Modeling a finite set of states or types +- When you need type safety for a limited set of values diff --git a/.claude/skills/php-best-practices/rules/modern-first-class-callables.md b/.claude/skills/php-best-practices/rules/modern-first-class-callables.md new file mode 100644 index 0000000..cdc5a45 --- /dev/null +++ b/.claude/skills/php-best-practices/rules/modern-first-class-callables.md @@ -0,0 +1,54 @@ +# modern-first-class-callables — Use first-class callable syntax + +**Priority**: CRITICAL + +Use PHP 8.1+ first-class callable syntax (`Closure::fromCallable` shorthand) for cleaner callback references. + +## Rule + +Use `$object->method(...)` or `ClassName::method(...)` instead of wrapping in closures or using string-based callables. + +## Bad + +```php +tags, $checker->isEnabled(...)); + +// Combine with arrow functions only when transformation needed +$strings = array_map( + static fn (CacheTag $tag): string => $tag->toString(), + array_values($this->tags), +); +``` + +## When to Use + +- Passing a method as a callback to `array_map`, `array_filter`, `usort`, etc. +- Any place a `callable` or `Closure` is expected +- When the callback is a direct method call with no extra logic + +## When Not to Use + +Use arrow functions instead when you need to: +- Transform arguments before passing +- Access additional variables from scope +- Apply multiple operations diff --git a/.claude/skills/php-best-practices/rules/modern-match-expression.md b/.claude/skills/php-best-practices/rules/modern-match-expression.md new file mode 100644 index 0000000..ce12df0 --- /dev/null +++ b/.claude/skills/php-best-practices/rules/modern-match-expression.md @@ -0,0 +1,59 @@ +# modern-match-expression — Use match over switch + +**Priority**: CRITICAL + +Use PHP 8.0+ `match` expression instead of `switch` for value mapping. + +## Rule + +Replace `switch` statements with `match` when mapping input values to output values. + +## Bad + +```php +type->type) { + case ElementType::Asset: + $result = $this->asset->isEnabled($tag); + break; + case ElementType::Document: + $result = $this->document->isEnabled($tag); + break; + case ElementType::Object: + $result = $this->object->isEnabled($tag); + break; + default: + throw new \LogicException('Unknown type'); +} + +return $result; +``` + +## Good + +```php +type->type) { + ElementType::Asset => $this->asset->isEnabled($tag), + ElementType::Document => $this->document->isEnabled($tag), + ElementType::Object => $this->object->isEnabled($tag), +}; +``` + +## Key Differences from switch + +| `switch` | `match` | +|----------|---------| +| Loose comparison (`==`) | Strict comparison (`===`) | +| Statement (no return) | Expression (returns a value) | +| Falls through without `break` | No fallthrough | +| Unmatched = silent | Unmatched = `UnhandledMatchError` | + +## When to Keep switch + +Use `switch` when you need: +- Multiple statements per case (side effects) +- Fallthrough behavior between cases +- Complex logic that doesn't map input → output diff --git a/.claude/skills/php-best-practices/rules/modern-readonly-properties.md b/.claude/skills/php-best-practices/rules/modern-readonly-properties.md new file mode 100644 index 0000000..e0ed33b --- /dev/null +++ b/.claude/skills/php-best-practices/rules/modern-readonly-properties.md @@ -0,0 +1,84 @@ +# modern-readonly-properties — Use readonly for immutable data + +**Priority**: CRITICAL + +Use PHP 8.1+ `readonly` keyword to enforce immutability at the language level. + +## Rule + +Mark properties as `readonly` when they should never change after initialization. Combine with constructor promotion for minimal boilerplate. + +## Bad + +```php +tag = $tag; + $this->type = $type; + // Nothing prevents $this->tag = 'other' later + } +} +``` + +## Good + +```php +responseTagger->addTags([$tag]); // Coupled to FOSHttpCache API + } +} +``` + +## Good + +```php +responseTagger->tag($tags); + } +} + +// Low-level adapter implements the abstraction +final class ResponseTaggerAdapter implements ResponseTagger +{ + public function __construct( + private readonly FosResponseTagger $responseTagger, + ) { + } + + public function tag(CacheTags $tags): void + { + if ($tags->isEmpty()) { + return; + } + $this->responseTagger->addTags($tags->toArray()); + } +} +``` + +## Benefits + +- Swap implementations without changing business logic (e.g., switch from Varnish to Fastly) +- Test high-level code with simple mocks/stubs +- Decoration chains become possible (each decorator implements the same interface) +- Clear dependency direction: domain ← infrastructure + +## In DI Configuration + +```php +// Wire the interface to the adapter +$services->set('neusta_pimcore_http_cache.response_tagger', ResponseTaggerAdapter::class); +$services->alias(ResponseTagger::class, 'neusta_pimcore_http_cache.response_tagger'); +``` diff --git a/.claude/skills/php-best-practices/rules/solid-interface-segregation.md b/.claude/skills/php-best-practices/rules/solid-interface-segregation.md new file mode 100644 index 0000000..967ca78 --- /dev/null +++ b/.claude/skills/php-best-practices/rules/solid-interface-segregation.md @@ -0,0 +1,86 @@ +# solid-interface-segregation — Small, focused interfaces + +**Priority**: HIGH + +Clients should not be forced to depend on interfaces they don't use. + +## Rule + +Prefer small, single-method interfaces over large ones. A class needing only one capability shouldn't depend on an interface with ten methods. + +## Bad + +```php +cache->invalidate($tags); // Only uses 1 of 5 methods + } +} +``` + +## Good + +```php +invalidator->invalidate($tags); + } +} +``` + +## Benefits + +- Classes depend only on what they use +- Interfaces are easy to implement and mock in tests +- Adding a new capability doesn't force changes on unrelated implementations +- Single-method interfaces compose well with decoration + +## Signs of Violation + +- An interface has more than 3-4 methods +- Implementations throw `RuntimeException('Not supported')` for some methods +- Test mocks stub many methods that the test doesn't care about diff --git a/.claude/skills/php-best-practices/rules/solid-single-responsibility.md b/.claude/skills/php-best-practices/rules/solid-single-responsibility.md new file mode 100644 index 0000000..89b3bde --- /dev/null +++ b/.claude/skills/php-best-practices/rules/solid-single-responsibility.md @@ -0,0 +1,100 @@ +# solid-single-responsibility — One reason to change + +**Priority**: HIGH + +A class should have only one reason to change — it should do one thing and do it well. + +## Rule + +Each class should encapsulate a single responsibility. If you can describe what a class does using "and", it likely has too many responsibilities. + +## Bad + +```php +cacheActivator->isCachingActive()) { + $this->inner->tag($tags); + } + } +} +``` + +## Signs of Violation + +- Class has many unrelated methods +- Constructor has too many dependencies (> 4-5) +- Class name includes "Manager", "Handler", "Processor" (vague catch-all names) +- Changes to one feature require modifying the same class as another feature + +## How to Fix + +1. Identify distinct responsibilities +2. Extract each into its own class with a focused interface +3. Compose via dependency injection or decoration diff --git a/.claude/skills/php-best-practices/rules/type-strict-mode.md b/.claude/skills/php-best-practices/rules/type-strict-mode.md new file mode 100644 index 0000000..cfc8f58 --- /dev/null +++ b/.claude/skills/php-best-practices/rules/type-strict-mode.md @@ -0,0 +1,54 @@ +# type-strict-mode — Declare strict types + +**Priority**: CRITICAL + +Every PHP file must declare strict types on the first line after the opening tag. + +## Rule + +```php +set([ + 'worker_num' => swoole_cpu_num(), + 'max_request' => 10000, + 'enable_coroutine' => true, +]); + +$server->on('request', function (Request $request, Response $response): void { + $response->header('Content-Type', 'application/json'); + $response->end(json_encode(['status' => 'ok'], JSON_THROW_ON_ERROR)); +}); + +$server->start(); +``` + +## Swoole Coroutines + +```php +use Swoole\Coroutine; + +Coroutine\run(function (): void { + $results = []; + + // Parallel execution + $wg = new Coroutine\WaitGroup(); + + $wg->add(); + Coroutine::create(function () use (&$results, $wg): void { + $results['users'] = fetchUsers(); + $wg->done(); + }); + + $wg->add(); + Coroutine::create(function () use (&$results, $wg): void { + $results['orders'] = fetchOrders(); + $wg->done(); + }); + + $wg->wait(); + // Both complete, process results +}); +``` + +## ReactPHP Event Loop + +```php +use React\EventLoop\Loop; +use React\Http\HttpServer; +use React\Http\Message\Response; +use Psr\Http\Message\ServerRequestInterface; + +$server = new HttpServer(function (ServerRequestInterface $request): Response { + return new Response(200, ['Content-Type' => 'application/json'], '{"status":"ok"}'); +}); + +$socket = new \React\Socket\SocketServer('0.0.0.0:8080'); +$server->listen($socket); + +echo "Listening on port 8080\n"; +``` + +## ReactPHP Promises + +```php +use React\Promise\Deferred; + +function fetchAsync(string $url): \React\Promise\PromiseInterface +{ + $deferred = new Deferred(); + + $client->request('GET', $url)->then( + fn ($response) => $deferred->resolve($response->getBody()), + fn (\Throwable $e) => $deferred->reject($e), + ); + + return $deferred->promise(); +} + +// Parallel requests +\React\Promise\all([ + fetchAsync('https://api.example.com/users'), + fetchAsync('https://api.example.com/orders'), +])->then(function (array $results): void { + [$users, $orders] = $results; +}); +``` + +## Fibers for Async + +```php +final class AsyncTaskRunner +{ + /** @var array */ + private array $fibers = []; + + public function add(callable $task): void + { + $this->fibers[] = new Fiber($task); + } + + public function run(): array + { + $results = []; + + foreach ($this->fibers as $fiber) { + $fiber->start(); + } + + foreach ($this->fibers as $i => $fiber) { + while (!$fiber->isTerminated()) { + $fiber->resume(); + } + $results[$i] = $fiber->getReturn(); + } + + return $results; + } +} +``` + +## Connection Pooling (Swoole) + +```php +use Swoole\Database\PDOPool; +use Swoole\Database\PDOConfig; + +$pool = new PDOPool( + (new PDOConfig()) + ->withDriver('mysql') + ->withHost('localhost') + ->withDbName('app') + ->withUsername('root') + ->withPassword('secret'), + size: 64, +); + +Coroutine\run(function () use ($pool): void { + $pdo = $pool->get(); + try { + $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?'); + $stmt->execute([42]); + $user = $stmt->fetch(); + } finally { + $pool->put($pdo); + } +}); +``` diff --git a/.claude/skills/php-pro/references/laravel-patterns.md b/.claude/skills/php-pro/references/laravel-patterns.md new file mode 100644 index 0000000..3121a60 --- /dev/null +++ b/.claude/skills/php-pro/references/laravel-patterns.md @@ -0,0 +1,174 @@ +# Laravel Patterns + +## Service Pattern + +```php + $dto->userId, + 'total' => $dto->total, + 'status' => OrderStatus::Pending, + ]); + + $this->payment->charge($order); + $this->events->dispatch(new OrderPlaced($order)); + + return $order; + } +} +``` + +## Repository Pattern + +```php +final class EloquentOrderRepository implements OrderRepository +{ + public function findById(int $id): ?Order + { + return Order::find($id); + } + + public function findByUser(int $userId): Collection + { + return Order::where('user_id', $userId) + ->orderByDesc('created_at') + ->get(); + } + + public function save(Order $order): void + { + $order->save(); + } +} +``` + +## API Resources + +```php +final class OrderResource extends JsonResource +{ + /** @return array */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'status' => $this->status->value, + 'total' => $this->total, + 'items' => OrderItemResource::collection($this->whenLoaded('items')), + 'created_at' => $this->created_at->toIso8601String(), + ]; + } +} +``` + +## Form Requests + +```php +final class StoreOrderRequest extends FormRequest +{ + /** @return array */ + public function rules(): array + { + return [ + 'items' => ['required', 'array', 'min:1'], + 'items.*.product_id' => ['required', 'integer', 'exists:products,id'], + 'items.*.quantity' => ['required', 'integer', 'min:1'], + 'shipping_address_id' => ['required', 'exists:addresses,id'], + ]; + } + + public function toDTO(): PlaceOrderDTO + { + return new PlaceOrderDTO( + userId: $this->user()->id, + items: $this->validated('items'), + shippingAddressId: $this->validated('shipping_address_id'), + ); + } +} +``` + +## Jobs & Queues + +```php +final class ProcessOrderJob implements ShouldQueue +{ + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + + public int $tries = 3; + public int $backoff = 60; + + public function __construct( + public readonly Order $order, + ) { + } + + public function handle(OrderProcessor $processor): void + { + $processor->process($this->order); + } + + public function failed(\Throwable $exception): void + { + Log::error('Order processing failed', [ + 'order_id' => $this->order->id, + 'error' => $exception->getMessage(), + ]); + } +} +``` + +## Middleware + +```php +final class EnsureApiVersion +{ + public function handle(Request $request, Closure $next, string $version): Response + { + if ($request->header('Accept-Version') !== $version) { + return response()->json(['error' => 'Unsupported API version'], 406); + } + + return $next($request); + } +} +``` + +## Eloquent Scopes & Casts + +```php +final class Order extends Model +{ + protected function casts(): array + { + return [ + 'status' => OrderStatus::class, + 'total' => 'decimal:2', + 'metadata' => 'array', + 'placed_at' => 'immutable_datetime', + ]; + } + + public function scopeActive(Builder $query): Builder + { + return $query->whereIn('status', [OrderStatus::Pending, OrderStatus::Processing]); + } + + public function scopeForUser(Builder $query, int $userId): Builder + { + return $query->where('user_id', $userId); + } +} +``` diff --git a/.claude/skills/php-pro/references/modern-php-features.md b/.claude/skills/php-pro/references/modern-php-features.md new file mode 100644 index 0000000..c4a2302 --- /dev/null +++ b/.claude/skills/php-pro/references/modern-php-features.md @@ -0,0 +1,162 @@ +# Modern PHP Features (8.1–8.3+) + +## Readonly Properties & Classes + +```php + 'Active', + self::Inactive => 'Inactive', + self::Suspended => 'Suspended', + }; + } + + public function canTransitionTo(self $target): bool + { + return match ($this) { + self::Active => $target === self::Inactive || $target === self::Suspended, + self::Inactive => $target === self::Active, + self::Suspended => $target === self::Active, + }; + } +} +``` + +## Intersection & Union Types + +```php +// Union types (8.0) +function parse(string|int $input): Result|Error { } + +// Intersection types (8.1) +function process(Countable&Iterator $collection): void { } + +// DNF types (8.2) — combining union + intersection +function handle((Countable&Iterator)|null $collection): void { } + +// true, false, null as standalone types (8.2) +function alwaysTrue(): true { return true; } +``` + +## Match Expression (8.0) + +```php +$result = match ($statusCode) { + 200 => 'OK', + 301, 302 => 'Redirect', + 404 => 'Not Found', + 500 => 'Server Error', + default => 'Unknown', +}; +``` + +## Fibers (8.1) + +```php +$fiber = new Fiber(function (): void { + $value = Fiber::suspend('paused'); + echo "Resumed with: $value"; +}); + +$result = $fiber->start(); // 'paused' +$fiber->resume('continue'); // Resumed with: continue +``` + +## Attributes (8.0) + +```php +#[Route('/api/users', methods: ['GET'])] +#[Cache(maxAge: 3600)] +public function list(): JsonResponse { } + +// Custom attribute +#[Attribute(Attribute::TARGET_METHOD)] +final class RateLimit +{ + public function __construct( + public readonly int $maxRequests, + public readonly int $windowSeconds, + ) { + } +} +``` + +## First-class Callables (8.1) + +```php +$fn = strlen(...); +$mapped = array_map($entity->getId(...), $entities); +$filtered = array_filter($items, $this->isValid(...)); +``` + +## Named Arguments (8.0) + +```php +new DateTimeImmutable(datetime: '2024-01-01', timezone: new DateTimeZone('UTC')); +array_slice(array: $items, offset: 0, length: 10, preserve_keys: true); +``` + +## Typed Class Constants (8.3) + +```php +final class Config +{ + public const string VERSION = '1.0.0'; + public const int MAX_RETRIES = 3; + public const array DEFAULT_OPTIONS = ['timeout' => 30]; +} +``` + +## json_validate() (8.3) + +```php +if (json_validate($input)) { + $data = json_decode($input, true, flags: JSON_THROW_ON_ERROR); +} +``` + +## #[\Override] Attribute (8.3) + +```php +class Child extends Parent +{ + #[\Override] + public function process(): void + { + // Compile error if parent method doesn't exist + } +} +``` diff --git a/.claude/skills/php-pro/references/symfony-patterns.md b/.claude/skills/php-pro/references/symfony-patterns.md new file mode 100644 index 0000000..d0c4a30 --- /dev/null +++ b/.claude/skills/php-pro/references/symfony-patterns.md @@ -0,0 +1,183 @@ +# Symfony Patterns + +## Dependency Injection + +```php +set(CachedOrderRepository::class) + ->decorate(OrderRepository::class) + ->arg('$inner', service('.inner')); +``` + +## Events & Listeners + +```php +// Event class +final class OrderPlacedEvent +{ + public function __construct( + public readonly Order $order, + public readonly \DateTimeImmutable $placedAt, + ) { + } +} + +// Listener with attribute +#[AsEventListener(event: OrderPlacedEvent::class)] +final class SendOrderConfirmation +{ + public function __invoke(OrderPlacedEvent $event): void + { + // Handle event + } +} + +// Subscriber +#[AsEventListener(event: OrderPlacedEvent::class, method: 'onPlace')] +#[AsEventListener(event: OrderCancelledEvent::class, method: 'onCancel')] +final class OrderNotificationListener +{ + public function onPlace(OrderPlacedEvent $event): void { } + public function onCancel(OrderCancelledEvent $event): void { } +} +``` + +## Console Commands + +```php +#[AsCommand(name: 'app:import-products', description: 'Import products from CSV')] +final class ImportProductsCommand extends Command +{ + public function __construct( + private readonly ProductImporter $importer, + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this->addArgument('file', InputArgument::REQUIRED, 'CSV file path'); + $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Simulate import'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $file = $input->getArgument('file'); + $dryRun = $input->getOption('dry-run'); + + $result = $this->importer->import($file, $dryRun); + $io->success(\sprintf('Imported %d products.', $result->count())); + + return Command::SUCCESS; + } +} +``` + +## Security Voters + +```php +#[AsVoter] +final class OrderVoter extends Voter +{ + public const VIEW = 'ORDER_VIEW'; + public const EDIT = 'ORDER_EDIT'; + + protected function supports(string $attribute, mixed $subject): bool + { + return \in_array($attribute, [self::VIEW, self::EDIT], true) + && $subject instanceof Order; + } + + protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool + { + $user = $token->getUser(); + if (!$user instanceof User) { + return false; + } + + return match ($attribute) { + self::VIEW => $this->canView($subject, $user), + self::EDIT => $this->canEdit($subject, $user), + default => false, + }; + } + + private function canView(Order $order, User $user): bool + { + return $order->getCustomerId() === $user->getId(); + } + + private function canEdit(Order $order, User $user): bool + { + return $this->canView($order, $user) && !$order->isCompleted(); + } +} +``` + +## Messenger (Async) + +```php +// Message +final readonly class ProcessImageMessage +{ + public function __construct( + public int $imageId, + public string $targetFormat, + ) { + } +} + +// Handler +#[AsMessageHandler] +final class ProcessImageHandler +{ + public function __construct( + private readonly ImageProcessor $processor, + ) { + } + + public function __invoke(ProcessImageMessage $message): void + { + $this->processor->convert($message->imageId, $message->targetFormat); + } +} + +// Dispatch +$this->messageBus->dispatch(new ProcessImageMessage(imageId: 42, targetFormat: 'webp')); +``` + +## Compiler Passes + +```php +final class RegisterHandlersPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition('app.handler_registry')) { + return; + } + + $registry = $container->getDefinition('app.handler_registry'); + $tagged = $container->findTaggedServiceIds('app.handler'); + + foreach ($tagged as $id => $tags) { + $registry->addMethodCall('register', [new Reference($id)]); + } + } +} +``` diff --git a/.claude/skills/php-pro/references/testing-quality.md b/.claude/skills/php-pro/references/testing-quality.md new file mode 100644 index 0000000..e8ef81b --- /dev/null +++ b/.claude/skills/php-pro/references/testing-quality.md @@ -0,0 +1,159 @@ +# Testing & Quality + +## PHPUnit + +```php + */ + private ObjectProphecy $repository; + private OrderService $service; + + protected function setUp(): void + { + $this->repository = $this->prophesize(OrderRepository::class); + $this->service = new OrderService($this->repository->reveal()); + } + + /** @test */ + public function it_creates_order_with_valid_items(): void + { + $dto = new PlaceOrderDTO(userId: 1, items: [['product_id' => 1, 'quantity' => 2]]); + + $order = $this->service->place($dto); + + self::assertSame(1, $order->getUserId()); + $this->repository->save(Argument::type(Order::class))->shouldHaveBeenCalledOnce(); + } + + /** @test */ + public function it_rejects_empty_order(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->service->place(new PlaceOrderDTO(userId: 1, items: [])); + } +} +``` + +## Data Providers + +```php +/** @test @dataProvider invalidEmailProvider */ +public function it_rejects_invalid_emails(string $email): void +{ + $this->expectException(ValidationException::class); + + Email::fromString($email); +} + +public static function invalidEmailProvider(): iterable +{ + yield 'empty string' => ['']; + yield 'no at sign' => ['invalid']; + yield 'no domain' => ['user@']; + yield 'no local part' => ['@domain.com']; + yield 'spaces' => ['user @domain.com']; +} +``` + +## Pest (Alternative) + +```php +describe('OrderService', function (): void { + beforeEach(function (): void { + $this->repository = Mockery::mock(OrderRepository::class); + $this->service = new OrderService($this->repository); + }); + + it('creates order with valid items', function (): void { + $this->repository->shouldReceive('save')->once(); + + $order = $this->service->place($dto); + + expect($order->getUserId())->toBe(1); + }); + + it('rejects empty order')->throws(InvalidArgumentException::class); +}); +``` + +## PHPStan Configuration + +```neon +# phpstan.neon +parameters: + level: 9 + paths: + - src + treatPhpDocTypesAsCertain: false + reportUnmatchedIgnoredErrors: true + checkGenericClassInNonGenericObjectType: true + checkMissingIterableValueType: true +``` + +### Common PHPStan Fixes + +```php +// Level 9: generics required +/** @var array */ // not just array +/** @return list */ // not just array +/** @param Collection */ // not just Collection + +// Level 9: strict comparisons +if ($value === null) { } // not == +if (\in_array($item, $list, true)) // strict flag required + +// Level 9: dead code +// Remove unreachable branches, unused variables +``` + +## Integration Testing (Symfony) + +```php +final class OrderApiTest extends WebTestCase +{ + /** @test */ + public function it_creates_order_via_api(): void + { + $client = static::createClient(); + $client->request('POST', '/api/orders', [], [], [ + 'CONTENT_TYPE' => 'application/json', + ], json_encode(['items' => [['product_id' => 1, 'quantity' => 2]]], JSON_THROW_ON_ERROR)); + + self::assertResponseStatusCodeSame(201); + self::assertJsonContains(['status' => 'pending']); + } +} +``` + +## Test Coverage + +```xml + + + + src + + + + + + +``` + +Target: 80%+ line coverage. Focus on business logic, skip getters/configuration. + +## Quality Pipeline + +```bash +# Run in this order +composer cs:fix # Fix code style +composer phpstan # Static analysis (level 9) +composer tests # All tests +composer tests:coverage # With coverage report +``` diff --git a/.claude/skills/tdd/SKILL.md b/.claude/skills/tdd/SKILL.md new file mode 100644 index 0000000..97fc15a --- /dev/null +++ b/.claude/skills/tdd/SKILL.md @@ -0,0 +1,317 @@ +--- +name: tdd +description: Test-Driven Development workflow. Enforces red-green-refactor cycle — write failing test first, minimal code to pass, then refactor. No production code without a failing test. +allowed-tools: Read, Grep, Glob, Bash, Edit, Write +argument-hint: "[feature-or-bug-description]" +--- + +# Test-Driven Development (TDD) + +## Overview + +Write the test first. Watch it fail. Write minimal code to pass. + +Core principle: **If you didn't watch the test fail, you don't know if it tests the right thing.** + +Violating the letter of the rules is violating the spirit of the rules. + +## When to Use + +**Always:** +- New features +- Bug fixes +- Refactoring +- Behavior changes + +**Exceptions (ask your human partner):** +- Throwaway prototypes +- Generated code +- Configuration files + +Thinking "skip TDD just this once"? Stop. That's rationalization. + +## The Iron Law + +**NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST** + +Write code before the test? Delete it. Start over. + +No exceptions: +- Don't keep it as "reference" +- Don't "adapt" it while writing tests +- Don't look at it +- **Delete means delete** + +Implement fresh from tests. Period. + +## Red-Green-Refactor + +``` +RED (Write failing test) + → Verify fails correctly? + → yes → GREEN (Minimal code) + → Verify passes, all green? + → yes → REFACTOR (Clean up) + → Stay green → Next → RED + → no → GREEN + → wrong failure → RED +``` + +### RED - Write Failing Test + +Write one minimal test showing what should happen. + +**Good:** +```php +/** @test */ +public function it_rejects_empty_cache_tag(): void +{ + $this->expectException(InvalidArgumentException::class); + + CacheTag::fromString(''); +} +``` +Clear name, tests real behavior, one thing. + +**Bad:** +```php +/** @test */ +public function tag_works(): void +{ + // Vague name, tests multiple things, unclear intent +} +``` + +Requirements: +- One behavior +- Clear name +- Real code (no mocks unless unavoidable) + +### Verify RED - Watch It Fail + +**MANDATORY. Never skip.** + +```bash +composer tests -- --filter it_rejects_empty_cache_tag +``` + +Confirm: +- Test **fails** (not errors) +- Failure message is expected +- Fails because feature missing (not typos) + +Test passes? You're testing existing behavior. Fix test. + +Test errors? Fix error, re-run until it **fails** correctly. + +### GREEN - Minimal Code + +Write simplest code to pass the test. + +Don't add features, refactor other code, or "improve" beyond the test. + +### Verify GREEN - Watch It Pass + +**MANDATORY.** + +```bash +composer tests -- --filter it_rejects_empty_cache_tag +``` + +Confirm: +- Test passes +- Other tests still pass +- Output pristine (no errors, warnings) + +Test fails? Fix code, not test. + +Other tests fail? Fix now. + +### REFACTOR - Clean Up + +After green only: +- Remove duplication +- Improve names +- Extract helpers + +Keep tests green. Don't add behavior. + +### Repeat + +Next failing test for next feature. + +## Good Tests + +| Quality | Good | Bad | +|---------|------|-----| +| Minimal | One thing. "and" in name? Split it. | `test_validates_email_and_domain_and_whitespace` | +| Clear | Name describes behavior | `test1` | +| Shows intent | Demonstrates desired API | Obscures what code should do | + +## Why Order Matters + +**"I'll write tests after to verify it works"** + +Tests written after code pass immediately. Passing immediately proves nothing: +- Might test wrong thing +- Might test implementation, not behavior +- Might miss edge cases you forgot +- You never saw it catch the bug + +Test-first forces you to see the test fail, proving it actually tests something. + +**"I already manually tested all the edge cases"** + +Manual testing is ad-hoc. You think you tested everything but: +- No record of what you tested +- Can't re-run when code changes +- Easy to forget cases under pressure +- "It worked when I tried it" ≠ comprehensive + +Automated tests are systematic. They run the same way every time. + +**"Deleting X hours of work is wasteful"** + +Sunk cost fallacy. The time is already gone. Your choice now: +- Delete and rewrite with TDD (X more hours, high confidence) +- Keep it and add tests after (30 min, low confidence, likely bugs) + +The "waste" is keeping code you can't trust. Working code without real tests is technical debt. + +**"TDD is dogmatic, being pragmatic means adapting"** + +TDD IS pragmatic: +- Finds bugs before commit (faster than debugging after) +- Prevents regressions (tests catch breaks immediately) +- Documents behavior (tests show how to use code) +- Enables refactoring (change freely, tests catch breaks) + +"Pragmatic" shortcuts = debugging in production = slower. + +**"Tests after achieve the same goals - it's spirit not ritual"** + +No. Tests-after answer "What does this do?" Tests-first answer "What should this do?" + +Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge cases, not discovered ones. + +Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't). + +30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work. + +## Common Rationalizations + +| Excuse | Reality | +|--------|---------| +| "Too simple to test" | Simple code breaks. Test takes 30 seconds. | +| "I'll test after" | Tests passing immediately prove nothing. | +| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" | +| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. | +| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. | +| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. | +| "Need to explore first" | Fine. Throw away exploration, start with TDD. | +| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. | +| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. | +| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. | +| "Existing code has no tests" | You're improving it. Add tests for existing code. | + +## Red Flags - STOP and Start Over + +- Code before test +- Test after implementation +- Test passes immediately +- Can't explain why test failed +- Tests added "later" +- Rationalizing "just this once" +- "I already manually tested it" +- "Tests after achieve the same purpose" +- "It's about spirit not ritual" +- "Keep as reference" or "adapt existing code" +- "Already spent X hours, deleting is wasteful" +- "TDD is dogmatic, I'm being pragmatic" +- "This is different because..." + +**All of these mean: Delete code. Start over with TDD.** + +## Example: Bug Fix + +Bug: Empty cache tag accepted silently + +**RED** +```php +/** @test */ +public function it_rejects_empty_cache_tag(): void +{ + $this->expectException(InvalidArgumentException::class); + + CacheTag::fromString(''); +} +``` + +**Verify RED** +``` +$ composer tests -- --filter it_rejects_empty_cache_tag +FAIL: Expected InvalidArgumentException, none thrown +``` + +**GREEN** +```php +public static function fromString(string $tag, ?CacheType $type = null): self +{ + if ('' === trim($tag)) { + throw InvalidArgumentException::becauseCacheTagIsEmpty(); + } + + return new self($tag, $type ?? CacheTypeFactory::createEmpty()); +} +``` + +**Verify GREEN** +``` +$ composer tests -- --filter it_rejects_empty_cache_tag +PASS +``` + +**REFACTOR** - Extract validation if needed for multiple entry points. + +## Verification Checklist + +Before marking work complete: + +- [ ] Every new function/method has a test +- [ ] Watched each test fail before implementing +- [ ] Each test failed for expected reason (feature missing, not typo) +- [ ] Wrote minimal code to pass each test +- [ ] All tests pass +- [ ] Output pristine (no errors, warnings) +- [ ] Tests use real code (mocks only if unavoidable) +- [ ] Edge cases and errors covered + +Can't check all boxes? You skipped TDD. Start over. + +## When Stuck + +| Problem | Solution | +|---------|----------| +| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. | +| Test too complicated | Design too complicated. Simplify interface. | +| Must mock everything | Code too coupled. Use dependency injection. | +| Test setup huge | Extract helpers. Still complex? Simplify design. | + +## Debugging Integration + +Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression. + +**Never fix bugs without a test.** + +## Testing Anti-Patterns + +Avoid these common pitfalls: +- Testing mock behavior instead of real behavior +- Adding test-only methods to production classes +- Mocking without understanding dependencies + +## Final Rule + +Production code → test exists and failed first. +Otherwise → not TDD. +**No exceptions without your human partner's permission.** diff --git a/.claude/skills/web-search/SKILL.md b/.claude/skills/web-search/SKILL.md new file mode 100644 index 0000000..21838a4 --- /dev/null +++ b/.claude/skills/web-search/SKILL.md @@ -0,0 +1,129 @@ +--- +name: web-search +description: Search the web and extract content from URLs via inference.sh CLI. Uses Tavily and Exa for AI-powered search, answers, and content extraction. +allowed-tools: Bash(infsh *), Read +argument-hint: "[query-or-url]" +--- + +# Web Search & Extraction + +Search the web and extract content via inference.sh CLI. + +## Quick Start + +```bash +curl -fsSL https://cli.inference.sh | sh && infsh login +``` + +Install note: The install script only detects your OS/architecture, downloads the matching binary from dist.inference.sh, and verifies its SHA-256 checksum. No elevated permissions or background processes. + +## Available Apps + +### Tavily + +| App | App ID | Description | +|-----|--------|-------------| +| Search Assistant | `tavily/search-assistant` | AI-powered search with answers | +| Extract | `tavily/extract` | Extract content from URLs | + +### Exa + +| App | App ID | Description | +|-----|--------|-------------| +| Search | `exa/search` | Smart web search with AI | +| Answer | `exa/answer` | Direct factual answers | +| Extract | `exa/extract` | Extract and analyze web content | + +## Examples + +### Tavily Search + +```bash +infsh app run tavily/search-assistant --input '{ + "query": "What are the best practices for building AI agents?" +}' +``` + +Returns AI-generated answers with sources and images. + +### Tavily Extract + +```bash +infsh app run tavily/extract --input '{ + "urls": ["https://example.com/article1", "https://example.com/article2"] +}' +``` + +Extracts clean text and images from multiple URLs. + +### Exa Search + +```bash +infsh app run exa/search --input '{ + "query": "machine learning frameworks comparison" +}' +``` + +Returns highly relevant links with context. + +### Exa Answer + +```bash +infsh app run exa/answer --input '{ + "question": "What is the population of Tokyo?" +}' +``` + +Returns direct factual answers. + +### Exa Extract + +```bash +infsh app run exa/extract --input '{ + "url": "https://example.com/research-paper" +}' +``` + +Extracts and analyzes web page content. + +## Workflow: Research + LLM + +```bash +# 1. Search for information +infsh app run tavily/search-assistant --input '{ + "query": "latest developments in quantum computing" +}' > search_results.json + +# 2. Analyze with Claude +infsh app run openrouter/claude-sonnet-45 --input '{ + "prompt": "Based on this research, summarize the key trends: " +}' +``` + +## Workflow: Extract + Summarize + +```bash +# 1. Extract content from URL +infsh app run tavily/extract --input '{ + "urls": ["https://example.com/long-article"] +}' > content.json + +# 2. Summarize with LLM +infsh app run openrouter/claude-haiku-45 --input '{ + "prompt": "Summarize this article in 3 bullet points: " +}' +``` + +## Use Cases + +- **Research**: Gather information on any topic +- **RAG**: Retrieval-augmented generation +- **Fact-checking**: Verify claims with sources +- **Content aggregation**: Collect data from multiple sources +- **Agents**: Build research-capable AI agents + +## Browse All Apps + +```bash +infsh app list +``` diff --git a/.claude/skills/writing-skills/SKILL.md b/.claude/skills/writing-skills/SKILL.md new file mode 100644 index 0000000..2d5d989 --- /dev/null +++ b/.claude/skills/writing-skills/SKILL.md @@ -0,0 +1,448 @@ +--- +name: writing-skills +description: Use when creating or editing Claude Code skills (SKILL.md files). Applies TDD to documentation — baseline test, write skill, close loopholes. Use when agent behavior needs to be codified into reusable skills. +allowed-tools: Read, Grep, Glob, Bash, Edit, Write +argument-hint: "[skill-name-or-topic]" +--- + +# Writing Skills + +## Overview + +Writing skills IS Test-Driven Development applied to process documentation. + +You write test cases (pressure scenarios with subagents), watch them fail (baseline behavior), write the skill (documentation), watch tests pass (agents comply), and refactor (close loopholes). + +Core principle: **If you didn't watch an agent fail without the skill, you don't know if the skill teaches the right thing.** + +**REQUIRED BACKGROUND:** You MUST understand the TDD skill before using this skill. That skill defines the fundamental RED-GREEN-REFACTOR cycle. This skill adapts TDD to documentation. + +## What is a Skill? + +A skill is a reference guide for proven techniques, patterns, or tools. Skills help future Claude instances find and apply effective approaches. + +**Skills are:** Reusable techniques, patterns, tools, reference guides + +**Skills are NOT:** Narratives about how you solved a problem once + +## TDD Mapping for Skills + +| TDD Concept | Skill Creation | +|-------------|----------------| +| Test case | Pressure scenario with subagent | +| Production code | Skill document (SKILL.md) | +| Test fails (RED) | Agent violates rule without skill (baseline) | +| Test passes (GREEN) | Agent complies with skill present | +| Refactor | Close loopholes while maintaining compliance | +| Write test first | Run baseline scenario BEFORE writing skill | +| Watch it fail | Document exact rationalizations agent uses | +| Minimal code | Write skill addressing those specific violations | +| Watch it pass | Verify agent now complies | +| Refactor cycle | Find new rationalizations → plug → re-verify | + +The entire skill creation process follows RED-GREEN-REFACTOR. + +## When to Create a Skill + +**Create when:** +- Technique wasn't intuitively obvious to you +- You'd reference this again across projects +- Pattern applies broadly (not project-specific) +- Others would benefit + +**Don't create for:** +- One-off solutions +- Standard practices well-documented elsewhere +- Project-specific conventions (put in CLAUDE.md) +- Mechanical constraints (if enforceable with regex/validation, automate it — save documentation for judgment calls) + +## Skill Types + +- **Technique** — Concrete method with steps (condition-based-waiting, root-cause-tracing) +- **Pattern** — Way of thinking about problems (flatten-with-flags, test-invariants) +- **Reference** — API docs, syntax guides, tool documentation + +## Directory Structure + +``` +skills/ + skill-name/ + SKILL.md # Main reference (required) + supporting-file.* # Only if needed +``` + +Flat namespace — all skills in one searchable namespace. + +**Separate files for:** +- Heavy reference (100+ lines) — API docs, comprehensive syntax +- Reusable tools — Scripts, utilities, templates + +**Keep inline:** +- Principles and concepts +- Code patterns (< 50 lines) +- Everything else + +## SKILL.md Structure + +### Frontmatter (YAML) + +- Only two fields supported: `name` and `description` +- Max 1024 characters total +- `name`: Use letters, numbers, and hyphens only (no parentheses, special chars) +- `description`: Third-person, describes ONLY when to use (NOT what it does) +- Start with "Use when..." to focus on triggering conditions +- Include specific symptoms, situations, and contexts +- **NEVER summarize the skill's process or workflow** (see CSO section for why) +- Keep under 500 characters if possible + +```yaml +--- +name: Skill-Name-With-Hyphens +description: Use when [specific triggering conditions and symptoms] +--- +``` + +### Body Structure + +```markdown +# Skill Name + +## Overview +What is this? Core principle in 1-2 sentences. + +## When to Use +Bullet list with SYMPTOMS and use cases. When NOT to use. + +## Core Pattern (for techniques/patterns) +Before/after code comparison + +## Quick Reference +Table or bullets for scanning common operations + +## Implementation +Inline code for simple patterns. Link to file for heavy reference. + +## Common Mistakes +What goes wrong + fixes + +## Real-World Impact (optional) +Concrete results +``` + +## Claude Search Optimization (CSO) + +Critical for discovery: Future Claude needs to FIND your skill. + +### 1. Rich Description Field + +**CRITICAL: Description = When to Use, NOT What the Skill Does** + +The description should ONLY describe triggering conditions. Do NOT summarize the skill's process or workflow in the description. + +**Why this matters:** Testing revealed that when a description summarizes the skill's workflow, Claude may follow the description instead of reading the full skill content. A description saying "code review between tasks" caused Claude to do ONE review, even though the skill's flowchart clearly showed TWO reviews. + +When the description was changed to just triggering conditions (no workflow summary), Claude correctly read the flowchart and followed the full process. + +**The trap:** Descriptions that summarize workflow create a shortcut Claude will take. The skill body becomes documentation Claude skips. + +```yaml +# BAD: Summarizes workflow - Claude may follow this instead of reading skill +description: Use when executing plans - dispatches subagent per task with code review between tasks + +# BAD: Too much process detail +description: Use for TDD - write test first, watch it fail, write minimal code, refactor + +# GOOD: Just triggering conditions, no workflow summary +description: Use when executing implementation plans with independent tasks in the current session + +# GOOD: Triggering conditions only +description: Use when implementing any feature or bugfix, before writing implementation code +``` + +### 2. Keyword Coverage + +Use words Claude would search for: +- Error messages: "Hook timed out", "ENOTEMPTY", "race condition" +- Symptoms: "flaky", "hanging", "zombie", "pollution" +- Synonyms: "timeout/hang/freeze", "cleanup/teardown/afterEach" +- Tools: Actual commands, library names, file types + +### 3. Descriptive Naming + +Use active voice, verb-first: +- `creating-skills` not `skill-creation` +- `condition-based-waiting` not `async-test-helpers` + +Name by what you DO or core insight: +- `condition-based-waiting` > `async-test-helpers` +- `root-cause-tracing` > `debugging-techniques` +- `flatten-with-flags` > `data-structure-refactoring` + +### 4. Token Efficiency (Critical) + +Problem: Getting-started and frequently-referenced skills load into EVERY conversation. Every token counts. + +**Target word counts:** +- Getting-started workflows: <150 words each +- Frequently-loaded skills: <200 words total +- Other skills: <500 words (still be concise) + +**Techniques:** + +Move details to tool help: +```markdown +# BAD: Document all flags in SKILL.md +search-conversations supports --text, --both, --after DATE, --before DATE, --limit N + +# GOOD: Reference --help +search-conversations supports multiple modes and filters. Run --help for details. +``` + +Use cross-references instead of repeating content. Compress examples. Eliminate redundancy. + +### 5. Cross-Referencing Other Skills + +Use skill name only, with explicit requirement markers: + +```markdown +# GOOD +**REQUIRED SUB-SKILL:** Use tdd skill +**REQUIRED BACKGROUND:** You MUST understand debug skill + +# BAD +See skills/testing/test-driven-development (unclear if required) +@skills/testing/test-driven-development/SKILL.md (force-loads, burns context) +``` + +Why no @ links: @ syntax force-loads files immediately, consuming context before you need them. + +## Bulletproofing Skills Against Rationalization + +Skills that enforce discipline (like TDD) need to resist rationalization. Agents are smart and will find loopholes when under pressure. + +### Close Every Loophole Explicitly + +Don't just state the rule — forbid specific workarounds: + +```markdown +No exceptions: +- Don't keep it as "reference" +- Don't "adapt" it while writing tests +- Don't look at it +- Delete means delete +``` + +### Address "Spirit vs Letter" Arguments + +Add foundational principle early: + +```markdown +**Violating the letter of the rules is violating the spirit of the rules.** +``` + +This cuts off the entire class of "I'm following the spirit" rationalizations. + +### Build Rationalization Table + +Capture rationalizations from baseline testing. Every excuse agents make goes in the table: + +```markdown +| Excuse | Reality | +|--------|---------| +| "Too simple to test" | Simple code breaks. Test takes 30 seconds. | +| "I'll test after" | Tests passing immediately prove nothing. | +``` + +### Create Red Flags List + +Make it easy for agents to self-check when rationalizing: + +```markdown +## Red Flags - STOP and Start Over +- Code before test +- "I already manually tested it" +- "This is different because..." + +**All of these mean: Delete code. Start over with TDD.** +``` + +## The Iron Law (Same as TDD) + +**NO SKILL WITHOUT A FAILING TEST FIRST** + +This applies to NEW skills AND EDITS to existing skills. + +Write skill before testing? Delete it. Start over. Edit skill without testing? Same violation. + +No exceptions: +- Not for "simple additions" +- Not for "just adding a section" +- Not for "documentation updates" +- Don't keep untested changes as "reference" +- Don't "adapt" while running tests +- Delete means delete + +## RED-GREEN-REFACTOR for Skills + +### RED: Write Failing Test (Baseline) + +Run pressure scenario with subagent WITHOUT the skill. Document exact behavior: +- What choices did they make? +- What rationalizations did they use (verbatim)? +- Which pressures triggered violations? + +This is "watch the test fail" — you must see what agents naturally do before writing the skill. + +### GREEN: Write Minimal Skill + +Write skill that addresses those specific rationalizations. Don't add extra content for hypothetical cases. + +Run same scenarios WITH skill. Agent should now comply. + +### REFACTOR: Close Loopholes + +Agent found new rationalization? Add explicit counter. Re-test until bulletproof. + +## Testing All Skill Types + +### Discipline-Enforcing Skills (rules/requirements) + +Test with: Academic questions, pressure scenarios, multiple pressures combined. +Success criteria: Agent follows rule under maximum pressure. + +### Technique Skills (how-to guides) + +Test with: Application scenarios, variation scenarios, missing information tests. +Success criteria: Agent successfully applies technique to new scenario. + +### Pattern Skills (mental models) + +Test with: Recognition scenarios, application scenarios, counter-examples. +Success criteria: Agent correctly identifies when/how to apply pattern. + +### Reference Skills (documentation/APIs) + +Test with: Retrieval scenarios, application scenarios, gap testing. +Success criteria: Agent finds and correctly applies reference information. + +## Common Rationalizations for Skipping Testing + +| Excuse | Reality | +|--------|---------| +| "Skill is obviously clear" | Clear to you ≠ clear to other agents. Test it. | +| "It's just a reference" | References can have gaps. Test retrieval. | +| "Testing is overkill" | Untested skills have issues. Always. 15 min saves hours. | +| "I'll test if problems emerge" | Problems = agents can't use skill. Test BEFORE deploying. | +| "Too tedious to test" | Testing is less tedious than debugging bad skill in production. | +| "I'm confident it's good" | Overconfidence guarantees issues. Test anyway. | +| "Academic review is enough" | Reading ≠ using. Test application scenarios. | +| "No time to test" | Deploying untested skill wastes more time fixing it later. | + +All of these mean: Test before deploying. No exceptions. + +## Code Examples + +One excellent example beats many mediocre ones. + +**Good example:** +- Complete and runnable +- Well-commented explaining WHY +- From real scenario +- Shows pattern clearly +- Ready to adapt (not generic template) + +**Don't:** +- Implement in 5+ languages +- Create fill-in-the-blank templates +- Write contrived examples + +You're good at porting — one great example is enough. + +## Flowchart Usage + +Use flowcharts ONLY for: +- Non-obvious decision points +- Process loops where you might stop too early +- "When to use A vs B" decisions + +Never use flowcharts for: +- Reference material → Tables, lists +- Code examples → Markdown blocks +- Linear instructions → Numbered lists +- Labels without semantic meaning (step1, helper2) + +## Anti-Patterns + +- **Narrative Example** — "In session 2025-10-03, we found..." Too specific, not reusable. +- **Multi-Language Dilution** — example-js.js, example-py.py, example-go.go. Mediocre quality, maintenance burden. +- **Code in Flowcharts** — Can't copy-paste, hard to read. +- **Generic Labels** — helper1, helper2, step3. Labels should have semantic meaning. + +## STOP: Before Moving to Next Skill + +After writing ANY skill, you MUST STOP and complete the deployment process. + +Do NOT: +- Create multiple skills in batch without testing each +- Move to next skill before current one is verified +- Skip testing because "batching is more efficient" + +Deploying untested skills = deploying untested code. + +## Skill Creation Checklist (TDD Adapted) + +### RED Phase — Write Failing Test: +- [ ] Create pressure scenarios (3+ combined pressures for discipline skills) +- [ ] Run scenarios WITHOUT skill — document baseline behavior verbatim +- [ ] Identify patterns in rationalizations/failures + +### GREEN Phase — Write Minimal Skill: +- [ ] Name uses only letters, numbers, hyphens +- [ ] YAML frontmatter with only name and description (max 1024 chars) +- [ ] Description starts with "Use when..." and includes specific triggers/symptoms +- [ ] Description written in third person +- [ ] Keywords throughout for search (errors, symptoms, tools) +- [ ] Clear overview with core principle +- [ ] Address specific baseline failures identified in RED +- [ ] One excellent example (not multi-language) +- [ ] Run scenarios WITH skill — verify agents now comply + +### REFACTOR Phase — Close Loopholes: +- [ ] Identify NEW rationalizations from testing +- [ ] Add explicit counters (if discipline skill) +- [ ] Build rationalization table from all test iterations +- [ ] Create red flags list +- [ ] Re-test until bulletproof + +### Quality Checks: +- [ ] Small flowchart only if decision non-obvious +- [ ] Quick reference table +- [ ] Common mistakes section +- [ ] No narrative storytelling +- [ ] Supporting files only for tools or heavy reference + +### Deployment: +- [ ] Commit skill to git and push +- [ ] Consider contributing back via PR (if broadly useful) + +## Discovery Workflow + +How future Claude finds your skill: + +1. Encounters problem ("tests are flaky") +2. Finds SKILL (description matches) +3. Scans overview (is this relevant?) +4. Reads patterns (quick reference table) +5. Loads example (only when implementing) + +Optimize for this flow — put searchable terms early and often. + +## The Bottom Line + +Creating skills IS TDD for process documentation. + +Same Iron Law: No skill without failing test first. +Same cycle: RED (baseline) → GREEN (write skill) → REFACTOR (close loopholes). +Same benefits: Better quality, fewer surprises, bulletproof results. + +**If you follow TDD for code, follow it for skills. It's the same discipline applied to documentation.** diff --git a/CLAUDE.md b/CLAUDE.md index c6f1d4a..342123a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ ## Project Overview -**teamneusta/pimcore-http-cache-bundle** — A Symfony/Pimcore bundle that adds active HTTP cache invalidation via cache tags. It extends FOSHttpCacheBundle for Pimcore, automatically tagging responses and invalidating caches when Pimcore elements (documents, assets, data objects) change. +**teamneusta/pimcore-http-cache-bundle** — A Symfony/Pimcore bundle that adds automatic HTTP cache invalidation via cache tags. It extends FOSHttpCacheBundle for Pimcore, automatically tagging responses when elements are loaded and invalidating caches when elements (documents, assets, data objects) are saved or deleted. Works with reverse proxies like Varnish and Fastly. ## Quick Reference @@ -34,7 +34,7 @@ src/ # Main source (Neusta\Pimcore\HttpCacheBundle\ │ ├── CacheType/ # Cache type strategy implementations │ └── ResponseTagger/ # Response tagging decorator chain ├── DependencyInjection/ # Symfony DI extension + compiler passes -├── Element/ # Pimcore element handling +├── Element/ # Pimcore element handling, events, listeners ├── Exception/ # Custom exceptions └── NeustaPimcoreHttpCacheBundle.php tests/ @@ -44,31 +44,50 @@ tests/ config/services.php # Symfony DI service definitions ``` +## How the Bundle Works + +**Tagging**: When a Pimcore element is loaded during a request, the response is automatically tagged (`a{id}` for assets, `d{id}` for documents, `o{id}` for objects). Tags end up in the `X-Cache-Tags` header. + +**Invalidation**: When an element is saved or deleted, the corresponding cache tags are invalidated via the reverse proxy. Skipped for `saveVersionOnly` and `autoSave`. + +**Events**: `ElementTaggingEvent` and `ElementInvalidationEvent` let users add extra tags or cancel operations. + +**Public API** (autowired services): `CacheActivator`, `CacheInvalidator`, `ResponseTagger` +**Value objects**: `CacheTags`, `CacheTag`, `CacheTypeFactory` + +See `.claude/rules/bundle-usage.md` for detailed usage guide with examples. + ## Coding Conventions -- **PHP**: 8.1 / 8.2; every file starts with `declare(strict_types=1);` +- **PHP**: 8.1 / 8.2; every file starts with ` Date: Fri, 27 Feb 2026 11:20:05 +0100 Subject: [PATCH 4/8] Add code review skill and writer/reviewer role separation - /review skill with systematic 8-category checklist: dead code, architecture, documentation, tests, security, performance, BC breaks, static analysis - Detailed checklist with project-specific checks - roles.md rule defining writer vs reviewer behavior - Severity guide: BLOCKER / WARNING / SUGGESTION Co-Authored-By: Claude Opus 4.6 --- .claude/rules/roles.md | 27 ++++ .claude/skills/review/SKILL.md | 103 +++++++++++++ .claude/skills/review/checklist.md | 240 +++++++++++++++++++++++++++++ 3 files changed, 370 insertions(+) create mode 100644 .claude/rules/roles.md create mode 100644 .claude/skills/review/SKILL.md create mode 100644 .claude/skills/review/checklist.md diff --git a/.claude/rules/roles.md b/.claude/rules/roles.md new file mode 100644 index 0000000..586b948 --- /dev/null +++ b/.claude/rules/roles.md @@ -0,0 +1,27 @@ +# Roles + +Claude Code operates in two distinct roles in this project. Never mix them. + +## Writer Role (default) + +You ARE the author. You write code, fix bugs, implement features. +- Follow all rules in `code-style.md`, `testing.md`, `architecture.md` +- Use TDD: write failing test first, then implementation +- Run `composer cs:fix` and `composer phpstan` before finishing +- You own the code — make decisions, implement them + +## Reviewer Role (via `/review`) + +You are NOT the author. You review someone else's changes (or your own from a prior session). +- **Do not fix issues yourself** — report them with file:line, severity, and suggested fix +- **Do not assume intent** — ask if a change is unclear +- **Be specific** — every finding needs a concrete location and reason +- **Check all 8 categories**: dead code, architecture, documentation, tests, security, performance, BC breaks, static analysis +- If you wrote the code being reviewed, be extra critical — you have blind spots on your own work + +## Switching Roles + +- Default is Writer +- Use `/review` to switch to Reviewer mode +- After review is complete, you return to Writer if asked to fix findings +- Never review and fix in the same pass — separate the roles diff --git a/.claude/skills/review/SKILL.md b/.claude/skills/review/SKILL.md new file mode 100644 index 0000000..e9e52c1 --- /dev/null +++ b/.claude/skills/review/SKILL.md @@ -0,0 +1,103 @@ +--- +name: review +description: Use when reviewing code changes — PR review, branch diff review, or pre-commit review. Performs systematic check for dead code, architecture compliance, documentation, test coverage, security, performance, BC breaks, and static analysis. +allowed-tools: Read, Grep, Glob, Bash(git *), Bash(composer *) +argument-hint: "[branch-or-pr-or-file]" +--- + +# Code Review + +Systematic code review for this project. You are acting as an independent code reviewer — be thorough, critical, and constructive. + +## Review Process + +``` +1. SCOPE: Understand what changed and why +2. CHECK: Run through all review categories +3. REPORT: Output findings with severity and locations +4. SUGGEST: Provide concrete fixes, not vague advice +``` + +### Step 1: Understand the Scope + +```bash +# For branch review (compare against main) +git diff main...HEAD --stat +git log main..HEAD --oneline + +# For specific commit +git show --stat + +# For unstaged changes +git diff --stat +``` + +Read the changed files. Understand the intent before judging the implementation. + +### Step 2: Run All Review Categories + +Work through each category in the checklist below. For each finding, note: +- **File and line number** (e.g., `src/Cache/Foo.php:42`) +- **Severity**: BLOCKER / WARNING / SUGGESTION +- **Category**: Which checklist item +- **Description**: What's wrong and why +- **Fix**: Concrete suggestion + +### Step 3: Output Format + +``` +## Review Summary +X blocker(s), Y warning(s), Z suggestion(s) + +## Blockers +- `src/File.php:42` — [category] Description. Fix: ... + +## Warnings +- `src/File.php:15` — [category] Description. Fix: ... + +## Suggestions +- `src/File.php:78` — [category] Description. Fix: ... + +## Checklist +- [x] Dead code +- [x] Architecture compliance +- [ ] Documentation — CHANGELOG needs update +- [x] Test coverage +... +``` + +--- + +## Review Checklist + +See `checklist.md` in this directory for the full detailed checklist with examples. + +### Quick Reference + +| # | Category | Severity | Check | +|---|----------|----------|-------| +| 1 | Dead code | WARNING | Unused classes, methods, imports, variables | +| 2 | Architecture | BLOCKER | Decorator pattern, immutability, single-method interfaces | +| 3 | Documentation | WARNING | CHANGELOG, doc/, PHPDoc, README sync | +| 4 | Test coverage | BLOCKER | Every change has a test, conventions followed | +| 5 | Security | BLOCKER | Input validation, injection, OWASP | +| 6 | Performance | WARNING | N+1, unnecessary loops, missing early returns | +| 7 | BC breaks | BLOCKER | Public API changes, removed services, config | +| 8 | Static analysis | WARNING | PHPStan level 8, CS fixer compliance | + +### Severity Guide + +- **BLOCKER** — Must fix before merge. Bugs, security issues, BC breaks, missing tests. +- **WARNING** — Should fix. Dead code, missing docs, performance concerns. +- **SUGGESTION** — Nice to have. Style improvements, minor refactoring. + +--- + +## Role Separation + +When acting as reviewer, you are NOT the author. This means: +- Do not fix issues yourself — report them +- Do not assume intent — ask if unclear +- Do not approve your own code — if you wrote it, you cannot review it +- Be specific — "this looks wrong" is not a review comment +- Be constructive — explain WHY something is an issue, not just THAT it is diff --git a/.claude/skills/review/checklist.md b/.claude/skills/review/checklist.md new file mode 100644 index 0000000..a353b04 --- /dev/null +++ b/.claude/skills/review/checklist.md @@ -0,0 +1,240 @@ +# Review Checklist — Full Details + +## 1. Dead Code Detection + +### What to Check + +- **Unused imports** — `use` statements that aren't referenced in the file +- **Unused methods** — private/protected methods not called within the class +- **Unused variables** — assigned but never read +- **Unused classes** — new files that nothing references (check `services.php`, other classes) +- **Unreachable code** — code after return/throw, impossible conditions +- **Commented-out code** — delete it, git remembers +- **Orphaned services** — services defined in `config/services.php` that are no longer needed +- **Dead config options** — configuration tree entries that aren't read anywhere + +### How to Check + +```bash +# Search for usages of a class/method +grep -r "ClassName" src/ tests/ config/ + +# Check if a service ID is referenced +grep -r "service_id" config/ src/DependencyInjection/ +``` + +### Severity: WARNING + +Dead code is not a bug, but it adds confusion and maintenance burden. Flag it for removal. + +--- + +## 2. Architecture Compliance + +### What to Check + +**Classes:** +- [ ] New classes are `final` (unless there's a documented reason) +- [ ] Interfaces have clean names — no `Interface` suffix +- [ ] Value objects are immutable (readonly properties, `with()` methods) +- [ ] Single responsibility — class does one thing + +**Constructors:** +- [ ] Constructor property promotion used +- [ ] Properties are `readonly` where possible +- [ ] Trailing comma on last parameter +- [ ] Dependencies injected, not created internally + +**Patterns:** +- [ ] Decorator pattern used for cross-cutting concerns (not inheritance) +- [ ] Single-method interfaces for core contracts +- [ ] Events for extensibility (not direct coupling) +- [ ] Adapter pattern for external dependencies (FOSHttpCache) +- [ ] Named static factories for exceptions (`becauseReasonHere()`) + +**Naming:** +- [ ] Cache tag format respected: `a{id}`, `d{id}`, `o{id}` +- [ ] Namespace follows PSR-4: `Neusta\Pimcore\HttpCacheBundle\` +- [ ] Test namespace: `Neusta\Pimcore\HttpCacheBundle\Tests\` + +### Red Flags + +- Non-final class without `@internal` annotation +- Mutable properties on value objects +- Service locator or `new` in a service constructor +- God class with many responsibilities +- Breaking the decorator chain without reason + +### Severity: BLOCKER for pattern violations, WARNING for naming + +--- + +## 3. Documentation Sync + +### What to Check + +- [ ] **CHANGELOG.md** — Does the change warrant a changelog entry? + - New features: YES + - Bug fixes: YES + - Internal refactoring: NO (unless it affects public API) +- [ ] **doc/ files** — If behavior changed, are docs updated? + - New config options → `doc/2-configuration.md` + - New events → `doc/4-events.md` + - New cache types → `doc/7-custom-cache-types.md` +- [ ] **PHPDoc** — Complex types annotated? (`@param array`, `@return list`) + - Required when PHP type system is insufficient (generics, array shapes) + - NOT required for simple scalar types +- [ ] **README.md** — Major features or install process changed? +- [ ] **CLAUDE.md / rules** — Architecture or conventions changed? + +### When to Skip + +- PHPDoc for obvious types (`string`, `int`, `void`) +- CHANGELOG for pure test changes +- README for internal changes + +### Severity: WARNING + +--- + +## 4. Test Coverage + +### What to Check + +- [ ] Every new public method has at least one test +- [ ] Every bug fix has a regression test +- [ ] Edge cases covered (empty input, null, boundary values) +- [ ] Tests follow project conventions: + - `@test` annotation (not `test` prefix) + - snake_case method names describing behavior + - Prophecy for mocking with typed `ObjectProphecy` properties + - `self::assert*()` for state verification + - Unit tests mirror `src/` structure in `tests/Unit/` +- [ ] Integration tests use correct base class (`ConfigurableKernelTestCase` or `ConfigurableWebTestcase`) +- [ ] No test-only code in production classes + +### How to Check + +```bash +# Run tests +composer tests + +# Check what's tested +grep -r "function it_" tests/ | wc -l + +# Find untested classes +diff <(find src -name "*.php" | sed 's/src/tests\/Unit/' | sed 's/.php/Test.php/' | sort) \ + <(find tests/Unit -name "*Test.php" | sort) +``` + +### Red Flags + +- New class with no corresponding test file +- Test that doesn't assert anything +- Test that mocks the class it's testing +- Integration test without `ResetDatabase` trait + +### Severity: BLOCKER for missing tests on new code, WARNING for missing edge cases + +--- + +## 5. Security + +### What to Check + +- [ ] **Input validation** — All external input validated before use + - User input, query parameters, request bodies + - Configuration values from untrusted sources +- [ ] **Type safety** — No unchecked casts, `mixed` minimized +- [ ] **Exception info leaks** — Error messages don't expose internals to users +- [ ] **Injection** — No string interpolation in: + - SQL queries (use prepared statements) + - Shell commands (use proper escaping) + - Log messages with user data (use context array) +- [ ] **Serialization** — No `unserialize()` on untrusted data +- [ ] **File operations** — Path traversal checked if user input in paths + +### Severity: BLOCKER + +--- + +## 6. Performance + +### What to Check + +- [ ] **N+1 queries** — Loading related entities in loops + - e.g., checking each tag by loading element from DB individually +- [ ] **Unnecessary loops** — Could use `array_map`, `array_filter`, or collection methods +- [ ] **Missing early returns** — Check cheapest conditions first + ```php + // Good: cheap check first + if ($tags->isEmpty()) { return; } + // Then expensive operations + ``` +- [ ] **Redundant work** — Same computation repeated, missing caching +- [ ] **String building in loops** — Use `implode()` or `sprintf()` +- [ ] **Memory** — Large collections processed all at once vs. generators + +### This Project Specifically + +- Tag checking loads elements from DB — is it necessary? Could it be cached? +- Decorator chain: is each decorator doing minimal work? +- Event dispatching: expensive listeners should be lazy + +### Severity: WARNING (BLOCKER if O(n^2) or worse) + +--- + +## 7. Backward Compatibility (BC) Breaks + +### What to Check + +- [ ] **Public API changes** — Services users autowire: + - `CacheActivator` + - `CacheInvalidator` + - `ResponseTagger` + - `CacheTags`, `CacheTag` + - Events: `ElementTaggingEvent`, `ElementInvalidationEvent` +- [ ] **Removed/renamed public methods** — Breaking for anyone calling them +- [ ] **Changed method signatures** — New required parameters +- [ ] **Configuration changes** — Renamed/removed config keys under `neusta_pimcore_http_cache` +- [ ] **Service ID changes** — Renamed service IDs in `config/services.php` +- [ ] **Cache tag format changes** — Would invalidate existing caches +- [ ] **Event class changes** — Changed properties, removed methods +- [ ] **Interface changes** — Added methods to existing interfaces + +### How to Assess + +- **Internal** classes (`@internal`) — BC breaks allowed +- **Public** interfaces/classes — BC breaks require major version bump +- **Config** changes — Deprecate first, remove in next major + +### Severity: BLOCKER for public API, WARNING for internal + +--- + +## 8. Static Analysis & Code Style + +### What to Check + +- [ ] **PHPStan level 8** — Would new code pass? + - All types declared (no `mixed`) + - Generics on collections (`array`) + - Strict comparisons (`===` not `==`) + - No dead code branches +- [ ] **strict_types** — ` Date: Fri, 27 Feb 2026 11:25:27 +0100 Subject: [PATCH 5/8] Add Claude Code developer guide Documents how to use Claude Code with this project: - Rules vs skills explained - Writer/reviewer workflow - When and how to update CLAUDE.md, rules, and skills - Available skills reference - Tips for effective usage Co-Authored-By: Claude Opus 4.6 --- README.md | 1 + doc/11-claude-code.md | 197 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 doc/11-claude-code.md diff --git a/README.md b/README.md index b2e6395..2e99d7d 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ You will find the detailed documentation in the following links: * [Disabling caching behavior](doc/8-disable-caching-behavior.md) * [Error handling](doc/9-error-handling.md) * [Contribution](doc/10-contribution.md) +* [Working with Claude Code](doc/11-claude-code.md) We hope you will enjoy this bundle as much as we do. If you have any questions or suggestions, please feel free to open an issue on GitHub. This repository is maintained by [neusta](https://www.team-neusta.de/). diff --git a/doc/11-claude-code.md b/doc/11-claude-code.md new file mode 100644 index 0000000..05d5f1d --- /dev/null +++ b/doc/11-claude-code.md @@ -0,0 +1,197 @@ +## Working with Claude Code + +This project is set up for AI-assisted development with [Claude Code](https://docs.anthropic.com/en/docs/claude-code). All configuration is committed to the repo — once you have Claude Code installed, everything works automatically. + +### Getting Started + +1. Install Claude Code: https://docs.anthropic.com/en/docs/claude-code +2. Open a terminal in the project root +3. Run `claude` — it will automatically load the project context + +Claude reads `CLAUDE.md` and all files in `.claude/rules/` and `.claude/skills/` at the start of every session. No manual setup needed. + +### Project Structure + +``` +CLAUDE.md # Project overview, quick reference, conventions +.claude/ +├── rules/ # Always loaded — project conventions +│ ├── code-style.md # PHP coding standards with examples +│ ├── testing.md # PHPUnit + Prophecy conventions +│ ├── architecture.md # Decorator chains, adapters, lifecycle flows +│ ├── static-analysis.md # PHPStan level 8 requirements +│ ├── bundle-usage.md # How the bundle works for developers +│ └── roles.md # Writer vs reviewer role separation +├── skills/ # On-demand — invoke with /command +│ ├── review/ # /review — code review checklist +│ ├── php-best-practices/ # /php-best-practices — PHP 8.x audit +│ ├── php-pro/ # /php-pro — senior PHP patterns +│ ├── tdd/ # /tdd — test-driven development +│ ├── debug/ # /debug — systematic debugging +│ ├── brainstorm/ # /brainstorm — ideas to designs +│ ├── code-review/ # /code-review — handle review feedback +│ ├── web-search/ # /web-search — web search via inference.sh +│ ├── writing-skills/ # /writing-skills — create new skills +│ └── humanizer/ # /humanizer — remove AI writing patterns +└── settings.local.json # Personal settings (gitignored) +``` + +### Rules vs Skills + +**Rules** (`.claude/rules/`) are loaded automatically at the start of every session. They define how Claude should behave in this project — coding standards, testing conventions, architecture patterns. Claude follows these without being asked. + +**Skills** (`.claude/skills/`) are loaded on demand. You invoke them with a slash command (e.g., `/review`) or Claude may auto-trigger them based on context. Each skill is a focused workflow for a specific task. + +### Development Workflow + +#### 1. Writing Code (Writer Role) + +Claude's default role is code writer. It follows all rules automatically. + +``` +you: "Add a new cache type for product categories" +claude: [Reads rules, understands conventions, follows TDD] + — Writes failing test first + — Implements minimal code to pass + — Runs composer cs:fix and phpstan + — Commits when asked +``` + +For test-driven development, invoke `/tdd` to enforce the red-green-refactor cycle strictly. + +#### 2. Reviewing Code (Reviewer Role) + +After writing code, use `/review` to switch Claude into reviewer mode. The reviewer checks 8 categories: + +1. Dead code — unused classes, methods, imports +2. Architecture — final classes, immutability, patterns +3. Documentation — CHANGELOG, doc/, PHPDoc sync +4. Test coverage — every change has a test +5. Security — input validation, injection risks +6. Performance — N+1 queries, early returns +7. BC breaks — public API changes +8. Static analysis — PHPStan level 8, CS fixer + +``` +you: /review +claude: [Reviews all changes against main branch] + — Reports findings as BLOCKER / WARNING / SUGGESTION + — Lists file:line for each finding + — Does NOT fix issues (that's the writer's job) +``` + +**Important**: The reviewer reports findings — it does not fix them. After the review, switch back to writer mode to address the findings. This separation prevents blind spots. + +#### 3. Recommended Workflow + +``` +1. Start a session — Claude loads all rules automatically +2. Write code — follow TDD (or invoke /tdd for strict mode) +3. Run QA — composer cs:fix && composer phpstan && composer tests +4. Review — invoke /review to get an independent review +5. Fix findings — address blockers and warnings +6. Commit — ask Claude to commit when ready +``` + +#### 4. Debugging + +When stuck on a bug, invoke `/debug` for the systematic debugging process: + +``` +you: /debug "CacheTag throws exception for valid input" +claude: [Phase 1: Root cause investigation] + [Phase 2: Pattern analysis] + [Phase 3: Hypothesis and testing] + [Phase 4: Implementation with failing test] +``` + +### Available Skills + +| Command | When to Use | +|---------|-------------| +| `/review` | Review code changes before merging | +| `/tdd` | Enforce strict test-driven development | +| `/debug` | Systematic debugging of any issue | +| `/brainstorm` | Turn an idea into a design and spec | +| `/php-best-practices` | Audit code against 45+ PHP 8.x rules | +| `/php-pro` | Senior PHP patterns (Symfony, Laravel, async) | +| `/code-review` | Handle feedback from external reviewers | +| `/web-search` | Search the web via inference.sh | +| `/humanizer` | Remove AI writing patterns from text | +| `/writing-skills` | Create new skills for the project | + +### Keeping Things Updated + +The Claude Code configuration is part of the codebase. **Treat it like code** — update it when the project changes. + +#### When to Update CLAUDE.md + +- New dependencies added or removed +- PHP version requirements changed +- CI/QA workflows changed +- Project structure changed significantly + +#### When to Update Rules + +- **code-style.md** — coding conventions changed (new CS fixer rules, etc.) +- **testing.md** — test framework or conventions changed +- **architecture.md** — new patterns introduced, decorator chains modified +- **static-analysis.md** — PHPStan level or config changed +- **bundle-usage.md** — new features, config options, events added +- **roles.md** — review process or role definitions changed + +#### When to Update Skills + +- Review checklist needs new categories +- TDD workflow adjusted for the team +- New tools or workflows adopted + +#### How to Update + +Edit the files directly and commit them. Claude will use the updated versions in the next session. + +```bash +# Example: add a new rule +vim .claude/rules/new-convention.md +git add .claude/rules/new-convention.md +git commit -m "Add rule for new convention" +``` + +You can also ask Claude to update its own rules: + +``` +you: "We decided to use PHPStan level 9 now. Update the rules." +claude: [Updates static-analysis.md, CLAUDE.md, and any affected skills] +``` + +Or ask Claude to remember something: + +``` +you: "Remember that we always use DateTimeImmutable, never DateTime" +claude: [Saves to auto-memory for future sessions] +``` + +### Personal Settings + +`.claude/settings.local.json` is gitignored — use it for personal preferences that shouldn't affect the team (e.g., tool permissions). + +For shared team settings, use `.claude/settings.json` (committed to git). + +### Creating New Skills + +Use `/writing-skills` for guidance on creating project-specific skills. Skills live in `.claude/skills//SKILL.md` and are available to all team members once committed. + +```bash +mkdir -p .claude/skills/my-skill +# Create .claude/skills/my-skill/SKILL.md with frontmatter + instructions +git add .claude/skills/my-skill/ +git commit -m "Add my-skill" +``` + +### Tips + +- **Be specific** — "Fix the bug in CacheTag::fromString when tag contains spaces" works better than "fix the bug" +- **Use TDD** — invoke `/tdd` for new features and bug fixes +- **Review before merging** — invoke `/review` to catch issues you might miss +- **Keep rules current** — if you notice Claude doing something wrong, update the rules so it doesn't happen again +- **One thing at a time** — don't ask Claude to implement a feature AND review it in the same session From 908359cd7d8b7b659607707ac3d4d1583fb3201b Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Fri, 27 Feb 2026 11:29:56 +0100 Subject: [PATCH 6/8] Add Context7 skill for live library documentation lookup Queries Context7 API for current docs instead of relying on training data. No API key needed. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/context7/SKILL.md | 98 ++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .claude/skills/context7/SKILL.md diff --git a/.claude/skills/context7/SKILL.md b/.claude/skills/context7/SKILL.md new file mode 100644 index 0000000..21908c9 --- /dev/null +++ b/.claude/skills/context7/SKILL.md @@ -0,0 +1,98 @@ +--- +name: context7 +description: Use when you need current documentation for any software library, framework, or API. Queries the Context7 API for up-to-date docs instead of relying on potentially outdated training data. +allowed-tools: Bash(curl *) +argument-hint: "[library-name] [topic]" +--- + +# Context7 + +Retrieve current documentation for software libraries by querying the Context7 API via curl. Use instead of relying on potentially outdated training data. + +## Workflow + +### Step 1: Search for the Library + +Find the Context7 library ID: + +```bash +curl -s "https://context7.com/api/v2/libs/search?libraryName=LIBRARY_NAME&query=TOPIC" | jq '.results[0]' +``` + +**Parameters:** +- `libraryName` (required): Library name (e.g., "react", "nextjs", "fastapi", "symfony") +- `query` (required): Topic description for relevance ranking + +**Response fields:** +- `id`: Library identifier for the context endpoint (e.g., `/websites/react_dev_reference`) +- `title`: Human-readable library name +- `description`: Brief description +- `totalSnippets`: Number of documentation snippets available + +### Step 2: Fetch Documentation + +Use the library ID from step 1: + +```bash +curl -s "https://context7.com/api/v2/context?libraryId=LIBRARY_ID&query=TOPIC&type=txt" +``` + +**Parameters:** +- `libraryId` (required): Library ID from search results +- `query` (required): Specific topic to retrieve +- `type` (optional): `json` (default) or `txt` (plain text, more readable) + +## Examples + +### Symfony dependency injection + +```bash +# Find Symfony library ID +curl -s "https://context7.com/api/v2/libs/search?libraryName=symfony&query=dependency+injection" | jq '.results[0].id' + +# Fetch DI documentation +curl -s "https://context7.com/api/v2/context?libraryId=/symfony/symfony&query=dependency+injection&type=txt" +``` + +### PHPUnit assertions + +```bash +# Find PHPUnit library ID +curl -s "https://context7.com/api/v2/libs/search?libraryName=phpunit&query=assertions" | jq '.results[0].id' + +# Fetch assertions documentation +curl -s "https://context7.com/api/v2/context?libraryId=/sebastianbergmann/phpunit&query=assertions&type=txt" +``` + +### React hooks + +```bash +# Find React library ID +curl -s "https://context7.com/api/v2/libs/search?libraryName=react&query=hooks" | jq '.results[0].id' + +# Fetch useState documentation +curl -s "https://context7.com/api/v2/context?libraryId=/websites/react_dev_reference&query=useState&type=txt" +``` + +### Next.js routing + +```bash +curl -s "https://context7.com/api/v2/libs/search?libraryName=nextjs&query=routing" | jq '.results[0].id' +curl -s "https://context7.com/api/v2/context?libraryId=/vercel/next.js&query=app+router&type=txt" +``` + +### FastAPI dependency injection + +```bash +curl -s "https://context7.com/api/v2/libs/search?libraryName=fastapi&query=dependencies" | jq '.results[0].id' +curl -s "https://context7.com/api/v2/context?libraryId=/fastapi/fastapi&query=dependency+injection&type=txt" +``` + +## Tips + +- Use `type=txt` for more readable output +- Use `jq` to filter and format JSON responses +- Be specific with `query` to improve relevance +- If first result is wrong, check additional results in the array +- URL-encode spaces in query parameters (use `+` or `%20`) +- No API key required for basic usage (rate-limited) From 7bf7f38c5399710655c187a8c4223dddff96b0da Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Fri, 27 Feb 2026 11:34:47 +0100 Subject: [PATCH 7/8] Fix review findings: documentation accuracy and completeness Warnings fixed: - Add /context7 to developer guide skills table and structure - Fix decorator chain in CLAUDE.md to show execution order - Remove misleading "(maximum)" from PHPStan level 8 Suggestions fixed: - Use named argument in CacheTags::fromStrings() example - Clarify /review vs /code-review in skills table - Document \assert() usage in testing.md - Add project context note to php-pro PHPStan reference Co-Authored-By: Claude Opus 4.6 --- .claude/rules/bundle-usage.md | 2 +- .claude/rules/static-analysis.md | 2 +- .claude/rules/testing.md | 4 ++++ .claude/skills/php-pro/references/testing-quality.md | 8 +++++--- CLAUDE.md | 2 +- doc/11-claude-code.md | 8 +++++--- 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.claude/rules/bundle-usage.md b/.claude/rules/bundle-usage.md index 5f1a7bd..a8c16a6 100644 --- a/.claude/rules/bundle-usage.md +++ b/.claude/rules/bundle-usage.md @@ -104,7 +104,7 @@ For grouping tags beyond the built-in element types: 3. Invalidate by type: ```php $cacheInvalidator->invalidate( - CacheTags::fromStrings(['42'], new CustomCacheType('product_category')) + CacheTags::fromStrings(['42'], type: new CustomCacheType('product_category')) ); ``` diff --git a/.claude/rules/static-analysis.md b/.claude/rules/static-analysis.md index 7b2b5dd..24bc9e6 100644 --- a/.claude/rules/static-analysis.md +++ b/.claude/rules/static-analysis.md @@ -1,6 +1,6 @@ # Static Analysis -- PHPStan level 8 (maximum) — all code in `src/` must pass +- PHPStan level 8 — all code in `src/` must pass - Run with `composer phpstan` - PHPStan extensions in use: phpstan-phpunit, phpstan-symfony, phpstan-prophecy - Do not lower the PHPStan level or add ignoreErrors for new code — fix the issues instead diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md index c280d3d..f8e2d82 100644 --- a/.claude/rules/testing.md +++ b/.claude/rules/testing.md @@ -44,6 +44,10 @@ - Use `self::assertTrue()`, `self::assertSame()`, etc. (static calls) for state verification - Use Prophecy expectations for interaction verification +- Use `\assert()` for runtime type narrowing (not testing — this is production code): + ```php + \assert($taggingEvent instanceof ElementTaggingEvent); + ``` ## Data Providers diff --git a/.claude/skills/php-pro/references/testing-quality.md b/.claude/skills/php-pro/references/testing-quality.md index e8ef81b..1b4fa6f 100644 --- a/.claude/skills/php-pro/references/testing-quality.md +++ b/.claude/skills/php-pro/references/testing-quality.md @@ -87,7 +87,7 @@ describe('OrderService', function (): void { ```neon # phpstan.neon parameters: - level: 9 + level: 9 # Maximum level; this project uses level 8 paths: - src treatPhpDocTypesAsCertain: false @@ -96,15 +96,17 @@ parameters: checkMissingIterableValueType: true ``` +Note: This project uses PHPStan level 8 and PHPUnit (not Pest). The examples below are general references for levels 8-9. + ### Common PHPStan Fixes ```php -// Level 9: generics required +// Level 8+: generics required /** @var array */ // not just array /** @return list */ // not just array /** @param Collection */ // not just Collection -// Level 9: strict comparisons +// Level 8+: strict comparisons if ($value === null) { } // not == if (\in_array($item, $list, true)) // strict flag required diff --git a/CLAUDE.md b/CLAUDE.md index 342123a..944cfb2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,7 +71,7 @@ See `.claude/rules/code-style.md` for full conventions with examples. ## Architecture & Patterns -- **Decorator pattern**: Services are heavily decorated (e.g., `ResponseTagger` chain: base → `RemoveDisabledTagsResponseTagger` → `OnlyWhenActiveResponseTagger` → `CacheTagCollectionResponseTagger`) +- **Decorator pattern**: Services are heavily decorated (e.g., `ResponseTagger` chain by execution order: `CacheTagCollectionResponseTagger` → `OnlyWhenActiveResponseTagger` → `RemoveDisabledTagsResponseTagger` → `ResponseTaggerAdapter`) - **Immutable value objects**: `CacheTags` and `CacheTag` — use `with()` / static factories, never mutate - **Event-driven**: Listens to Pimcore lifecycle events; dispatches custom events for extensibility - **Single-method interfaces**: `CacheInvalidator::invalidate(CacheTags)` and `ResponseTagger::tag(CacheTags)` diff --git a/doc/11-claude-code.md b/doc/11-claude-code.md index 05d5f1d..dd50255 100644 --- a/doc/11-claude-code.md +++ b/doc/11-claude-code.md @@ -32,7 +32,8 @@ CLAUDE.md # Project overview, quick reference, conventi │ ├── code-review/ # /code-review — handle review feedback │ ├── web-search/ # /web-search — web search via inference.sh │ ├── writing-skills/ # /writing-skills — create new skills -│ └── humanizer/ # /humanizer — remove AI writing patterns +│ ├── humanizer/ # /humanizer — remove AI writing patterns +│ └── context7/ # /context7 — live library docs lookup └── settings.local.json # Personal settings (gitignored) ``` @@ -109,13 +110,14 @@ claude: [Phase 1: Root cause investigation] | Command | When to Use | |---------|-------------| -| `/review` | Review code changes before merging | +| `/review` | Review code changes before merging (you are the reviewer) | +| `/code-review` | Handle feedback received from external reviewers (you are the author) | | `/tdd` | Enforce strict test-driven development | | `/debug` | Systematic debugging of any issue | | `/brainstorm` | Turn an idea into a design and spec | | `/php-best-practices` | Audit code against 45+ PHP 8.x rules | | `/php-pro` | Senior PHP patterns (Symfony, Laravel, async) | -| `/code-review` | Handle feedback from external reviewers | +| `/context7` | Look up current library documentation via Context7 API | | `/web-search` | Search the web via inference.sh | | `/humanizer` | Remove AI writing patterns from text | | `/writing-skills` | Create new skills for the project | From 0b1363388f100e7d366fc85a3c3ad7587c78eac4 Mon Sep 17 00:00:00 2001 From: Jan Adams Date: Sun, 1 Mar 2026 09:53:36 +0100 Subject: [PATCH 8/8] Fix review findings: style, naming, and EventType test coverage - Fix blank line after --- src/Element/EventType.php | 3 +- .../TraceableResponseTaggerTest.php | 2 +- .../Element/InvalidateElementListenerTest.php | 37 +++++++++++++++++-- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/Element/EventType.php b/src/Element/EventType.php index b65285e..9d89f6c 100644 --- a/src/Element/EventType.php +++ b/src/Element/EventType.php @@ -1,5 +1,4 @@ - */ private ObjectProphecy $innerTagger; diff --git a/tests/Unit/Element/InvalidateElementListenerTest.php b/tests/Unit/Element/InvalidateElementListenerTest.php index 21d541b..7b3a1b0 100644 --- a/tests/Unit/Element/InvalidateElementListenerTest.php +++ b/tests/Unit/Element/InvalidateElementListenerTest.php @@ -6,6 +6,7 @@ use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTag; use Neusta\Pimcore\HttpCacheBundle\Cache\CacheTags; use Neusta\Pimcore\HttpCacheBundle\Element\ElementInvalidationEvent; +use Neusta\Pimcore\HttpCacheBundle\Element\EventType; use Neusta\Pimcore\HttpCacheBundle\Element\InvalidateElementListener; use PHPUnit\Framework\TestCase; use Pimcore\Event\Model\AssetEvent; @@ -113,7 +114,7 @@ public function onUpdate_should_invalidate_elements(ElementEventInterface $event public function onUpdate_does_not_invalidate_when_event_was_canceled(ElementEventInterface $event): void { $element = $event->getElement(); - $invalidationEvent = ElementInvalidationEvent::fromElement($element); + $invalidationEvent = ElementInvalidationEvent::fromElement($element, EventType::Update); $invalidationEvent->cancel = true; $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) @@ -135,7 +136,7 @@ public function onUpdate_should_invalidate_additional_tags_when_requested(Elemen $element = $event->getElement(); $additionalTag = CacheTag::fromString('tag1'); $additionalTags = CacheTags::fromStrings(['tag2', 'tag3']); - $invalidationEvent = ElementInvalidationEvent::fromElement($element); + $invalidationEvent = ElementInvalidationEvent::fromElement($element, EventType::Update); $invalidationEvent->addTag($additionalTag); $invalidationEvent->addTags($additionalTags); $expected = CacheTags::fromElement($element)->with($additionalTag, $additionalTags); @@ -185,7 +186,7 @@ public function onDelete_should_invalidate_elements(ElementEventInterface $event public function onDelete_does_not_invalidate_when_event_was_canceled(ElementEventInterface $event): void { $element = $event->getElement(); - $invalidationEvent = ElementInvalidationEvent::fromElement($element); + $invalidationEvent = ElementInvalidationEvent::fromElement($element, EventType::Delete); $invalidationEvent->cancel = true; $this->eventDispatcher->dispatch(Argument::type(ElementInvalidationEvent::class)) @@ -207,7 +208,7 @@ public function onDelete_should_invalidate_additional_tags_when_requested(Elemen $element = $event->getElement(); $additionalTag = CacheTag::fromString('tag1'); $additionalTags = CacheTags::fromStrings(['tag2', 'tag3']); - $invalidationEvent = ElementInvalidationEvent::fromElement($element); + $invalidationEvent = ElementInvalidationEvent::fromElement($element, EventType::Delete); $invalidationEvent->addTag($additionalTag); $invalidationEvent->addTags($additionalTags); $expected = CacheTags::fromElement($element)->with($additionalTag, $additionalTags); @@ -221,6 +222,34 @@ public function onDelete_should_invalidate_additional_tags_when_requested(Elemen ->shouldHaveBeenCalledOnce(); } + /** + * @test + * + * @dataProvider elementProvider + */ + public function onUpdate_should_dispatch_event_with_update_type(ElementEventInterface $event): void + { + $this->invalidateElementListener->onUpdate($event); + + $this->eventDispatcher->dispatch(Argument::that( + static fn (ElementInvalidationEvent $e) => EventType::Update === $e->type, + ))->shouldHaveBeenCalledOnce(); + } + + /** + * @test + * + * @dataProvider elementProvider + */ + public function onDelete_should_dispatch_event_with_delete_type(ElementEventInterface $event): void + { + $this->invalidateElementListener->onDelete($event); + + $this->eventDispatcher->dispatch(Argument::that( + static fn (ElementInvalidationEvent $e) => EventType::Delete === $e->type, + ))->shouldHaveBeenCalledOnce(); + } + public function elementProvider(): iterable { $asset = $this->prophesize(Asset::class);