Skip to content

Conversation

@widoz
Copy link
Member

@widoz widoz commented Jan 20, 2026

Introduce ItemRegistry and ItemRegistryKey classes to manage items associated with posts. Update Repository to utilize these new classes for improved item handling and storage operations.

Summary by CodeRabbit

Release Notes

  • Refactor

    • Improved internal architecture for managing post items with enhanced error handling and state recovery for failed save operations.
  • Tests

    • Added comprehensive test coverage for new item registry functionality and edge-case scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

Introduce ItemRegistry and ItemRegistryKey classes to manage items
associated with posts. Update Repository to utilize these new classes
for improved item handling and storage operations.
Copilot AI review requested due to automatic review settings January 20, 2026 22:48
@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new in-memory ItemRegistry to manage User items grouped by post and item group, with composite key generation via ItemRegistryKey. Repository refactored to use registry-backed state management with explicit save rollback, guarded validation, and clearer separation between storage reads and registry population.

Changes

Cohort / File(s) Summary
Core Registry Classes
sources/Post/ItemRegistry.php, sources/Post/ItemRegistryKey.php
Two new classes added: ItemRegistry provides fluent interface for in-memory item storage with composite key grouping (post ID + item group); ItemRegistryKey generates sanitized keys by concatenating postId and group value, removing non-alphanumeric characters except dots. Both use static factory constructors (new()) with private instances.
Dependency Injection Configuration
sources/Post/Module.php
DI wiring added for ItemRegistryKey and ItemRegistry. Repository constructor updated to accept and inject ItemRegistry as a dependency.
Repository Refactoring
sources/Post/Repository.php
Significant internal refactoring to use registry-backed state: find() and save() now delegate to loadItems() for registry population; save() captures registry snapshot for rollback on storage write failure; new internal methods (prepareDataToStore(), serializeData(), rollbackRegistry(), loadItems(), canSave()) support registry-centric data flow; replaced direct array mutation with explicit registry set/unset operations.
Test Coverage
tests/unit/php/Post/ItemRegistryKeyTest.php, tests/integration/php/PostRepositoryTest.php
New unit tests validate key generation and sanitization with edge cases (empty post ID throws exception). Integration tests updated to construct Repository with ItemRegistry; expanded with rollback scenarios for failed storage writes and registry restore via replace() method.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Repository
    participant ItemRegistry
    participant Storage
    
    rect rgba(0, 100, 200, 0.5)
    Note over Client,Storage: Find Items Flow
    Client->>Repository: find(postId, userId, group)
    Repository->>ItemRegistry: hasGroup(postId, group)
    alt Not loaded
        Repository->>Storage: read(postId, group)
        Storage-->>Repository: raw data
        Repository->>ItemRegistry: set/all operations
    end
    ItemRegistry-->>Repository: all(postId, group)
    Repository-->>Client: items array
    end
    
    rect rgba(200, 100, 0, 0.5)
    Note over Client,Storage: Save with Rollback Flow
    Client->>Repository: save(postId, userId, item)
    Repository->>ItemRegistry: snapshot state
    Repository->>ItemRegistry: prepareDataToStore()
    Repository->>Storage: write data
    alt Write succeeds
        Storage-->>Repository: ✓
    else Write fails
        Repository->>ItemRegistry: rollbackRegistry(snapshot)
        ItemRegistry-->>Repository: ✓ state restored
    end
    Repository-->>Client: result
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Spaghetti-Dojo/konomi#23: Introduces registry snapshot-and-restore pattern (ItemRegistry::replace and rollbackRegistry) for failed save recovery, directly related to state management changes in this PR.

Poem

🐰 A registry to track each hop,
Items nested, grouped non-stop,
Keys are tidy, sanitized right,
Rollback ready if writes take flight!
~Sparkle Whiskers, Chief Code Keeper

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introduction of two new classes (ItemRegistry and ItemRegistryKey) for managing items. It is specific, concise, and directly reflects the primary objective of the pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Jan 20, 2026

Codecov Report

❌ Patch coverage is 90.90909% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
sources/Post/ItemRegistry.php 80.43% 9 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a registry pattern for managing items associated with posts, mirroring the existing User\ItemRegistry pattern. The changes add ItemRegistry and ItemRegistryKey classes to cache and manage post-item associations, refactoring the Repository class to use these new components and adding rollback functionality for storage write failures.

Changes:

  • Introduces ItemRegistry and ItemRegistryKey classes for post-based item management
  • Refactors Post\Repository to use the registry pattern with caching and rollback support
  • Updates Module.php dependency injection configuration
  • Adds integration tests for rollback scenarios

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
sources/Post/ItemRegistry.php New registry class to cache and manage items by post ID and user ID, with methods for setting, getting, and rolling back state
sources/Post/ItemRegistryKey.php New key generator for creating registry keys from post ID and item group
sources/Post/Repository.php Refactored to use ItemRegistry for caching, adds rollback mechanism when storage writes fail
sources/Post/Module.php Updated dependency injection to wire ItemRegistry and ItemRegistryKey into Repository
tests/unit/php/Post/ItemRegistryKeyTest.php Basic unit tests for key generation and validation
tests/unit/php/Post/ItemRegistryTest.php Empty test file (missing implementation)
tests/integration/php/PostRepositoryTest.php Added three rollback scenarios to verify registry state restoration on write failures

Comment on lines +10 to +25
describe('for', function (): void {
it('should generate a valid key for a post and group', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(123, ItemGroup::REACTION);
expect($result)->toBe('123.reaction');
});

it('should sanitize invalid characters', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(456, ItemGroup::REACTION);
expect($result)->toMatch('/^[a-z0-9.]+$/');
});

it('should throw exception for empty post ID', function (): void {
$key = ItemRegistryKey::new();
expect(fn () => $key->for(0, ItemGroup::REACTION))->toThrow(\UnexpectedValueException::class);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The test description is incomplete. Following the pattern in User\ItemRegistryKeyTest (line 22-29), this test should be wrapped in a describe block and have a more descriptive name like "throw error when cannot generate the key because post ID is empty" or similar, and should use the expectException pattern as demonstrated in the User tests.

Suggested change
describe('for', function (): void {
it('should generate a valid key for a post and group', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(123, ItemGroup::REACTION);
expect($result)->toBe('123.reaction');
});
it('should sanitize invalid characters', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(456, ItemGroup::REACTION);
expect($result)->toMatch('/^[a-z0-9.]+$/');
});
it('should throw exception for empty post ID', function (): void {
$key = ItemRegistryKey::new();
expect(fn () => $key->for(0, ItemGroup::REACTION))->toThrow(\UnexpectedValueException::class);
describe(ItemRegistryKey::class, function (): void {
describe('for', function (): void {
it('should generate a valid key for a post and group', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(123, ItemGroup::REACTION);
expect($result)->toBe('123.reaction');
});
it('should sanitize invalid characters', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(456, ItemGroup::REACTION);
expect($result)->toMatch('/^[a-z0-9.]+$/');
});
it('throw error when cannot generate the key because post ID is empty', function (): void {
$key = ItemRegistryKey::new();
$this->expectException(\UnexpectedValueException::class);
$key->for(0, ItemGroup::REACTION);
});

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +21
it('should sanitize invalid characters', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(456, ItemGroup::REACTION);
expect($result)->toMatch('/^[a-z0-9.]+$/');
});
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The test description says "should sanitize invalid characters" but the test only verifies the output format matches a regex pattern. It doesn't actually test that invalid characters are being sanitized. Consider adding a test case with actual invalid characters in the input (e.g., special characters in the group name or attempting to inject invalid characters) to verify sanitization works correctly.

Copilot uses AI. Check for mistakes.
yield from $this->rawDataAsserter->ensureDataStructure($storedData);
}


Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

This extra blank line is inconsistent with the coding style in the rest of the file. There should be only one blank line between method definitions.

Suggested change

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +25
describe('for', function (): void {
it('should generate a valid key for a post and group', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(123, ItemGroup::REACTION);
expect($result)->toBe('123.reaction');
});

it('should sanitize invalid characters', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(456, ItemGroup::REACTION);
expect($result)->toMatch('/^[a-z0-9.]+$/');
});

it('should throw exception for empty post ID', function (): void {
$key = ItemRegistryKey::new();
expect(fn () => $key->for(0, ItemGroup::REACTION))->toThrow(\UnexpectedValueException::class);
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The test suite is missing a top-level describe block. Following the pattern in User\ItemRegistryKeyTest, this should be wrapped with describe('ItemRegistryKey', function (): void { ... }) to properly group all tests for this class.

Suggested change
describe('for', function (): void {
it('should generate a valid key for a post and group', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(123, ItemGroup::REACTION);
expect($result)->toBe('123.reaction');
});
it('should sanitize invalid characters', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(456, ItemGroup::REACTION);
expect($result)->toMatch('/^[a-z0-9.]+$/');
});
it('should throw exception for empty post ID', function (): void {
$key = ItemRegistryKey::new();
expect(fn () => $key->for(0, ItemGroup::REACTION))->toThrow(\UnexpectedValueException::class);
describe('ItemRegistryKey', function (): void {
describe('for', function (): void {
it('should generate a valid key for a post and group', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(123, ItemGroup::REACTION);
expect($result)->toBe('123.reaction');
});
it('should sanitize invalid characters', function (): void {
$key = ItemRegistryKey::new();
$result = $key->for(456, ItemGroup::REACTION);
expect($result)->toMatch('/^[a-z0-9.]+$/');
});
it('should throw exception for empty post ID', function (): void {
$key = ItemRegistryKey::new();
expect(fn () => $key->for(0, ItemGroup::REACTION))->toThrow(\UnexpectedValueException::class);
});

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@sources/Post/ItemRegistryKey.php`:
- Around line 23-33: The current for(int $postId, User\ItemGroup $group) method
allows negative or zero IDs and then sanitizes away the minus sign with
preg_replace, causing collisions (e.g., -5 -> 5); update the method to require
$postId be a positive integer (throw the existing UnexpectedValueException if
$postId <= 0), coerce/use the integer value (e.g., (int)$postId) when building
the raw key string ($postId . '.' . $groupValue), and then run the sanitization
step — this ensures negatives are rejected and preg_replace is only applied to a
guaranteed positive integer and group value to avoid mapping different IDs to
the same key.
🧹 Nitpick comments (2)
sources/Post/ItemRegistry.php (1)

30-33: Avoid static calls to non-static keyFor.

self::keyFor(...) calls a non-static method statically, which triggers deprecation warnings in PHP 8.x and can be treated as errors in strict environments. Use $this->keyFor(...) instead.

♻️ Proposed fix
-        $key = self::keyFor($postId, $group);
+        $key = $this->keyFor($postId, $group);
...
-        $key = self::keyFor($postId, $group);
+        $key = $this->keyFor($postId, $group);
...
-        $key = self::keyFor($postId, $group);
+        $key = $this->keyFor($postId, $group);
...
-        $key = self::keyFor($postId, $item->group());
+        $key = $this->keyFor($postId, $item->group());
...
-        $key = self::keyFor($postId, $group);
+        $key = $this->keyFor($postId, $group);
...
-        $key = self::keyFor($postId, $group);
+        $key = $this->keyFor($postId, $group);

Also applies to: 36-38, 47-51, 59-62, 87-88, 98-99

tests/unit/php/Post/ItemRegistryKeyTest.php (1)

10-26: Add coverage for negative post IDs.

Once you enforce positive post IDs, add a test for negative IDs to prevent future regressions.

🧪 Suggested test addition
     it('should throw exception for empty post ID', function (): void {
         $key = ItemRegistryKey::new();
         expect(fn () => $key->for(0, ItemGroup::REACTION))->toThrow(\UnexpectedValueException::class);
     });
+
+    it('should throw exception for negative post ID', function (): void {
+        $key = ItemRegistryKey::new();
+        expect(fn () => $key->for(-1, ItemGroup::REACTION))->toThrow(\UnexpectedValueException::class);
+    });
 });

Comment on lines +23 to +33
public function for(int $postId, User\ItemGroup $group): string
{
$groupValue = $group->value;

if (!$postId) {
throw new \UnexpectedValueException(
'Item Registry Key cannot be generated with empty post ID value'
);
}

return (string) preg_replace('/[^a-z0-9.]/', '', $postId . '.' . $groupValue);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent key collisions from negative IDs and sanitization changes.

if (!$postId) allows negative IDs; preg_replace removes the -, so -5 and 5 map to the same key. That can cause cross-post data leakage and incorrect cache hits. Enforce positive IDs and ensure sanitization doesn’t alter the expected key.

🐛 Proposed fix
-        if (!$postId) {
+        if ($postId <= 0) {
             throw new \UnexpectedValueException(
-                'Item Registry Key cannot be generated with empty post ID value'
+                'Item Registry Key cannot be generated with a non-positive post ID value'
             );
         }
-
-        return (string) preg_replace('/[^a-z0-9.]/', '', $postId . '.' . $groupValue);
+        $expectedKey = $postId . '.' . $groupValue;
+        $key = (string) preg_replace('/[^a-z0-9.]/', '', $expectedKey);
+        if ($key !== $expectedKey) {
+            throw new \UnexpectedValueException(
+                'Item Registry Key cannot be generated from invalid characters'
+            );
+        }
+        return $key;
📝 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
public function for(int $postId, User\ItemGroup $group): string
{
$groupValue = $group->value;
if (!$postId) {
throw new \UnexpectedValueException(
'Item Registry Key cannot be generated with empty post ID value'
);
}
return (string) preg_replace('/[^a-z0-9.]/', '', $postId . '.' . $groupValue);
public function for(int $postId, User\ItemGroup $group): string
{
$groupValue = $group->value;
if ($postId <= 0) {
throw new \UnexpectedValueException(
'Item Registry Key cannot be generated with a non-positive post ID value'
);
}
$expectedKey = $postId . '.' . $groupValue;
$key = (string) preg_replace('/[^a-z0-9.]/', '', $expectedKey);
if ($key !== $expectedKey) {
throw new \UnexpectedValueException(
'Item Registry Key cannot be generated from invalid characters'
);
}
return $key;
}
🤖 Prompt for AI Agents
In `@sources/Post/ItemRegistryKey.php` around lines 23 - 33, The current for(int
$postId, User\ItemGroup $group) method allows negative or zero IDs and then
sanitizes away the minus sign with preg_replace, causing collisions (e.g., -5 ->
5); update the method to require $postId be a positive integer (throw the
existing UnexpectedValueException if $postId <= 0), coerce/use the integer value
(e.g., (int)$postId) when building the raw key string ($postId . '.' .
$groupValue), and then run the sanitization step — this ensures negatives are
rejected and preg_replace is only applied to a guaranteed positive integer and
group value to avoid mapping different IDs to the same key.

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.

2 participants