Skip to content

Rebuild cache observability on Koriym.SemanticLogger#178

Open
koriym wants to merge 5 commits into
bearsunday:1.xfrom
koriym:cache-observability
Open

Rebuild cache observability on Koriym.SemanticLogger#178
koriym wants to merge 5 commits into
bearsunday:1.xfrom
koriym:cache-observability

Conversation

@koriym

@koriym koriym commented Jun 2, 2026

Copy link
Copy Markdown
Member

Summary

Rebuilds cache observability on Koriym.SemanticLogger: the cache create/invalidate log becomes an open/event/close tree whose nesting is the embed/dependency structure (a parent's embedded children nest under it). The legacy flat RepositoryLogger is deprecated but stays bound for BC and receives no internal events.

Included

  • SemanticLogger-based logging at the five AOP boundaries: CacheInterceptor / AbstractDonutCacheInterceptor (GET) and CommandInterceptor / RefreshInterceptor / DonutCommandInterceptor (command).
  • SafeSemanticLogger best-effort decorator (a logging failure can never break a cache read/write), NullSemanticLogger zero-cost no-op default, bound via SafeSemanticLoggerProvider.
  • Typed AbstractContext subclasses in src/Log/Context/ with per-context JSON Schemas in docs/schemas/context/.
  • invalidate context records self-describing status words: roPool/etagPool (invalidated|failed), cdn (purged|failed), plus durationMs. The CDN purge is best-effort and no longer fails local invalidation on outage.
  • Manual (direct, non-AOP) top-level put() and invalidateTags() are rooted in manual_store / manual_invalidate scopes so their events are not dropped at flush, mirroring the existing manual_purge treatment.
  • Adds runtime dependency koriym/semantic-logger ^0.8.0; vendor/bin/stree renders the cache log as a readable tree (demo/run-dependency.php, demo/run-donut.php).

Not included (deliberate)

Verification

  • phpcs (PSR-12) clean; PHPStan (max) and Psalm (level 1) report no errors.
  • phpunit: 141 tests, 270 assertions, OK.
  • demo/run-dependency.php and demo/run-donut.php render the SemanticLogger tree (exit 0).

Commits

  1. Rebuild cache observability on Koriym.SemanticLogger
  2. Log manual (top-level) put() and invalidateTags() in dedicated scopes

koriym added 2 commits June 1, 2026 13:38
Migrate the cache create/invalidate log to Koriym.SemanticLogger's open/event/close tree, where the nesting mirrors the embed/dependency structure. Typed Context classes (src/Log/Context/) carry per-context JSON Schemas (docs/schemas/context/), and SafeSemanticLogger guarantees logging never breaks cache reads/writes (NullSemanticLogger is the no-op default).

Adds koriym/semantic-logger ^0.8.0; vendor/bin/stree renders the cache log as a tree (demo/run-dependency.php, demo/run-donut.php). The legacy RepositoryLogger interface stays bound for BC but receives no internal events.

The multi-embed dependency fix and diamond tests are intentionally excluded here; they already shipped in 1.16.1 (bearsunday#177).
Direct, non-AOP cache operations had no enclosing log scope, so their save/invalidate events were dropped at flush (an event-only session renders empty). A top-level put() now opens a manual_store scope (closed with manual_store_result) and a top-level invalidateTags() opens a manual_invalidate scope (closed with the existing InvalidateContext), mirroring the existing manual_purge treatment. Nested (AOP) calls are unchanged, so the in-flow tree shape is identical.

Adds the three Context classes and their JSON Schemas, and tests that flush-validate the new top-level trees.
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c9ddf40d-da83-4d34-a665-3cfae06a7df9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR migrates BEAR.QueryRepository from flat string-based repository logging to structured Koriym.SemanticLogger with 25 typed context classes. Cache components now emit observable scopes for dependencies, commands, and manual operations, validated against JSON schemas, while a failure-tolerant wrapper ensures logging failures do not disrupt cache behavior.

Changes

Semantic Logger Migration for Cache Observability

Layer / File(s) Summary
Schema and typed context contracts
docs/schemas/context/*.json, src/Log/Context/*.php
25 JSON Schemas and corresponding PHP context classes define contracts for cache hits/misses, invalidation outcomes, manual operations, command execution, and dependency resolution. Each context extends AbstractContext and declares TYPE and SCHEMA_URL constants.
Semantic logger implementations and adapter layer
src/Log/NullSemanticLogger.php, src/Log/SafeSemanticLogger.php, src/Log/SafeSemanticLoggerProvider.php, src/NullRepositoryLogger.php, src/RepositoryLoggerInterface.php, src/StructuredRepositoryLoggerInterface.php, src/RepositoryLogger.php
No-op and safe-mode logger wrappers; SafeSemanticLogger prevents delegate failures from disrupting cache; provider for DI binding; legacy RepositoryLoggerInterface marked deprecated; new StructuredRepositoryLoggerInterface exposes getLogs() and getOps() accessors for backward-compatible structured logging.
Cache interceptor instrumentation with hit/miss tracking
src/CacheInterceptor.php, src/AbstractDonutCacheInterceptor.php, src/DonutRepository.php
CacheInterceptor and AbstractDonutCacheInterceptor open a GET scope on invoke, track $hit flag when cached state succeeds, and close the scope with CacheHitContext or CacheMissContext. DonutRepository emits PutDonutContext on cache writes and CacheHitContext/CacheMissContext/RefreshDonutContext on donut refresh.
Dependency resolution and donut cache instrumentation
src/CacheDependency.php
CacheDependency emits DependsOnContext after accumulating surrogate child tags, capturing parent URI, child URI, and computed child tags array.
Top-level manual scope management for cache writes and invalidations
src/QueryRepository.php, src/ResourceStorage.php
QueryRepository wraps top-level put() and purge() in ManualStoreContext/ManualPurgeContext scopes; ResourceStorage wraps top-level invalidateTags() in ManualInvalidateContext and treats CDN purger as best-effort, recording outcome in InvalidateContext.
Command context factory and command interceptor scopes
src/CommandContextFactory.php, src/CommandInterceptor.php, src/DonutCommandInterceptor.php, src/RefreshInterceptor.php
CommandContextFactory extracts method name and command annotations; CommandInterceptor, DonutCommandInterceptor, and RefreshInterceptor open a command scope on invoke and close it with CommandResultContext derived from resource code.
Dependency injection wiring for semantic logger
src/DonutCacheModule.php
SemanticLoggerInterface bound to SafeSemanticLoggerProvider as singleton; legacy RepositoryLoggerInterface binding retained for backward compatibility but receives no internal cache events.
Test helpers and fixtures for semantic logging validation
tests/RecordingSemanticLogger.php, tests/ThrowingSemanticLogger.php, tests/FakeThrowingPurger.php, tests/SemanticLogTreeTrait.php, tests/SafeSemanticLoggerTest.php, tests/SemanticLogSchemaTest.php, tests/GracefulLoggingTest.php
RecordingSemanticLogger and ThrowingSemanticLogger test doubles; FakeThrowingPurger simulates CDN failures; SemanticLogTreeTrait provides schema validation and tree traversal helpers; SafeSemanticLoggerTest validates failure recovery; SemanticLogSchemaTest exercises dependency nesting and command causality; GracefulLoggingTest confirms cache works when logger throws.
Test updates to use semantic logger and validation
tests/DonutCacheInterceptorTest.php, tests/DonutCommandInterceptorTest.php, tests/DonutQueryInterceptorTest.php, tests/DonutQueryInterceptorPurgeTest.php, tests/DonutCommandRedisCacheTest.php, tests/QueryRepositoryTest.php, tests/ResourceRepositoryTest.php, tests/ResourceStorageTest.php, tests/RepositoryLoggerTest.php
All tests switch from RepositoryLoggerInterface to SemanticLoggerInterface; tearDown calls flushAndValidate for schema drift detection; assertions use log-tree operation types instead of JSON strings; ResourceStorageTest adds invalidateTags outcome tests.
Documentation, changelog, and demo updates
CHANGELOG.md, composer.json, tests/CACHE_DEPENDENCY_TESTS.md, demo/run-dependency.php, demo/run-donut.php
CHANGELOG documents migration and new dependency koriym/semantic-logger; CACHE_DEPENDENCY_TESTS.md expanded with observability section and schema validation details; demo scripts updated to render semantic log tree via TreeRenderer.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • bearsunday/BEAR.QueryRepository#170: Both PRs are tightly connected to the logger API/format migration: #170 changes RepositoryLoggerInterface/RepositoryLogger and updates cache call sites to emit structured JSON (log(op, context)), while the main PR continues that same logging evolution by deprecating the legacy repository logger and refactoring those same cache components (ResourceStorage, QueryRepository, DonutRepository, interceptors) to use SemanticLoggerInterface with typed context classes/scopes.

Suggested reviewers

  • NaokiTsuchiya
  • jingu
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@koriym

koriym commented Jun 2, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (a4cd9e8) to head (aa99a81).

Additional details and impacted files
@@             Coverage Diff              @@
##                 1.x      #178    +/-   ##
============================================
  Coverage     100.00%   100.00%            
- Complexity       247       314    +67     
============================================
  Files             53        77    +24     
  Lines            749       902   +153     
============================================
+ Hits             749       902   +153     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/StructuredRepositoryLoggerInterface.php (1)

7-17: 💤 Low value

Merge the two stacked docblocks into one.

Only the docblock immediately preceding the interface declaration (the @deprecated block) is attached by doc/IDE tooling; the first block describing the BC rationale is effectively dropped. Consider combining them.

♻️ Proposed merge
 /**
  * A repository logger that also exposes its entries as structured data
  *
  * Separated from RepositoryLoggerInterface so that adding structured accessors
  * does not break third-party RepositoryLoggerInterface implementations (BC).
  * Use this for structural assertions instead of substring-matching __toString().
- */
-/**
+ *
  * `@deprecated` Since the SemanticLogger migration; structured logs are now the
  *   {`@see` \Koriym\SemanticLogger\LogJson} tree returned by SemanticLogger::flush().
  */
 interface StructuredRepositoryLoggerInterface extends RepositoryLoggerInterface
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/StructuredRepositoryLoggerInterface.php` around lines 7 - 17, Combine the
two separate docblocks into a single docblock immediately above the
StructuredRepositoryLoggerInterface declaration: include the descriptive
paragraph about it being a repository logger exposing entries as structured data
and the BC rationale mentioning separation from RepositoryLoggerInterface, then
append the `@deprecated` tag with its message referencing SemanticLogger, LogJson,
and SemanticLogger::flush(); ensure the merged docblock replaces both existing
blocks so IDE/doc tooling sees the full description and deprecation together for
StructuredRepositoryLoggerInterface.
tests/SemanticLogTreeTrait.php (1)

46-58: 💤 Low value

Optional: unlink the temp file after validation.

tempnam() creates a file that is never removed, so each scenario leaves a slog* file in the system temp dir across the suite. Harmless but accumulates.

♻️ Suggested cleanup
         ob_start();
         try {
             (new SemanticLogValidator())->validate($file, $schemaDir);
         } finally {
             ob_get_clean();
+            `@unlink`($file);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/SemanticLogTreeTrait.php` around lines 46 - 58, The temp file created
via tempnam() and stored in $file is never removed; after calling (new
SemanticLogValidator())->validate($file, $schemaDir) ensure the temporary file
is unlinked—add an unlink($file) in the finally block (after ob_get_clean()) so
the temp slog* file is always deleted even on validation failure; reference the
$file variable and the validate call to locate where to add the cleanup.
src/QueryRepository.php (1)

49-60: ⚖️ Poor tradeoff

Manual-scope logging: isTopLevel() exists, and DI guarantees SafeSemanticLogger

  • put()/purge() only open manual scopes when the bound logger is SafeSemanticLogger and isTopLevel() is true; SafeSemanticLogger does define isTopLevel(): bool.
  • DonutCacheModule binds SemanticLoggerInterface as a singleton via SafeSemanticLoggerProvider (always returning SafeSemanticLogger), and the same pattern is used in ResourceStorage::invalidateTags(), so the top-level manual-scope path is satisfied for the actual runtime binding.
  • If replacing SemanticLoggerInterface with other implementations is a goal, introduce a narrower interface for the “top-level + manual scope” capability (instead of relying on instanceof SafeSemanticLogger).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/QueryRepository.php` around lines 49 - 60, The code uses instanceof
SafeSemanticLogger and isTopLevel() inside put()/purge() to decide whether to
open manual logging scopes; replace this instanceof-based runtime check with a
clearer contract by introducing a small interface (e.g.,
TopLevelManualScopeLogger with methods isTopLevel(): bool,
open(ManualStoreContext): mixed, close(ManualStoreResultContext, mixed): void),
have SafeSemanticLogger implement it, update the method signatures/DI usage to
type-hint/accept TopLevelManualScopeLogger where manual scopes are needed (or
use a safe cast via interface check), and update callers like
QueryRepository::put()/purge() and ResourceStorage::invalidateTags() to use that
interface instead of instanceof SafeSemanticLogger so other logger
implementations can opt in without relying on instanceof or concrete class
bindings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@psalm.xml`:
- Around line 23-29: psalm.xml currently suppresses the InvalidClassConstantType
rule for the entire src/Log/Context directory; instead, remove that
directory-wide suppression and add targeted suppressions for only the concrete
Context classes that override Koriym\SemanticLogger\AbstractContext's untyped
constants (specifically the classes that declare public const TYPE and public
const SCHEMA_URL). Locate each concrete *Context class that defines those
constants and add a file- or class-scoped Psalm suppression for
InvalidClassConstantType (or an inline `@psalm-suppress` on the class) so other
files in the directory remain checked normally.

In `@src/Log/NullSemanticLogger.php`:
- Around line 23-37: The no-op methods open, event, and close in
NullSemanticLogger are tripping PHPMD for unused parameters; fix by explicitly
consuming the parameters to silence the linter — add a void-cast or unset for
the parameters (e.g., (void)$context; and for close also (void)$openId;) at the
start of each method body so the methods remain no-ops but PHPMD no longer flags
the parameters as unused.

In `@src/Log/SafeSemanticLogger.php`:
- Around line 133-138: The __unserialize method in SafeSemanticLogger currently
ignores its $data parameter, triggering PHPMD UnusedFormalParameter; update
SafeSemanticLogger::__unserialize(array $data): void to consume $data (for
example with unset($data) or (void)$data;) before initializing $this->logger and
$this->broken, so the parameter is referenced and the linter warning is
suppressed while preserving existing behavior of new SemanticLogger() and
$this->broken = false.

---

Nitpick comments:
In `@src/QueryRepository.php`:
- Around line 49-60: The code uses instanceof SafeSemanticLogger and
isTopLevel() inside put()/purge() to decide whether to open manual logging
scopes; replace this instanceof-based runtime check with a clearer contract by
introducing a small interface (e.g., TopLevelManualScopeLogger with methods
isTopLevel(): bool, open(ManualStoreContext): mixed,
close(ManualStoreResultContext, mixed): void), have SafeSemanticLogger implement
it, update the method signatures/DI usage to type-hint/accept
TopLevelManualScopeLogger where manual scopes are needed (or use a safe cast via
interface check), and update callers like QueryRepository::put()/purge() and
ResourceStorage::invalidateTags() to use that interface instead of instanceof
SafeSemanticLogger so other logger implementations can opt in without relying on
instanceof or concrete class bindings.

In `@src/StructuredRepositoryLoggerInterface.php`:
- Around line 7-17: Combine the two separate docblocks into a single docblock
immediately above the StructuredRepositoryLoggerInterface declaration: include
the descriptive paragraph about it being a repository logger exposing entries as
structured data and the BC rationale mentioning separation from
RepositoryLoggerInterface, then append the `@deprecated` tag with its message
referencing SemanticLogger, LogJson, and SemanticLogger::flush(); ensure the
merged docblock replaces both existing blocks so IDE/doc tooling sees the full
description and deprecation together for StructuredRepositoryLoggerInterface.

In `@tests/SemanticLogTreeTrait.php`:
- Around line 46-58: The temp file created via tempnam() and stored in $file is
never removed; after calling (new SemanticLogValidator())->validate($file,
$schemaDir) ensure the temporary file is unlinked—add an unlink($file) in the
finally block (after ob_get_clean()) so the temp slog* file is always deleted
even on validation failure; reference the $file variable and the validate call
to locate where to add the cleanup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 431b3a67-ada3-42a2-bbed-724f5a8f5882

📥 Commits

Reviewing files that changed from the base of the PR and between a4cd9e8 and c8744bf.

📒 Files selected for processing (81)
  • CHANGELOG.md
  • composer.json
  • demo/run-dependency.php
  • demo/run-donut.php
  • docs/schemas/context/cache_hit.json
  • docs/schemas/context/cache_miss.json
  • docs/schemas/context/command.json
  • docs/schemas/context/command_result.json
  • docs/schemas/context/depends_on.json
  • docs/schemas/context/get.json
  • docs/schemas/context/invalidate.json
  • docs/schemas/context/manual_invalidate.json
  • docs/schemas/context/manual_purge.json
  • docs/schemas/context/manual_purge_result.json
  • docs/schemas/context/manual_store.json
  • docs/schemas/context/manual_store_result.json
  • docs/schemas/context/purge.json
  • docs/schemas/context/put_donut.json
  • docs/schemas/context/refresh_donut.json
  • docs/schemas/context/save_donut.json
  • docs/schemas/context/save_donut_view.json
  • docs/schemas/context/save_etag.json
  • docs/schemas/context/save_value.json
  • docs/schemas/context/save_view.json
  • docs/schemas/repository-log.json
  • psalm.xml
  • src/AbstractDonutCacheInterceptor.php
  • src/CacheDependency.php
  • src/CacheInterceptor.php
  • src/CommandContextFactory.php
  • src/CommandInterceptor.php
  • src/DonutCacheModule.php
  • src/DonutCommandInterceptor.php
  • src/DonutRepository.php
  • src/Log/Context/CacheHitContext.php
  • src/Log/Context/CacheMissContext.php
  • src/Log/Context/CommandContext.php
  • src/Log/Context/CommandResultContext.php
  • src/Log/Context/DependsOnContext.php
  • src/Log/Context/GetContext.php
  • src/Log/Context/InvalidateContext.php
  • src/Log/Context/ManualInvalidateContext.php
  • src/Log/Context/ManualPurgeContext.php
  • src/Log/Context/ManualPurgeResultContext.php
  • src/Log/Context/ManualStoreContext.php
  • src/Log/Context/ManualStoreResultContext.php
  • src/Log/Context/PurgeContext.php
  • src/Log/Context/PutDonutContext.php
  • src/Log/Context/RefreshDonutContext.php
  • src/Log/Context/SaveDonutContext.php
  • src/Log/Context/SaveDonutViewContext.php
  • src/Log/Context/SaveEtagContext.php
  • src/Log/Context/SaveValueContext.php
  • src/Log/Context/SaveViewContext.php
  • src/Log/NullSemanticLogger.php
  • src/Log/SafeSemanticLogger.php
  • src/Log/SafeSemanticLoggerProvider.php
  • src/NullRepositoryLogger.php
  • src/QueryRepository.php
  • src/RefreshInterceptor.php
  • src/RepositoryLogger.php
  • src/RepositoryLoggerInterface.php
  • src/ResourceStorage.php
  • src/StructuredRepositoryLoggerInterface.php
  • tests/CACHE_DEPENDENCY_TESTS.md
  • tests/DonutCacheInterceptorTest.php
  • tests/DonutCommandInterceptorTest.php
  • tests/DonutCommandRedisCacheTest.php
  • tests/DonutQueryInterceptorPurgeTest.php
  • tests/DonutQueryInterceptorTest.php
  • tests/FakeThrowingPurger.php
  • tests/GracefulLoggingTest.php
  • tests/QueryRepositoryTest.php
  • tests/RecordingSemanticLogger.php
  • tests/RepositoryLoggerTest.php
  • tests/ResourceRepositoryTest.php
  • tests/ResourceStorageTest.php
  • tests/SafeSemanticLoggerTest.php
  • tests/SemanticLogSchemaTest.php
  • tests/SemanticLogTreeTrait.php
  • tests/ThrowingSemanticLogger.php
💤 Files with no reviewable changes (1)
  • docs/schemas/repository-log.json

Comment thread psalm.xml
Comment on lines +23 to +29
<!-- Koriym\SemanticLogger\AbstractContext declares TYPE/SCHEMA_URL = '' (untyped), which Psalm
infers as the literal '' type; concrete Context subclasses legitimately set string values. -->
<InvalidClassConstantType>
<errorLevel type="suppress">
<directory name="src/Log/Context" />
</errorLevel>
</InvalidClassConstantType>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Current Psalm suppression:"
rg -n -C2 'InvalidClassConstantType|src/Log/Context' psalm.xml

echo
echo "Context classes covered by this suppression:"
fd -e php . src/Log/Context | sort | while read -r file; do
  echo "== $file =="
  rg -n 'public const (TYPE|SCHEMA_URL)' "$file" || true
done

echo
echo "Existing localized suppressions, if any:"
rg -n '`@psalm-suppress` InvalidClassConstantType|psalm-suppress InvalidClassConstantType' src/Log/Context psalm.xml || true

Repository: bearsunday/BEAR.QueryRepository

Length of output: 4757


Localize the Psalm InvalidClassConstantType suppression instead of disabling it for src/Log/Context wholesale

psalm.xml currently suppresses InvalidClassConstantType for the entire src/Log/Context directory, which covers all 20 concrete *Context.php classes that declare public const TYPE / public const SCHEMA_URL. Replace the directory-wide suppression with targeted file/class-level suppressions for only the specific workaround cases tied to the Koriym\SemanticLogger\AbstractContext constant-type mismatch, so new/changed constants don’t get silently ignored.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@psalm.xml` around lines 23 - 29, psalm.xml currently suppresses the
InvalidClassConstantType rule for the entire src/Log/Context directory; instead,
remove that directory-wide suppression and add targeted suppressions for only
the concrete Context classes that override
Koriym\SemanticLogger\AbstractContext's untyped constants (specifically the
classes that declare public const TYPE and public const SCHEMA_URL). Locate each
concrete *Context class that defines those constants and add a file- or
class-scoped Psalm suppression for InvalidClassConstantType (or an inline
`@psalm-suppress` on the class) so other files in the directory remain checked
normally.

Comment on lines +23 to +37
#[Override]
public function open(AbstractContext $context): string
{
return '';
}

#[Override]
public function event(AbstractContext $context): void
{
}

#[Override]
public function close(AbstractContext $context, string $openId): void
{
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Silence PHPMD on the intentionally unused interface parameters.

These no-op methods currently fail CI because PHPMD flags the required interface parameters as unused on Lines 24, 30, and 35. Add a local suppression or explicitly consume the parameters so the null logger remains build-clean.

Proposed fix
 final class NullSemanticLogger implements SemanticLoggerInterface
 {
     private const EMPTY_SCHEMA_URL = 'https://koriym.github.io/Koriym.SemanticLogger/schemas/semantic-log.json';
 
+    /** `@phpmdSuppressWarnings`(UnusedFormalParameter) */
     #[Override]
     public function open(AbstractContext $context): string
     {
         return '';
     }
 
+    /** `@phpmdSuppressWarnings`(UnusedFormalParameter) */
     #[Override]
     public function event(AbstractContext $context): void
     {
     }
 
+    /** `@phpmdSuppressWarnings`(UnusedFormalParameter) */
     #[Override]
     public function close(AbstractContext $context, string $openId): void
     {
     }
📝 Committable suggestion

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

Suggested change
#[Override]
public function open(AbstractContext $context): string
{
return '';
}
#[Override]
public function event(AbstractContext $context): void
{
}
#[Override]
public function close(AbstractContext $context, string $openId): void
{
}
#[Override]
/** `@phpmdSuppressWarnings`(UnusedFormalParameter) */
public function open(AbstractContext $context): string
{
return '';
}
#[Override]
/** `@phpmdSuppressWarnings`(UnusedFormalParameter) */
public function event(AbstractContext $context): void
{
}
#[Override]
/** `@phpmdSuppressWarnings`(UnusedFormalParameter) */
public function close(AbstractContext $context, string $openId): void
{
}
🧰 Tools
🪛 GitHub Actions: Static Analysis / 1_sa _ PHPMD.txt

[error] 24-24: PHP Mess Detector (phpmd) rule 'UnusedFormalParameter': Avoid unused parameters such as '$context'.


[error] 30-30: PHP Mess Detector (phpmd) rule 'UnusedFormalParameter': Avoid unused parameters such as '$context'.


[error] 35-35: PHP Mess Detector (phpmd) rule 'UnusedFormalParameter': Avoid unused parameters such as '$context'.


[error] 35-35: PHP Mess Detector (phpmd) rule 'UnusedFormalParameter': Avoid unused parameters such as '$openId'.

🪛 GitHub Actions: Static Analysis / sa _ PHPMD

[error] 24-24: PHPMD (text ruleset): UnusedFormalParameter. Avoid unused parameters such as '$context'.


[error] 30-30: PHPMD (text ruleset): UnusedFormalParameter. Avoid unused parameters such as '$context'.


[error] 35-35: PHPMD (text ruleset): UnusedFormalParameter. Avoid unused parameters such as '$context'.


[error] 35-35: PHPMD (text ruleset): UnusedFormalParameter. Avoid unused parameters such as '$openId'.

🪛 PHPMD (2.15.0)

[warning] 24-24: Avoid unused parameters such as '$context'. (undefined)

(UnusedFormalParameter)


[warning] 30-30: Avoid unused parameters such as '$context'. (undefined)

(UnusedFormalParameter)


[warning] 35-35: Avoid unused parameters such as '$context'. (undefined)

(UnusedFormalParameter)


[warning] 35-35: Avoid unused parameters such as '$openId'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Log/NullSemanticLogger.php` around lines 23 - 37, The no-op methods open,
event, and close in NullSemanticLogger are tripping PHPMD for unused parameters;
fix by explicitly consuming the parameters to silence the linter — add a
void-cast or unset for the parameters (e.g., (void)$context; and for close also
(void)$openId;) at the start of each method body so the methods remain no-ops
but PHPMD no longer flags the parameters as unused.

Comment thread src/Log/SafeSemanticLogger.php Outdated
Comment on lines +133 to +138
/** @param array<string, mixed> $data */
public function __unserialize(array $data): void
{
$this->logger = new SemanticLogger();
$this->broken = false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant portion of SafeSemanticLogger.php
FILE="src/Log/SafeSemanticLogger.php"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "=== Context around __serialize/__unserialize in $FILE ==="
nl -ba "$FILE" | sed -n '1,220p' | sed -n '100,190p'

echo "=== Find all references to \$depth and \$broken in SafeSemanticLogger.php ==="
rg -n "\$depth|\$broken|__serialize|__unserialize|isTopLevel|open\s*\(" "$FILE"

Repository: bearsunday/BEAR.QueryRepository

Length of output: 220


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/Log/SafeSemanticLogger.php"
test -f "$FILE" || { echo "Missing file: $FILE" >&2; exit 1; }

echo "=== Context around __serialize/__unserialize in $FILE (lines 100-190) ==="
cat -n "$FILE" | sed -n '100,190p'

echo "=== Find references to \$depth, \$broken, and magic serialization methods ==="
rg -n "\$depth|\$broken|__serialize|__unserialize|isTopLevel|open\s*\(" "$FILE"

Repository: bearsunday/BEAR.QueryRepository

Length of output: 2004


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/Log/SafeSemanticLogger.php"
test -f "$FILE" || { echo "Missing file: $FILE" >&2; exit 1; }

echo "=== Property/methods context (lines 1-120) ==="
cat -n "$FILE" | sed -n '1,120p'

echo "=== Exact occurrences of depth usage ==="
rg -n "\$this->depth|private int \$depth|isTopLevel|function open\(" "$FILE"

echo "=== Exact occurrences of magic serialization params ==="
rg -n "__unserialize\(array \$data\)" "$FILE" && rg -n "\$data" "$FILE"

echo "=== Check composer availability ==="
command -v composer >/dev/null && composer --version || echo "composer not available"

# Try to run PHPMD/PSR checks only if composer exists; keep output bounded.
if command -v composer >/dev/null; then
  echo "=== Running composer cs (PSR-12) ==="
  composer cs 2>&1 | head -n 120 || true

  echo "=== Running composer sa (PHPStan/Psalm) ==="
  composer sa 2>&1 | head -n 160 || true
fi

Repository: bearsunday/BEAR.QueryRepository

Length of output: 5179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -la
echo "=== composer.json (top 200 lines) ==="
test -f composer.json && sed -n '1,200p' composer.json || true

echo "=== vendor/bin (if present) ==="
ls -la vendor 2>/dev/null || true
ls -la vendor/bin 2>/dev/null || true

echo "=== Search for phpmmd/phpmd/phpcs config ==="
rg -n "phpmd|phpmmd|phpcs|phpstan|psalm|php-cs-fixer" -S . || true

Repository: bearsunday/BEAR.QueryRepository

Length of output: 28025


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== phpmd.xml rules relevant sections ==="
cat -n phpmd.xml | sed -n '1,200p'

echo "=== Run phpmd on SafeSemanticLogger.php (bounded output) ==="
./vendor/bin/phpmd src/Log/SafeSemanticLogger.php text phpmd.xml | head -n 80 || true

Repository: bearsunday/BEAR.QueryRepository

Length of output: 2603


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Locate phpmd executable ==="
find vendor -type f \( -iname '*phpmd*' -o -path '*phpmd*' \) | head -n 80
echo "=== Locate vendor-bin phpmd ==="
find vendor-bin -type f \( -iname '*phpmd*' -o -path '*phpmd*' \) | head -n 80

echo "=== Search composer.json scripts for phpmd path ==="
cat composer.json | rg -n "\"phpmd\""

Repository: bearsunday/BEAR.QueryRepository

Length of output: 317


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for phpmd binaries/scripts/PHARs ==="
find . -type f \( -iname 'phpmd*' -o -path '*phpmd*' \) | head -n 200

echo "=== Search inside vendor for phpmd/phpmd package directory ==="
find vendor -type d -iname '*phpmd*' | head -n 50

echo "=== Search vendor-bin/tools for phpmd ==="
find vendor-bin -type f \( -iname '*phpmd*' -o -path '*phpmd*' \) | head -n 200

echo "=== Check whether phpmd is installed as a PHAR anywhere ==="
find vendor -type f \( -iname '*.phar' -o -iname '*phpmd*' \) | head -n 200

Repository: bearsunday/BEAR.QueryRepository

Length of output: 374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Locate phpmd package directories ==="
find vendor -maxdepth 4 -type d \( -iname 'phpmd' -o -path '*phpmd*' \) | head -n 200

echo "=== Locate phpmd entrypoint files named phpmd ==="
find vendor -type f \( -iname 'phpmd' -o -path '*bin/phpmd*' -o -path '*phpmd*bin*' \) | head -n 200

echo "=== Locate phpmd.php / launcher scripts ==="
find vendor -type f \( -iname '*phpmd*.php' -o -path '*phpmd*/*.php' \) | head -n 200

Repository: bearsunday/BEAR.QueryRepository

Length of output: 210


Consume SafeSemanticLogger::__unserialize(array $data)’s $data parameter to satisfy PHPMD UnusedFormalParameter.

src/Log/SafeSemanticLogger.php lines 133-138: $data is never used. Add a no-op consumption (e.g., unset($data);). The depth concern is not applicable here since private int $depth = 0; provides a default value.

🧰 Tools
🪛 GitHub Actions: Static Analysis / 1_sa _ PHPMD.txt

[error] 134-134: PHP Mess Detector (phpmd) rule 'UnusedFormalParameter': Avoid unused parameters such as '$data'.

🪛 GitHub Actions: Static Analysis / sa _ PHPMD

[error] 134-134: PHPMD (text ruleset): UnusedFormalParameter. Avoid unused parameters such as '$data'.

🪛 PHPMD (2.15.0)

[warning] 134-134: Avoid unused parameters such as '$data'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Log/SafeSemanticLogger.php` around lines 133 - 138, The __unserialize
method in SafeSemanticLogger currently ignores its $data parameter, triggering
PHPMD UnusedFormalParameter; update SafeSemanticLogger::__unserialize(array
$data): void to consume $data (for example with unset($data) or (void)$data;)
before initializing $this->logger and $this->broken, so the parameter is
referenced and the linter warning is suppressed while preserving existing
behavior of new SemanticLogger() and $this->broken = false.

@koriym

koriym commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai Addressed in df8c8b6:

  • PHPMD UnusedFormalParameter: added @SuppressWarnings("PHPMD.UnusedFormalParameter") on NullSemanticLogger (class-level) and SafeSemanticLogger::__unserialize. Quoted so PHPStan's phpDoc parser accepts the dotted rule name. Preferred over scattering (void) casts through each no-op body.
  • Coverage: the no-op NullSemanticLogger methods and the best-effort catch blocks in SafeSemanticLogger::event()/close() were uncovered. Added NullSemanticLoggerTest and two SafeSemanticLogger failure tests, restoring 100% on the changed code.
  • Nitpick (StructuredRepositoryLoggerInterface): merged the two stacked docblocks into one.
  • Nitpick (SemanticLogTreeTrait): unlink() the temp file in the finally.

Not changed, with reasons:

  • instanceof SafeSemanticLogger + isTopLevel(): kept for consistency with the existing purge() path, which already uses the same check. Introducing a TopLevelManualScopeLogger interface for only the new call sites would be inconsistent; a holistic refactor across all manual-scope sites can be a separate change.
  • psalm.xml directory-wide InvalidClassConstantType suppression: every Log/Context class shares the same untyped-constant override from AbstractContext, so the directory-level suppression is intentional; per-class @psalm-suppress across ~18 classes would add more noise than it removes.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@koriym Thanks for the thorough follow-up! All the addressed items look good:

  • PHPMD suppression — class-level @SuppressWarnings on NullSemanticLogger and method-level on SafeSemanticLogger::__unserialize is cleaner than scattered (void) casts.
  • Coverage — adding NullSemanticLoggerTest and the two SafeSemanticLogger failure tests properly closes the gaps on the no-op bodies and best-effort catch blocks.
  • Docblock consolidation in StructuredRepositoryLoggerInterface and the finally-guarded unlink() in SemanticLogTreeTrait are solid improvements.

The reasoning for the two items left unchanged is sound as well:

  • instanceof SafeSemanticLogger + isTopLevel(): Keeping parity with the existing purge() path is the right call. A cross-cutting TopLevelManualScopeLogger interface is a worthwhile refactor but belongs in its own change.
  • Directory-wide Psalm suppression: With ~18 context classes all inheriting the same untyped constant from AbstractContext, a single <directory>-scoped suppression is far less noisy than per-class annotations that would all repeat the same justification.

Everything looks well-reasoned. Happy with the state of the PR! 🐇✨


🧠 Learnings used
Learnt from: CR
Repo: bearsunday/BEAR.QueryRepository PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-02T15:21:24.922Z
Learning: Applies to **/*.php : Follow PSR-12 coding standards as enforced by PHP_CodeSniffer

CI surfaced two checks the unpushed branch had never run locally:

- sa/PHPMD flagged UnusedFormalParameter on no-op / contract-driven signatures.
  Add @SuppressWarnings("PHPMD.UnusedFormalParameter") (quoted so PHPStan's
  phpDoc parser accepts the dotted rule name) on NullSemanticLogger and on
  SafeSemanticLogger::__unserialize, whose parameters are required by the
  interface / magic-method contract but intentionally unused.
- codecov (100% target) flagged the no-op NullSemanticLogger methods and the
  best-effort catch blocks in SafeSemanticLogger::event()/close() as uncovered.
  NullSemanticLogger is a pure no-op null object, so its methods are marked
  @codeCoverageIgnore; the SafeSemanticLogger catch blocks are the resilience
  guarantee, so they get real failure tests instead.

Also address review nitpicks: merge the two stacked docblocks on
StructuredRepositoryLoggerInterface, and unlink the temp file in
SemanticLogTreeTrait.
@koriym koriym force-pushed the cache-observability branch from df8c8b6 to c5f2099 Compare June 3, 2026 14:51
@koriym

koriym commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai Follow-up (force-pushed c5f2099): for the codecov gap, NullSemanticLogger is a pure no-op null object, so its methods are now marked @codeCoverageIgnore (matching the existing /** @codeCoverageIgnore */ usage in the codebase) instead of a no-op test. The SafeSemanticLogger::event()/close() resilience paths keep their real failure tests.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@koriym That's the right call. @codeCoverageIgnore on a pure null-object is idiomatic PHP — it signals the skip is intentional rather than an oversight, avoids cluttering the test suite with trivial pass-through assertions, and aligns with the existing convention in the codebase. Keeping the real failure tests on SafeSemanticLogger::event()/close() where the resilience logic actually matters is the correct balance. 👍

🐇✨

Adversarial review flagged that the observability change had made CDN purge
silently best-effort: invalidateTags() swallowed a purger exception and still
reported success, so a write could leave stale CDN content unnoticed.

Restore the 1.x fail-closed behavior: the local pools are invalidated first, the
outcome is logged as cdn=failed, and the purge exception is then re-thrown to the
caller. The purger-failure test now asserts propagation instead of best-effort
masking.
@koriym koriym force-pushed the cache-observability branch from 0eac6c6 to eb337fb Compare June 4, 2026 07:11
The adversarial review noted the singleton SemanticLogger session can drop a
request's log under concurrent coroutines (interleaved open/close violates LIFO,
SafeSemanticLogger marks the session broken, the flush is empty) rather than
merely interleave it. Sharpen the Known Limitations note: cache behavior is
unaffected (logging is a best-effort side-channel), PHP-FPM is unaffected, and a
request/coroutine-scoped logger is the robust fix, intentionally deferred to the
host flush-lifecycle work.
@koriym

koriym commented Jun 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai Recording the reasoning for the design points raised in review (an out-of-band adversarial pass surfaced the same three):

  1. CDN purge — now fail-closed (eb337fb). ResourceStorage::invalidateTags() previously swallowed a purger exception and still reported success, which could silently leave stale CDN content after a write. Restored the 1.x fail-closed behavior: the local pools are invalidated, the outcome is logged as cdn: failed, then the purge exception propagates. Logging stays a side-channel and never masks the failure.

  2. The logger constructor type change is not a BC break. The constructor is not part of the public contract — neither QueryRepositoryInterface nor ResourceStorageInterface declares it, and consumers use the interfaces via DI (the module binds SemanticLoggerInterface). So no compatibility bridge is added; the RepositoryLoggerInterface binding is retained as an intentional, inert deprecation.

  3. Singleton logger under concurrent runtimes — documented + deferred to Make the SemanticLogger session request/coroutine-scoped for concurrent runtimes #179. The singleton binding is pre-existing (1.x bound the logger as a singleton too). Under Swoole/RoadRunner coroutines an interleaved open/close can drop a request's log. However: cache behavior is unaffected (logging is best-effort), the default PHP-FPM deployment is unaffected, and the robust fix (request/coroutine-scoped logger) belongs with the host flush-lifecycle. Documented in tests/CACHE_DEPENDENCY_TESTS.md and tracked in Make the SemanticLogger session request/coroutine-scoped for concurrent runtimes #179; intentionally out of scope here to keep this PR focused.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@koriym Thanks for documenting these — all three are well-reasoned:

  1. Fail-closed CDN purge: Restoring the 1.x behavior is the right call. Silently swallowing a purger exception and reporting success is a correctness hazard (stale CDN content with no signal). Logging cdn: failed as a side-channel and then propagating keeps the failure observable without masking it.

  2. BC non-issue: Correct framing. The constructor is an implementation detail; the public contract lives on QueryRepositoryInterface / ResourceStorageInterface. DI consumers bind through the module and never construct these classes directly, so the logger parameter type change is purely internal.

  3. Coroutine-scope deferral: Pragmatic. The singleton pattern is inherited from 1.x, PHP-FPM (the dominant deployment) is unaffected, and the fix belongs at the request-lifecycle layer of the host framework. Documenting it and tracking in #179 keeps the scope of this PR clean.

All three noted for future reviews. 🐇


✏️ Learnings added
Learnt from: koriym
URL: https://github.com/bearsunday/BEAR.QueryRepository/pull/178

Timestamp: 2026-06-05T06:52:10.071Z
Learning: In bearsunday/BEAR.QueryRepository, `ResourceStorage::invalidateTags()` is intentionally fail-closed for CDN purge: local pools are invalidated first, the outcome is logged as `cdn: failed` in `InvalidateContext`, and then the purger exception is allowed to propagate. This restores 1.x behavior and ensures failures are never silently swallowed.

Learnt from: koriym
URL: https://github.com/bearsunday/BEAR.QueryRepository/pull/178

Timestamp: 2026-06-05T06:52:10.071Z
Learning: In bearsunday/BEAR.QueryRepository, changing the logger constructor parameter from `RepositoryLoggerInterface` to `SemanticLoggerInterface` in `QueryRepository`, `ResourceStorage`, etc. is not a BC break. Constructors are not part of the public contract — neither `QueryRepositoryInterface` nor `ResourceStorageInterface` declares them — and all consumers obtain instances via DI using the module bindings.

Learnt from: koriym
URL: https://github.com/bearsunday/BEAR.QueryRepository/pull/178

Timestamp: 2026-06-05T06:52:10.071Z
Learning: In bearsunday/BEAR.QueryRepository, the `SemanticLoggerInterface` singleton binding under concurrent runtimes (Swoole/RoadRunner) can interleave open/close calls and drop request-scoped log entries. This is a known, pre-existing limitation (1.x also bound the logger as a singleton), cache behavior is unaffected since logging is best-effort, and the fix (request/coroutine-scoped logger) is tracked in issue `#179`. It is intentionally out of scope for PR `#178`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant