From 46cffea6aff651de0605d0e0a41721febe128a32 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 18 Feb 2026 15:54:40 +0300 Subject: [PATCH 1/4] remove CMS form layer Co-Authored-By: Claude Opus 4.6 --- Cms/Form/Dto/MetaDto.php | 19 ------ Cms/Form/Dto/MetaTranslatableDto.php | 21 ------- Cms/Form/Type/MetaTranslatableType.php | 30 --------- Cms/Form/Type/MetaType.php | 86 -------------------------- 4 files changed, 156 deletions(-) delete mode 100644 Cms/Form/Dto/MetaDto.php delete mode 100644 Cms/Form/Dto/MetaTranslatableDto.php delete mode 100644 Cms/Form/Type/MetaTranslatableType.php delete mode 100644 Cms/Form/Type/MetaType.php diff --git a/Cms/Form/Dto/MetaDto.php b/Cms/Form/Dto/MetaDto.php deleted file mode 100644 index ce7e122..0000000 --- a/Cms/Form/Dto/MetaDto.php +++ /dev/null @@ -1,19 +0,0 @@ -translations = new DtoCollection(MetaDto::class); - parent::__construct(MetaTranslatableType::class); - } -} diff --git a/Cms/Form/Type/MetaTranslatableType.php b/Cms/Form/Type/MetaTranslatableType.php deleted file mode 100644 index d0b05bc..0000000 --- a/Cms/Form/Type/MetaTranslatableType.php +++ /dev/null @@ -1,30 +0,0 @@ -setDefaults([ - 'data_class' => MetaTranslatableDto::class, - 'translation_domain' => 'cms', - 'label_format' => 'meta.field.%name%', - ]); - } - - public function buildForm(FormBuilderInterface $builder, array $options): void - { - $builder->add('translations', TranslationsType::class, [ - 'entry_type' => MetaType::class, - ]); - } -} diff --git a/Cms/Form/Type/MetaType.php b/Cms/Form/Type/MetaType.php deleted file mode 100644 index 15c58d3..0000000 --- a/Cms/Form/Type/MetaType.php +++ /dev/null @@ -1,86 +0,0 @@ -setDefaults([ - 'data_class' => MetaDto::class, - 'translation_domain' => 'cms', - 'label_format' => 'meta.field.%name%', - ]); - } - - public function buildForm(FormBuilderInterface $builder, array $options): void - { - $builder - ->add('metaImage', ImageType::class, [ - 'required' => false, - 'constraints' => [ - new Image(), - ], - ]) - ->add('title', TextType::class, [ - 'required' => true, - 'attr' => ['maxlength' => self::MAX_STRING_LENGTH], - 'constraints' => [ - new NotBlank(), - new Length(max: self::MAX_STRING_LENGTH), - ], - ]) - ->add('robotsBehaviour', EnumType::class, [ - 'class' => RobotsBehaviour::class, - 'required' => true, - 'choice_label' => static fn (RobotsBehaviour $case): string => match ($case) { - RobotsBehaviour::IndexFollow => 'robots_behaviour.indexfollow', - RobotsBehaviour::IndexNoFollow => 'robots_behaviour.indexnofollow', - RobotsBehaviour::NoIndexFollow => 'robots_behaviour.noindexfollow', - RobotsBehaviour::NoIndexNoFollow => 'robots_behaviour.noindexnofollow', - }, - 'constraints' => [ - new NotBlank(), - ], - ]) - ->add('metaTitle', TextType::class, [ - 'required' => false, - 'attr' => ['maxlength' => self::MAX_STRING_LENGTH], - 'constraints' => [ - new Length(max: self::MAX_STRING_LENGTH), - ], - ]) - ->add('metaDescription', TextareaType::class, [ - 'required' => false, - 'attr' => ['data-maxlength' => self::MAX_DESCRIPTION_LENGTH, 'rows' => 3], - 'constraints' => [ - new Length(max: self::MAX_DESCRIPTION_LENGTH), - ], - ]) - ->add('metaKeywords', TextType::class, [ - 'required' => false, - 'attr' => ['maxlength' => self::MAX_STRING_LENGTH], - 'constraints' => [ - new Length(max: self::MAX_STRING_LENGTH), - ], - ]); - } -} From 41d1d3f3ab50e5a5a904739a0dfc90eca83aa297 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 18 Feb 2026 16:12:02 +0300 Subject: [PATCH 2/4] add Claude Code agent config and memory Co-Authored-By: Claude Opus 4.6 --- .../code-improvement-reviewer/MEMORY.md | 53 +++++++ .../code-improvement-reviewer/patterns.md | 113 +++++++++++++++ .claude/agents/code-improvement-reviewer.md | 137 ++++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 .claude/agent-memory/code-improvement-reviewer/MEMORY.md create mode 100644 .claude/agent-memory/code-improvement-reviewer/patterns.md create mode 100644 .claude/agents/code-improvement-reviewer.md diff --git a/.claude/agent-memory/code-improvement-reviewer/MEMORY.md b/.claude/agent-memory/code-improvement-reviewer/MEMORY.md new file mode 100644 index 0000000..a3d5d85 --- /dev/null +++ b/.claude/agent-memory/code-improvement-reviewer/MEMORY.md @@ -0,0 +1,53 @@ +# meta-bundle: Code Improvement Reviewer Memory + +## Project Overview +- Package: `chamber-orchestra/meta-bundle` +- Namespace: `ChamberOrchestra\MetaBundle` (PSR-4 from root, no src/) +- PHP ^8.5, Doctrine ORM ^3.0, Symfony 8.0 +- Bundle class: `ChamberOrchestraMetaBundle`, DI extension: `ChamberOrchestraMetaExtension` + +## Key Architectural Notes (post-round-1 fixes) +- Entity layer is standalone (no CMS deps); CMS/Form layer excluded from autoload and coverage +- `MetaTrait` now uses `enumType: RobotsBehaviour::class` on SMALLINT column — native enum mapping +- `MetaTrait::robotsBehaviour` is now `protected RobotsBehaviour` (not raw int) +- `MetaTrait::getRobotsBehaviour()` returns `RobotsBehaviour` enum case (matches MetaInterface contract) +- `MetaInterface` now includes `getMetaImagePath(): ?string` and `getMeta(): array` +- `getRobotsBehaviour(): RobotsBehaviour` returns enum (not raw int) +- `getMeta()` calls `strip_tags()` on description — intentional sanitization +- `robotsBehaviour` defaults to `IndexNoFollow(1)` — deliberately conservative + +## Round 1 Issues (FIXED) +- CI cache key version mismatch — fixed (now uses `steps.setup-php.outputs.php-version`) +- AGENTS.md stale bundle class name — fixed +- MetaTrait had stray `File` import — removed +- ORM column types now use `Types::STRING` / `Types::SMALLINT` / `Types::TEXT` constants +- MetaInterface missing `getMetaImagePath()` — added +- MetaDto dead `$slug` / `$seoDescription` fields — removed +- MetaTrait now stores enum (not int) + +## Round 2 Issues Found (2026-02-17) +See patterns.md for full detail. Key items: +- `RobotsBehaviour::getFormattedBehaviour(int)` is now dead code — entity stores enum, no caller passes raw int +- `MetaInterface::getMeta()` return type is `array` with no shape documentation +- `composer.json` requires `symfony/http-foundation` for entity-layer but it is only used by CMS DTO (excluded) +- `MetaType` uses a variable `$max` shared across multiple field definitions — confusing +- `MetaType::buildForm()` `metaDescription` uses TextareaType but `attr maxlength` doesn't work on textareas in HTML +- `MetaTranslatableDto` constructor passes `MetaDto::class` hardcoded to `DtoCollection` ignoring translatable-specific DTO +- tag.yml: on the very first run from a clean repo it emits `v0.0.1` correctly; but `$major` from `cut -d. -f1` includes the `v` prefix, so subsequent tags are `v1.2.N` (correct). No bug. +- `services.yaml` body registers no services — misleading but harmless +- `phpunit.xml.dist` has no `` element with thresholds +- `CLAUDE.md` line 29 says `getRobotsBehaviour(): int` — stale after round-1 fix (now returns RobotsBehaviour) +- `OutOfBoundsException` / `ExceptionInterface` are only used by `getFormattedBehaviour(int)` — if that's removed, Exception layer becomes dead code +- `MetaDto::$robotsBehaviour` is `?int` but `RobotsBehaviour::choices()` values are also int — no type mismatch, but should be `?RobotsBehaviour` for type-safety +- `MetaInterface::getRobotsBehaviour()` is not in `getMeta()` output — robots tag is not in the returned array +- `MetaTraitTest` missing: test for `getFormattedRobotsBehaviour()` with every case (not just default+custom) +- No test for `getMeta()` including robots behaviour key (because it isn't there — by design or oversight?) + +## Testing Patterns +- Tests in `tests/Unit/`, namespace `Tests\Unit\` +- Uses anonymous class implementing `MetaInterface` + `MetaTrait` for entity tests +- PHPUnit 13.x with `#[DataProvider]` attributes +- CMS layer excluded from both autoload and coverage (external deps) + +## Links +- Detailed notes: `patterns.md` diff --git a/.claude/agent-memory/code-improvement-reviewer/patterns.md b/.claude/agent-memory/code-improvement-reviewer/patterns.md new file mode 100644 index 0000000..4686933 --- /dev/null +++ b/.claude/agent-memory/code-improvement-reviewer/patterns.md @@ -0,0 +1,113 @@ +# meta-bundle: Detailed Review Patterns + +## Bugs & Issues Found (2026-02-17) + +### B1 - CI cache key version mismatch (php.yml L35-37) +Cache key uses `php-8.4` but the workflow sets up PHP 8.5. Cache will never hit. + +### B2 - AGENTS.md references stale bundle class name +Line 6 says `DevMetaBundle.php` — bundle was renamed to `ChamberOrchestraMetaBundle.php`. + +### B3 - MetaTrait implicit symfony/http-foundation dependency +`MetaTrait.php` imports `Symfony\Component\HttpFoundation\File\File` at line 9. +`symfony/http-foundation` is in composer.json `require` so this is technically fine, +but the trait is documented as "standalone entity layer" — tight coupling to File is architectural noise. + +### B4 - services.yaml body is empty — no services registered +This may work (Symfony tolerates empty `services:` block) but is misleading. +Should either register form types or be conditional on CMS packages being present. + +### B5 - MetaInterface missing getMetaImagePath() +`MetaTrait` implements `getMetaImage(): ?File` and `getMetaImagePath(): ?string` +but `MetaInterface` only declares the 5 core getters (title, metaTitle, metaDescription, +metaKeywords, robotsBehaviour). `getMetaImagePath()` is missing from the interface contract. + +### B6 - MetaDto fields not mapped in MetaType +`MetaDto` has `$slug` and `$seoDescription` properties. `MetaType::buildForm()` never +adds form fields for them. These are dead DTO fields or unfinished features. + +### B7 - tag.yml auto-tagging drops the `v` prefix on all but the first tag +Line 34: `echo "next=${major}.${minor}.${next_patch}"` - `$major` is captured from +`cut -d. -f1` which would be `v0`, `v1`, etc. On the first release the tag is `v0.0.1` +(hardcoded correctly), but on subsequent releases `major=v1` so the tag becomes `v1.2.4` +(preserving prefix). Actually this is fine, but the cut on line 31-33 makes `$major` include +the `v`, which means the next tag format is correct. This is fine; no bug. + +## Code Quality Issues + +### Q1 - ORM column type uses legacy string literals +`MetaTrait.php` lines 13, 16, 27: `type: "string"` and `type: 'smallint'` +Should use `Doctrine\DBAL\Types\Types::STRING` and `Types::SMALLINT` for type safety +and IDE navigation. Doctrine deprecated raw string types in ORM 3.x. + +### Q2 - RobotsBehaviour::getFormattedBehaviour() should be an instance method +The static method takes `int $behaviour` and internally does `tryFrom()`. Since this +is an enum, this should be an instance method `getFormatted(): string` — callers +pass the enum case, not a raw int. The static form exists to bridge the raw-int +`robotsBehaviour` property in MetaTrait. If MetaTrait stored the enum case directly +this indirection would disappear. + +### Q3 - MetaTrait stores int not enum +`protected int $robotsBehaviour` instead of `protected RobotsBehaviour $robotsBehaviour`. +Doctrine ORM 3.x supports PHP enum column mapping natively. This would: +- Make `getRobotsBehaviour()` return the enum type +- Eliminate `getFormattedRobotsBehaviour()` needing to call the static bridge method +- Require changing `MetaInterface::getRobotsBehaviour()` signature + +### Q4 - Default robots behaviour is IndexNoFollow(1) — debatable default +New/draft content indexed with nofollow may not be desirable. Most CMS patterns +default to `NoIndexNoFollow` for new records. However this is a design preference. + +### Q5 - getMeta() return type is untyped array +`getMeta(): array` could be typed as `getMeta(): array{pageTitle: ?string, title: ?string, image: ?string, description: ?string, keywords: ?string}` +using PHPDoc, or as a typed struct/DTO in PHP 8.5+. + +### Q6 - MetaTrait::$metaImage is never persisted +`protected ?File $metaImage = null` is a transient property (no ORM column). +It lives alongside persisted `$metaImagePath`. The `getMetaImage()` getter is in the +trait but there is no corresponding setter in the trait — only in the CMS DTO. +This is a valid upload-handling pattern (file object is transient, path is persisted) +but it should be documented with a comment. + +### Q7 - MetaType sets maxlength=127 for title but MetaTrait column is length=255 +`MetaType.php` L42-45: maxlength is 127, Length constraint max is 127. +`MetaTrait.php` L13: column length is 255. The DB can hold 255 chars but the +form rejects anything over 127. This inconsistency means data imported or set +programmatically up to 255 chars would pass the DB layer but fail form validation. +Decide on one canonical max (e.g., 255) and align both. + +### Q8 - MetaType sets maxlength=127 for metaTitle but column is also 255 +Same issue as Q7 for `metaTitle` (L58-62 in MetaType, L16 in MetaTrait). + +### Q9 - MetaType metaDescription column is `text` type (unlimited) but form caps at 255 +`MetaTrait.php` L24: `type: 'text'` has no DB length limit. +`MetaType.php` L66-69: form caps metaDescription at 255 chars. +`MetaType.php` L71-76: same for metaKeywords but metaKeywords column is varchar(255). +For description this is likely intentional (SEO best practice: ~160 chars), but it +should be consistent. At minimum add a code comment explaining why. + +### Q10 - phpunit.xml.dist missing coverage configuration +The `` block excludes `Cms/` and `vendor/` but there is no `` element. +Running with `--coverage-html` would still work but there is no `failOnEmptyCoverage` +or minimum threshold. Consider adding `` with `processUncoveredFiles="true"`. + +## Missing Test Coverage + +### T1 - getFormattedRobotsBehaviour() with invalid stored DB value +`testGetFormattedRobotsBehaviourDefault()` and `testGetFormattedRobotsBehaviourCustom()` exist, +but there is no test for what happens when the DB contains a corrupt value (e.g., 99). +`getFormattedRobotsBehaviour()` would throw `OutOfBoundsException` — this should be tested +via `MetaTraitTest`, not just in `RobotsBehaviourTest`. + +### T2 - getMeta() strip_tags with complex HTML +`testGetMetaStripsHtmlFromDescription` only tests simple nested tags. +Missing: script tags, HTML entities, multiline HTML, empty-after-strip-tags result. + +### T3 - MetaInterface contract test +No test verifies that a class using MetaTrait actually satisfies MetaInterface. +The anonymous class in MetaTraitTest does implement MetaInterface but this is never +asserted explicitly (`self::assertInstanceOf(MetaInterface::class, $entity)`). + +### T4 - RobotsBehaviour::choices() order guarantee +`testChoicesReturnsAllCases()` checks count and specific keys but not that the +ordering matches enum declaration order, which matters for form rendering. diff --git a/.claude/agents/code-improvement-reviewer.md b/.claude/agents/code-improvement-reviewer.md new file mode 100644 index 0000000..7a3632d --- /dev/null +++ b/.claude/agents/code-improvement-reviewer.md @@ -0,0 +1,137 @@ +--- +name: code-improvement-reviewer +description: "Use this agent when you need comprehensive code quality review and improvement suggestions for backend code. Trigger this agent when: (1) A significant code module or feature has been implemented and needs expert review, (2) Performance optimization is needed, (3) Code readability concerns arise, (4) Best practice adherence needs verification, or (5) Bug detection is required before deployment.\\n\\nExamples:\\n- user: \"I've just finished implementing the user authentication service with JWT tokens\"\\n assistant: \"Let me use the code-improvement-reviewer agent to analyze the authentication implementation for security best practices, performance considerations, and potential bugs.\"\\n Since significant authentication code was written, proactively use the code-improvement-reviewer agent to ensure security and quality standards are met.\\n\\n- user: \"Can you review the database query optimizations I made in the OrderRepository?\"\\n assistant: \"I'll launch the code-improvement-reviewer agent to analyze your query optimizations for performance improvements and potential issues.\"\\n User explicitly requested code review, so use the code-improvement-reviewer agent rather than responding directly.\\n\\n- user: \"I've refactored the payment processing module\"\\n assistant: \"Since you've completed a refactoring of critical payment code, let me use the code-improvement-reviewer agent to verify the changes maintain correctness and follow best practices.\"\\n Critical business logic was modified, proactively trigger code review for safety." +tools: Glob, Grep, Read, WebFetch, WebSearch +model: sonnet +color: green +memory: project +--- + +You are a distinguished Senior Backend Engineer with 15+ years of experience across multiple languages, frameworks, and architectural patterns. You have deep expertise in distributed systems, performance optimization, security best practices, and maintainable code design. Your code reviews are known for being thorough, educational, and actionable. + +**Your Core Responsibilities:** + +1. **Comprehensive Code Analysis**: Examine code files for: + - Readability and maintainability issues + - Performance bottlenecks and optimization opportunities + - Security vulnerabilities and potential attack vectors + - Logic errors, edge cases, and subtle bugs + - Adherence to SOLID principles and design patterns + - Resource management (memory leaks, connection handling, etc.) + - Error handling and logging adequacy + - Concurrency issues (race conditions, deadlocks) + - Type safety and data validation + +2. **Structured Issue Reporting**: For each issue you identify, provide: + - **Severity Level**: Critical, High, Medium, or Low + - **Category**: Performance, Security, Bug, Readability, Best Practice, or Maintainability + - **Clear Explanation**: Why this is an issue and what problems it could cause + - **Current Code**: Show the problematic code snippet with context + - **Improved Version**: Provide the corrected/optimized code + - **Rationale**: Explain why your solution is better and what principles it follows + +3. **Educational Approach**: Don't just point out problems—teach. Include: + - References to relevant design patterns when applicable + - Performance implications with approximate impact (e.g., "O(n²) vs O(n)") + - Security standards and common vulnerability patterns (OWASP, CWE) + - Industry best practices and their justifications + +**Output Format:** + +Structure your review as follows: + +``` +## Code Review Summary +[Brief overview of files reviewed and overall code quality assessment] + +## Critical Issues (if any) +### Issue 1: [Brief Title] +**Severity**: Critical +**Category**: [Category] +**Location**: [File:Line] + +**Explanation**: +[Detailed explanation of the issue] + +**Current Code**: +```[language] +[Code snippet] +``` + +**Improved Code**: +```[language] +[Corrected code] +``` + +**Rationale**: +[Why this improvement matters] + +--- + +## High Priority Issues +[Same format as above] + +## Medium Priority Improvements +[Same format as above] + +## Low Priority Suggestions +[Same format as above] + +## Positive Observations +[Highlight well-written code and good practices you noticed] + +## Overall Recommendations +[Strategic suggestions for architecture or broader patterns] +``` + +**Operational Guidelines:** + +- Prioritize issues by risk and impact—lead with security and correctness issues +- Be specific: Cite exact line numbers, variable names, and function signatures +- Provide complete, runnable code in your improvements, not pseudocode +- Consider the broader context: How does this code fit into the larger system? +- Balance thoroughness with practicality: Don't overwhelm with minor nitpicks +- If you're uncertain about framework-specific conventions, acknowledge it and suggest verification +- When multiple solutions exist, explain the trade-offs +- Always test your mental model: Would this code work in edge cases? + +**Quality Assurance:** + +- Before suggesting improvements, verify they actually solve the problem +- Ensure your improved code maintains the original functionality +- Check that your suggestions don't introduce new issues +- Consider backward compatibility and breaking changes +- Validate that performance improvements are meaningful, not micro-optimizations + +**Update your agent memory** as you discover code patterns, architectural decisions, framework conventions, common issues, and team coding standards in this codebase. This builds up institutional knowledge across conversations. Write concise notes about what you found and where. + +Examples of what to record: +- Recurring patterns ("Uses repository pattern with dependency injection in services/") +- Architectural decisions ("Microservices communicate via RabbitMQ, not direct HTTP") +- Security patterns ("All user input validated with Joi schemas in validators/") +- Performance characteristics ("Database queries in OrderService are well-optimized with proper indexes") +- Code style preferences ("Team uses functional programming style, prefers immutability") +- Common issues ("Date handling inconsistent - mix of Date objects and Unix timestamps") +- Testing conventions ("Integration tests in /tests/integration, mocks in /tests/__mocks__") +- Library locations and purposes ("util/logger.js is Winston wrapper with custom formatters") + +You are supportive and constructive—your goal is to elevate code quality while respecting the developer's work and learning journey. + +# Persistent Agent Memory + +You have a persistent Persistent Agent Memory directory at `./view-bundle/.claude/agent-memory/code-improvement-reviewer/`. Its contents persist across conversations. + +As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned. + +Guidelines: +- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise +- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md +- Record insights about problem constraints, strategies that worked or failed, and lessons learned +- Update or remove memories that turn out to be wrong or outdated +- Organize memory semantically by topic, not chronologically +- Use the Write and Edit tools to update your memory files +- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project + +## MEMORY.md + +Your MEMORY.md is currently empty. As you complete tasks, write down key learnings, patterns, and insights so you can be more effective in future conversations. Anything saved in MEMORY.md will be included in your system prompt next time. From acb98e91babe51b28fe3386ba0917fcf87401722 Mon Sep 17 00:00:00 2001 From: Dev Date: Fri, 20 Feb 2026 00:21:35 +0000 Subject: [PATCH 3/4] modernize bundle: switch to MIT license, restructure sources, add CI badges - Change license from Apache 2.0 to MIT - Move sources to src/ with PSR-4 autoloading - Remove CMS form types (MetaType, MetaTranslatableType) - Add phpstan, dependabot, php-cs-fixer configs - Add shields.io badges to README - Rename package to chamber-orchestra/meta-bundle - Simplify and update unit tests Co-Authored-By: Claude Opus 4.6 --- .claude/agents/code-improvement-analyst.md | 137 +++++++++++ .github/dependabot.yml | 13 + .github/workflows/php.yml | 19 +- .github/workflows/tag.yml | 15 +- ...s-fixer.dist.php => .php-cs-fixer.dist.php | 5 +- AGENTS.md | 6 +- CLAUDE.md | 17 +- ChamberOrchestraMetaBundle.php | 11 - .../ChamberOrchestraMetaExtension.php | 15 -- Entity/MetaInterface.php | 30 --- LICENSE | 222 ++---------------- README.md | 124 ++++++---- Resources/translations/cms.en.yaml | 18 -- Resources/translations/cms.ru.yaml | 18 -- composer.json | 38 ++- phpstan.neon | 7 + phpunit.xml.dist | 8 +- .../Entity}/Helper/RobotsBehaviour.php | 2 +- {Entity => src/Entity}/MetaTrait.php | 4 +- tests/Integrational/BundleBootTest.php | 34 --- tests/Integrational/DoctrineMetadataTest.php | 57 ----- tests/Integrational/Entity/TestArticle.php | 70 ------ tests/Integrational/EntityPersistenceTest.php | 178 -------------- tests/Integrational/TestKernel.php | 72 ------ tests/Unit/ChamberOrchestraMetaBundleTest.php | 34 --- .../ChamberOrchestraMetaExtensionTest.php | 34 --- .../Entity/Helper/RobotsBehaviourTest.php | 2 +- tests/Unit/Entity/MetaMappingTest.php | 7 +- tests/Unit/Entity/MetaTraitTest.php | 14 +- 29 files changed, 308 insertions(+), 903 deletions(-) create mode 100644 .claude/agents/code-improvement-analyst.md create mode 100644 .github/dependabot.yml rename php-cs-fixer.dist.php => .php-cs-fixer.dist.php (93%) delete mode 100644 ChamberOrchestraMetaBundle.php delete mode 100644 DependencyInjection/ChamberOrchestraMetaExtension.php delete mode 100644 Entity/MetaInterface.php delete mode 100644 Resources/translations/cms.en.yaml delete mode 100644 Resources/translations/cms.ru.yaml create mode 100644 phpstan.neon rename {Entity => src/Entity}/Helper/RobotsBehaviour.php (90%) rename {Entity => src/Entity}/MetaTrait.php (95%) delete mode 100644 tests/Integrational/BundleBootTest.php delete mode 100644 tests/Integrational/DoctrineMetadataTest.php delete mode 100644 tests/Integrational/Entity/TestArticle.php delete mode 100644 tests/Integrational/EntityPersistenceTest.php delete mode 100644 tests/Integrational/TestKernel.php delete mode 100644 tests/Unit/ChamberOrchestraMetaBundleTest.php delete mode 100644 tests/Unit/DependencyInjection/ChamberOrchestraMetaExtensionTest.php diff --git a/.claude/agents/code-improvement-analyst.md b/.claude/agents/code-improvement-analyst.md new file mode 100644 index 0000000..5ab5587 --- /dev/null +++ b/.claude/agents/code-improvement-analyst.md @@ -0,0 +1,137 @@ +--- +name: code-improvement-analyst +description: "Use this agent when you need comprehensive code quality analysis and improvement suggestions. Specifically use this agent when:\\n\\n- A significant chunk of code has been written and needs review for quality, readability, and best practices\\n- You want to identify potential bugs, performance issues, or security vulnerabilities in existing code\\n- You need detailed explanations of code issues with concrete improvement suggestions\\n- You want to ensure code follows established patterns and conventions\\n- Before merging code or after completing a feature implementation\\n\\nExamples:\\n\\n\\nContext: The user just finished implementing a new API endpoint handler.\\n\\nuser: \"I've finished writing the user authentication endpoint. Here's the code:\"\\n\\n\\nassistant: \"Great! Now that you've completed this authentication endpoint, let me use the Task tool to launch the code-improvement-analyst agent to review it for potential issues, best practices, and improvements.\"\\n\\n\\nSince a significant piece of functionality was just written, proactively use the code-improvement-analyst agent to analyze the code for bugs, performance issues, readability concerns, and adherence to best practices.\\n\\n\\n\\n\\nContext: The user is working on refactoring a database query module.\\n\\nuser: \"I've refactored the query builder to support complex joins. Can you take a look?\"\\n\\nassistant: \"I'll use the Task tool to launch the code-improvement-analyst agent to perform a thorough review of your refactored query builder.\"\\n\\n\\nThe user is requesting a code review. Use the code-improvement-analyst agent to examine the refactored code for potential bugs, performance optimizations, readability improvements, and best practice violations.\\n\\n\\n\\n\\nContext: The user mentions they're having intermittent failures in production.\\n\\nuser: \"We're seeing random failures in the payment processing module. Not sure what's causing it.\"\\n\\nassistant: \"Let me use the Task tool to launch the code-improvement-analyst agent to analyze the payment processing module for potential race conditions, error handling issues, and other bugs that could cause intermittent failures.\"\\n\\n\\nIntermittent failures often indicate subtle bugs like race conditions, improper error handling, or state management issues. Use the code-improvement-analyst agent to perform a deep analysis focused on bug detection.\\n\\n" +tools: Glob, Grep, Read, WebFetch, WebSearch +model: sonnet +color: green +memory: project +--- + +You are an elite backend engineering specialist with 15+ years of experience building large-scale, production-grade systems. Your expertise spans performance optimization, architectural patterns, security best practices, and code maintainability. You have a keen eye for subtle bugs and a deep understanding of how code behaves in production environments. + +**Your Mission**: Analyze code with the rigor of a distinguished backend engineer performing a critical production readiness review. Identify issues that impact readability, performance, maintainability, security, and correctness. Provide actionable improvements with clear explanations. + +**Analysis Framework**: + +1. **Bug Detection** - Your highest priority + - Race conditions and concurrency issues + - Off-by-one errors and boundary conditions + - Null/undefined reference errors + - Resource leaks (memory, connections, file handles) + - Error handling gaps and exception swallowing + - State management bugs + - Input validation vulnerabilities + - Edge cases that could cause failures + +2. **Performance Issues** + - N+1 queries and inefficient database operations + - Unnecessary loops or redundant computations + - Memory inefficiencies and excessive allocations + - Blocking I/O in async contexts + - Missing caching opportunities + - Inefficient data structures or algorithms + - Premature optimization (flag it, don't always fix it) + +3. **Readability & Maintainability** + - Unclear variable/function names + - Complex conditional logic that needs simplification + - Long functions that violate single responsibility + - Missing or misleading comments + - Inconsistent code style + - Magic numbers and hard-coded values + - Duplicate code that should be extracted + +4. **Best Practices & Architecture** + - SOLID principle violations + - Tight coupling and poor separation of concerns + - Missing error handling or logging + - Security vulnerabilities (SQL injection, XSS, etc.) + - Inadequate input validation + - Missing tests or untestable code + - Dependency management issues + - Configuration management anti-patterns + +**Output Format**: + +For each issue you identify, provide: + +``` +## Issue #[N]: [Brief, specific title] + +**Severity**: [Critical/High/Medium/Low] +**Category**: [Bug/Performance/Readability/Best Practice] + +**Explanation**: +[Clear, detailed explanation of why this is an issue, what problems it could cause, and the underlying principle being violated. Use concrete examples of how this could fail in production.] + +**Current Code**: +```[language] +[Show the specific problematic code snippet with context] +``` + +**Improved Code**: +```[language] +[Show the corrected version with improvements] +``` + +**Why This Is Better**: +[Explain the specific improvements and benefits of the new approach] +``` + +**Operational Guidelines**: + +- **Prioritize bugs over style**: Critical bugs and security issues come first +- **Be specific, not generic**: Point to exact lines/patterns, don't give general advice +- **Context matters**: Consider the broader codebase context when available +- **Explain the 'why'**: Every suggestion should teach, not just prescribe +- **Show working code**: Your improved versions must be production-ready, not pseudocode +- **Balance perfection with pragmatism**: Flag issues but acknowledge when "good enough" is appropriate +- **Consider trade-offs**: If a performance fix reduces readability, discuss the trade-off +- **Limit scope appropriately**: For large files, focus on the most impactful issues first + +**Quality Assurance**: + +- Verify your improved code compiles/runs and actually solves the issue +- Ensure you haven't introduced new bugs while fixing old ones +- Double-check that your suggestions align with the language's idioms and conventions +- If you're uncertain about a potential issue, clearly state your uncertainty and reasoning + +**When Analysis Is Complete**: + +Provide a summary that includes: +- Total number of issues found by severity +- Most critical items requiring immediate attention +- Quick wins that provide high value for low effort +- Overall code quality assessment + +**Update your agent memory** as you discover code patterns, architectural decisions, common anti-patterns in this codebase, performance bottlenecks, bug categories, and team coding conventions. This builds up institutional knowledge across conversations. Write concise notes about what you found and where. + +Examples of what to record: +- Recurring patterns or anti-patterns you've seen multiple times +- Project-specific architectural decisions or constraints +- Common bug categories specific to this codebase +- Performance characteristics of key components +- Team conventions that differ from standard practices +- Security requirements or compliance considerations +- Testing patterns and coverage gaps + +Remember: You are not just finding problems—you are mentoring through code review. Each issue you identify is an opportunity to elevate the codebase and the team's engineering practices. + +# Persistent Agent Memory + +You have a persistent Persistent Agent Memory directory at `./form-bundle/.claude/agent-memory/code-improvement-analyst/`. Its contents persist across conversations. + +As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned. + +Guidelines: +- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise +- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md +- Record insights about problem constraints, strategies that worked or failed, and lessons learned +- Update or remove memories that turn out to be wrong or outdated +- Organize memory semantically by topic, not chronologically +- Use the Write and Edit tools to update your memory files +- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project + +## MEMORY.md + +Your MEMORY.md is currently empty. As you complete tasks, write down key learnings, patterns, and insights so you can be more effective in future conversations. Anything saved in MEMORY.md will be included in your system prompt next time. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cf4b173 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 512030b..0c17b42 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -2,9 +2,9 @@ name: PHP Composer on: push: - branches: [ "master", "8.0" ] + branches: ["main"] pull_request: - branches: [ "master", "8.0" ] + branches: ["main"] permissions: contents: read @@ -14,10 +14,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup PHP 8.5 - id: setup-php uses: shivammathur/setup-php@v2 with: php-version: "8.5" @@ -28,17 +27,23 @@ jobs: run: composer validate --strict - name: Cache Composer packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | vendor ~/.composer/cache/files - key: ${{ runner.os }}-php-${{ steps.setup-php.outputs.php-version }}-composer-${{ hashFiles('**/composer.lock') }} + key: ${{ runner.os }}-php-8.5-composer-${{ hashFiles('**/composer.lock') }} restore-keys: | - ${{ runner.os }}-php-${{ steps.setup-php.outputs.php-version }}-composer- + ${{ runner.os }}-php-8.5-composer- - name: Install dependencies run: composer install --prefer-dist --no-progress --no-interaction - name: Run test suite run: composer run-script test + + - name: Run PHPStan + run: composer run-script analyse + + - name: Run PHP-CS-Fixer + run: composer run-script cs-check diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index f3a5b49..4fb5eb6 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -3,7 +3,7 @@ name: Tag Release on: pull_request: types: [closed] - branches: [master, "8.0"] + branches: [main] permissions: contents: write @@ -14,9 +14,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: - ref: ${{ github.event.pull_request.base.ref }} + ref: main fetch-depth: 0 fetch-tags: true @@ -37,10 +37,5 @@ jobs: - name: Create and push tag run: | - next="${{ steps.version.outputs.next }}" - if [[ -z "$next" ]]; then - echo "::error::Version computation produced an empty tag — aborting" - exit 1 - fi - git tag "$next" - git push origin "$next" + git tag "${{ steps.version.outputs.next }}" + git push origin "${{ steps.version.outputs.next }}" diff --git a/php-cs-fixer.dist.php b/.php-cs-fixer.dist.php similarity index 93% rename from php-cs-fixer.dist.php rename to .php-cs-fixer.dist.php index 10928e2..9b9d4c6 100644 --- a/php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -3,9 +3,8 @@ declare(strict_types=1); $finder = (new PhpCsFixer\Finder()) - ->in(__DIR__) - ->exclude('var') - ->exclude('vendor') + ->in(__DIR__ . '/src') + ->in(__DIR__ . '/tests') ; return (new PhpCsFixer\Config()) diff --git a/AGENTS.md b/AGENTS.md index 73da4fe..482fc70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,11 +1,9 @@ # Repository Guidelines ## Project Structure & Module Organization -- Entity layer (`Entity/`) provides `MetaInterface`, `MetaTrait`, and `RobotsBehaviour` enum under `ChamberOrchestra\MetaBundle`. -- CMS form layer (`Cms/Form/`) provides DTOs and Symfony form types — requires external `dev/*` packages. -- Bundle entry point is `ChamberOrchestraMetaBundle.php`; DI extension in `DependencyInjection/ChamberOrchestraMetaExtension.php`. +- Entity layer (`src/Entity/`) provides `MetaTrait` and `RobotsBehaviour` enum under `ChamberOrchestra\Meta`. - Tests belong in `tests/` (autoloaded as `Tests\`); tools are in `bin/` (`bin/phpunit`). -- Autoloading is PSR-4 from the package root (no `src/` directory). +- Autoloading is PSR-4 from `src/`. - Requirements: PHP 8.5+, Doctrine ORM ^3.0, Symfony 8.0. ## Build, Test, and Development Commands diff --git a/CLAUDE.md b/CLAUDE.md index 94071c0..4befe6a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,13 +4,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -A Symfony bundle providing a reusable SEO meta data layer — entity interface, Doctrine ORM trait, enum, and optionally CMS form types/DTOs — designed to mix into content entities. +A Symfony package providing a reusable SEO meta data layer — Doctrine ORM trait, enum, and helper methods — designed to mix into content entities. -**Requirements:** PHP ^8.5, Doctrine ORM ^3.0, Symfony ^8.0, `chamber-orchestra/file-bundle` ^8.0 +**Requirements:** PHP ^8.5, Doctrine ORM ^3.0, Symfony ^8.0, `chamber-orchestra/view-bundle` ^8.0 -**Namespace:** `ChamberOrchestra\MetaBundle` (PSR-4 from package root — no `src/` directory) - -**Bundle class:** `ChamberOrchestraMetaBundle` — DI extension is `ChamberOrchestraMetaExtension` (no services registered; entity layer is pure PHP) +**Namespace:** `ChamberOrchestra\Meta` (PSR-4 from `src/`) ## Commands @@ -26,20 +24,13 @@ composer test # Alias for vendor/bin/phpunit ### Entity Layer -- `MetaInterface` — contract for `getTitle()`, `getMetaTitle()`, `getMetaDescription()`, `getMetaKeywords()`, `getMetaImage()`, `getMetaImagePath()`, `getRobotsBehaviour()`, `getMeta()` -- `MetaTrait` — Doctrine ORM implementation with mapped properties: `title`, `metaTitle`, `metaImage` (transient, `#[UploadableProperty]`), `metaImagePath`, `metaDescription`, `metaKeywords`, `robotsBehaviour` (smallint with `enumType: RobotsBehaviour`). The `metaImage` File property integrates with file-bundle's upload system via `#[Upload\UploadableProperty(mappedBy: 'metaImagePath')]`. +- `MetaTrait` — Doctrine ORM trait with mapped properties: `title`, `metaTitle`, `metaImage` (transient, `#[UploadableProperty]`), `metaImagePath`, `metaDescription`, `metaKeywords`, `robotsBehaviour` (smallint with `enumType: RobotsBehaviour`). The `metaImage` File property integrates with file-bundle's upload system via `#[Upload\UploadableProperty(mappedBy: 'metaImagePath')]`. - `RobotsBehaviour` — int-backed enum: `IndexFollow(0)`, `IndexNoFollow(1)`, `NoIndexFollow(2)`, `NoIndexNoFollow(3)`. Provides `format(): string` for robots meta tag strings. -### CMS/Form Layer (requires external packages) - -- `MetaDto` / `MetaType` — admin forms using Symfony `EnumType` for robots behaviour (requires `dev/cms-bundle`, `dev/file-bundle`) -- `MetaTranslatableDto` / `MetaTranslatableType` — multi-language support (requires `dev/translation-bundle`) - ## Testing - PHPUnit 13.x; tests in `tests/` autoloaded as `Tests\` - Unit tests in `tests/Unit/` extend `TestCase` -- Cms/Form layer excluded from coverage (depends on external packages) ## Code Conventions diff --git a/ChamberOrchestraMetaBundle.php b/ChamberOrchestraMetaBundle.php deleted file mode 100644 index c2b420b..0000000 --- a/ChamberOrchestraMetaBundle.php +++ /dev/null @@ -1,11 +0,0 @@ -` rendering, with automatic HTML stripping on descriptions -- **Native Doctrine enum mapping** — `RobotsBehaviour` is stored as `SMALLINT` with `enumType`, hydrated directly as an enum case +- **`getMeta()`** — returns a clean associative array for rendering, with automatic HTML stripping on descriptions +- **Native Doctrine enum mapping** — `RobotsBehaviour` stored as `SMALLINT` with `enumType`, hydrated directly as an enum case - **File-bundle integration** — transient `File $metaImage` property with `#[UploadableProperty]` for automatic image upload handling via `chamber-orchestra/file-bundle` -- **CMS form types** (optional) — `MetaType` with Symfony `EnumType`, image upload, and validation constraints aligned to column lengths -- **Translatable support** (optional) — `MetaTranslatableType` / `MetaTranslatableDto` for multi-language meta data -- **Translation files** — ships with English and Russian labels for form fields and robots choices ## Requirements - PHP ^8.5 -- Doctrine ORM ^3.0 - Symfony ^8.0 -- `chamber-orchestra/file-bundle` ^8.0 - -Optional (for CMS form layer): - -- `chamber-orchestra/cms-bundle` -- `chamber-orchestra/translation-bundle` (for i18n) +- Doctrine ORM ^3.0 +- `chamber-orchestra/view-bundle` ^8.0 +- `chamber-orchestra/file-bundle` (in consuming application, for image upload support) ## Installation @@ -37,13 +39,12 @@ composer require chamber-orchestra/meta-bundle ```php use ChamberOrchestra\FileBundle\Mapping\Annotation as Upload; -use ChamberOrchestra\MetaBundle\Entity\MetaInterface; -use ChamberOrchestra\MetaBundle\Entity\MetaTrait; +use ChamberOrchestra\Meta\Entity\MetaTrait; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] #[Upload\Uploadable(prefix: 'article')] -class Article implements MetaInterface +class Article { use MetaTrait; @@ -56,9 +57,9 @@ class Article implements MetaInterface } ``` -The `#[Uploadable]` class attribute is required for file-bundle to handle the `metaImage` upload automatically. +The `#[Uploadable]` attribute is required for file-bundle to handle the `metaImage` upload automatically. -This adds the following columns and properties to your entity: +This adds the following columns and properties: | Property | Type | Persisted | Description | |--------------------|------------|-----------|-----------------------------------| @@ -72,16 +73,59 @@ This adds the following columns and properties to your entity: ### 2. Render meta tags in Twig +Pass the `getMeta()` array and the entity to your base layout, then build the ``: + ```twig -{% set meta = article.meta %} - -{{ meta.title ?? meta.pageTitle }} - - - -{% if meta.image %} - -{% endif %} +{# base.html.twig #} +{% set meta = entity.meta %} + +{%- set title -%} + {{ "meta.title.common"|trans }}{{ meta.title|default('') ? ' | ' ~ meta.title }} + {%- if app.request.query.has("page") and app.request.query.get("page") > 1 -%} + | {{ "meta.title.page"|trans({"page": app.request.query.get("page")}) -}} + {%- endif -%} +{%- endset -%} +{%- set title = title|replace({"\n": "", "\r\n": "", "\t": "", "\n\r": ""})|trim -%} +{%- set description = meta.description|default("meta.description.common"|trans) -%} +{%- set keywords = meta.keywords|default("meta.keywords.common"|trans) -%} +{%- set socialTitle = meta.title|default(title) -%} +{%- set socialDescription = meta.description|default(description) -%} + + + + + + {{ title }} + + {%- if + not app.request.query.has("page") + and not app.request.query.has("filter") + and not app.request.query.has("sort") + and not app.request.query.has("order") + and not app.request.query.has("limit") -%} + + + {%- endif %} + + + + + + {%- if meta.image %} + + {%- endif %} + + + + + + + + + + + {% block stylesheets %}{% endblock %} + ``` The `getMeta()` method automatically strips HTML tags from the description. @@ -89,7 +133,7 @@ The `getMeta()` method automatically strips HTML tags from the description. ### 3. Robots behaviour enum ```php -use ChamberOrchestra\MetaBundle\Entity\Helper\RobotsBehaviour; +use ChamberOrchestra\Meta\Entity\Helper\RobotsBehaviour; $entity->getRobotsBehaviour(); // RobotsBehaviour::IndexNoFollow $entity->getFormattedRobotsBehaviour(); // "index, nofollow" @@ -106,31 +150,15 @@ Available cases: | `NoIndexFollow` | 2 | `noindex, follow` | | `NoIndexNoFollow` | 3 | `noindex, nofollow` | -### 4. CMS admin forms (optional) - -Embed the meta form type in your admin form: - -```php -use ChamberOrchestra\MetaBundle\Cms\Form\Type\MetaType; - -$builder->add('meta', MetaType::class); -``` - -For translatable entities: - -```php -use ChamberOrchestra\MetaBundle\Cms\Form\Type\MetaTranslatableType; - -$builder->add('meta', MetaTranslatableType::class); -``` - ## Testing ```bash composer install composer test +composer analyse +composer cs-check ``` ## License -Apache-2.0 +MIT diff --git a/Resources/translations/cms.en.yaml b/Resources/translations/cms.en.yaml deleted file mode 100644 index 84f8a5b..0000000 --- a/Resources/translations/cms.en.yaml +++ /dev/null @@ -1,18 +0,0 @@ -meta: - field: - title: "Title (H1)" - metaTitle: "Meta title" - metaDescription: "Meta description" - metaKeywords: "Keywords" - robotsBehaviour: "Robots Behaviour" - metaImage: "Meta image" - -common: - nav: - meta: Meta - -robots_behaviour: - indexfollow: "INDEX, FOLLOW" - indexnofollow: "INDEX, NO FOLLOW" - noindexfollow: "NO INDEX, FOLLOW" - noindexnofollow: "NO INDEX, NO FOLLOW" \ No newline at end of file diff --git a/Resources/translations/cms.ru.yaml b/Resources/translations/cms.ru.yaml deleted file mode 100644 index ae239fc..0000000 --- a/Resources/translations/cms.ru.yaml +++ /dev/null @@ -1,18 +0,0 @@ -meta: - field: - title: "Заголовок (H1)" - metaTitle: "Meta заголовок" - metaDescription: "Meta описание" - metaKeywords: "Ключевые слова" - robotsBehaviour: "Тэг robots" - metaImage: "Meta изображение" - -common: - nav: - meta: Meta - -robots_behaviour: - indexfollow: "INDEX, FOLLOW" - indexnofollow: "INDEX, NO FOLLOW" - noindexfollow: "NO INDEX, FOLLOW" - noindexnofollow: "NO INDEX, NO FOLLOW" \ No newline at end of file diff --git a/composer.json b/composer.json index 39545b7..d1e85e6 100644 --- a/composer.json +++ b/composer.json @@ -1,22 +1,20 @@ { "name": "chamber-orchestra/meta-bundle", "type": "symfony-bundle", - "description": "Symfony 8 bundle providing reusable SEO meta data layer — Doctrine ORM trait, interface, robots enum, and optional CMS admin form types with i18n support", + "description": "Symfony 8 bundle providing a Doctrine ORM trait for SEO meta fields — title, description, keywords, Open Graph image, and robots behaviour", "keywords": [ "symfony", - "symfony8", + "symfony-bundle", "seo", "meta", "meta-tags", "open-graph", - "file-upload", "robots", "doctrine", "orm", - "trait", - "bundle" + "php" ], - "license": "Apache-2.0", + "license": "MIT", "authors": [ { "name": "Andrew Lukin", @@ -27,26 +25,19 @@ ], "require": { "php": "^8.5", - "chamber-orchestra/file-bundle": "^8.0", + "chamber-orchestra/view-bundle": "8.0.*", "doctrine/orm": "^3.0", - "symfony/http-foundation": "^8.0", - "symfony/http-kernel": "^8.0" + "symfony/http-foundation": "8.0.*" }, "require-dev": { - "doctrine/doctrine-bundle": "^3.2", "friendsofphp/php-cs-fixer": "^3.0", - "phpunit/phpunit": "^13.0", - "symfony/framework-bundle": "^8.0", - "symfony/yaml": "^8.0" + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^13.0" }, "autoload": { "psr-4": { - "ChamberOrchestra\\MetaBundle\\": "" - }, - "exclude-from-classmap": [ - "/tests/", - "/Cms/" - ] + "ChamberOrchestra\\Meta\\": "src/" + } }, "autoload-dev": { "psr-4": { @@ -55,9 +46,14 @@ }, "minimum-stability": "stable", "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "symfony/runtime": true + } }, "scripts": { - "test": "vendor/bin/phpunit" + "test": "vendor/bin/phpunit", + "analyse": "vendor/bin/phpstan analyse", + "cs-check": "vendor/bin/php-cs-fixer fix --dry-run --diff" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..0056c71 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +parameters: + level: max + paths: + - src + phpVersion: 80500 + ignoreErrors: + - identifier: trait.unused diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 30d15dc..8ff7c86 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,10 +10,6 @@ - - - - @@ -22,9 +18,7 @@ - Entity - DependencyInjection - ChamberOrchestraMetaBundle.php + src diff --git a/Entity/Helper/RobotsBehaviour.php b/src/Entity/Helper/RobotsBehaviour.php similarity index 90% rename from Entity/Helper/RobotsBehaviour.php rename to src/Entity/Helper/RobotsBehaviour.php index 2a633d1..f276021 100644 --- a/Entity/Helper/RobotsBehaviour.php +++ b/src/Entity/Helper/RobotsBehaviour.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ChamberOrchestra\MetaBundle\Entity\Helper; +namespace ChamberOrchestra\Meta\Entity\Helper; enum RobotsBehaviour: int { diff --git a/Entity/MetaTrait.php b/src/Entity/MetaTrait.php similarity index 95% rename from Entity/MetaTrait.php rename to src/Entity/MetaTrait.php index 019dbda..18dcfa8 100644 --- a/Entity/MetaTrait.php +++ b/src/Entity/MetaTrait.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace ChamberOrchestra\MetaBundle\Entity; +namespace ChamberOrchestra\Meta\Entity; use ChamberOrchestra\FileBundle\Mapping\Annotation as Upload; -use ChamberOrchestra\MetaBundle\Entity\Helper\RobotsBehaviour; +use ChamberOrchestra\Meta\Entity\Helper\RobotsBehaviour; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\HttpFoundation\File\File; diff --git a/tests/Integrational/BundleBootTest.php b/tests/Integrational/BundleBootTest.php deleted file mode 100644 index d13c1e1..0000000 --- a/tests/Integrational/BundleBootTest.php +++ /dev/null @@ -1,34 +0,0 @@ -getEnvironment()); - } - - public function testMetaBundleIsRegistered(): void - { - self::bootKernel(); - - $bundles = self::$kernel->getBundles(); - - self::assertArrayHasKey('ChamberOrchestraMetaBundle', $bundles); - self::assertInstanceOf(ChamberOrchestraMetaBundle::class, $bundles['ChamberOrchestraMetaBundle']); - } -} diff --git a/tests/Integrational/DoctrineMetadataTest.php b/tests/Integrational/DoctrineMetadataTest.php deleted file mode 100644 index 3954e34..0000000 --- a/tests/Integrational/DoctrineMetadataTest.php +++ /dev/null @@ -1,57 +0,0 @@ -get(EntityManagerInterface::class); - $metadata = $em->getClassMetadata(TestArticle::class); - - self::assertSame('test_article', $metadata->getTableName()); - } - - public function testMetaTraitFieldsAreInMetadata(): void - { - self::bootKernel(); - $em = self::getContainer()->get(EntityManagerInterface::class); - $metadata = $em->getClassMetadata(TestArticle::class); - - $expectedFields = ['title', 'metaTitle', 'metaImagePath', 'metaDescription', 'metaKeywords', 'robotsBehaviour']; - - foreach ($expectedFields as $field) { - self::assertTrue( - $metadata->hasField($field), - \sprintf('Field "%s" is missing from metadata', $field), - ); - } - } - - public function testRobotsBehaviourEnumTypeMapping(): void - { - self::bootKernel(); - $em = self::getContainer()->get(EntityManagerInterface::class); - $metadata = $em->getClassMetadata(TestArticle::class); - - $mapping = $metadata->fieldMappings['robotsBehaviour']; - - self::assertSame(Types::SMALLINT, $mapping->type); - self::assertSame(RobotsBehaviour::class, $mapping->enumType); - } -} diff --git a/tests/Integrational/Entity/TestArticle.php b/tests/Integrational/Entity/TestArticle.php deleted file mode 100644 index a1e3e61..0000000 --- a/tests/Integrational/Entity/TestArticle.php +++ /dev/null @@ -1,70 +0,0 @@ -id; - } - - public function setTitle(?string $title): self - { - $this->title = $title; - - return $this; - } - - public function setMetaTitle(?string $metaTitle): self - { - $this->metaTitle = $metaTitle; - - return $this; - } - - public function setMetaDescription(?string $metaDescription): self - { - $this->metaDescription = $metaDescription; - - return $this; - } - - public function setMetaKeywords(?string $metaKeywords): self - { - $this->metaKeywords = $metaKeywords; - - return $this; - } - - public function setMetaImagePath(?string $metaImagePath): self - { - $this->metaImagePath = $metaImagePath; - - return $this; - } - - public function setRobotsBehaviour(RobotsBehaviour $robotsBehaviour): self - { - $this->robotsBehaviour = $robotsBehaviour; - - return $this; - } -} diff --git a/tests/Integrational/EntityPersistenceTest.php b/tests/Integrational/EntityPersistenceTest.php deleted file mode 100644 index b627fe7..0000000 --- a/tests/Integrational/EntityPersistenceTest.php +++ /dev/null @@ -1,178 +0,0 @@ -em = self::getContainer()->get(EntityManagerInterface::class); - - $schemaTool = new SchemaTool($this->em); - $schemaTool->createSchema([$this->em->getClassMetadata(TestArticle::class)]); - } - - protected function tearDown(): void - { - $schemaTool = new SchemaTool($this->em); - $schemaTool->dropSchema([$this->em->getClassMetadata(TestArticle::class)]); - - parent::tearDown(); - \restore_exception_handler(); - } - - public function testPersistAndRetrieveWithAllFields(): void - { - $article = (new TestArticle()) - ->setTitle('Test Page') - ->setMetaTitle('SEO Title') - ->setMetaDescription('

Description with HTML

') - ->setMetaKeywords('php, symfony') - ->setMetaImagePath('/images/test.jpg') - ->setRobotsBehaviour(RobotsBehaviour::NoIndexNoFollow); - - $this->em->persist($article); - $this->em->flush(); - - $id = $article->getId(); - self::assertNotNull($id); - - $this->em->clear(); - - $loaded = $this->em->find(TestArticle::class, $id); - - self::assertNotNull($loaded); - self::assertSame('Test Page', $loaded->getTitle()); - self::assertSame('SEO Title', $loaded->getMetaTitle()); - self::assertSame('

Description with HTML

', $loaded->getMetaDescription()); - self::assertSame('php, symfony', $loaded->getMetaKeywords()); - self::assertSame('/images/test.jpg', $loaded->getMetaImagePath()); - self::assertSame(RobotsBehaviour::NoIndexNoFollow, $loaded->getRobotsBehaviour()); - } - - public function testPersistWithDefaultValues(): void - { - $article = new TestArticle(); - - $this->em->persist($article); - $this->em->flush(); - $this->em->clear(); - - $loaded = $this->em->find(TestArticle::class, $article->getId()); - - self::assertNotNull($loaded); - self::assertNull($loaded->getTitle()); - self::assertNull($loaded->getMetaTitle()); - self::assertNull($loaded->getMetaDescription()); - self::assertNull($loaded->getMetaKeywords()); - self::assertNull($loaded->getMetaImagePath()); - self::assertSame(RobotsBehaviour::IndexNoFollow, $loaded->getRobotsBehaviour()); - } - - public function testRobotsBehaviourEnumRoundTrip(): void - { - foreach (RobotsBehaviour::cases() as $case) { - $article = (new TestArticle()) - ->setRobotsBehaviour($case); - - $this->em->persist($article); - $this->em->flush(); - $this->em->clear(); - - $loaded = $this->em->find(TestArticle::class, $article->getId()); - - self::assertSame($case, $loaded->getRobotsBehaviour(), \sprintf('Round-trip failed for %s', $case->name)); - } - } - - public function testGetMetaAfterPersistAndReload(): void - { - $article = (new TestArticle()) - ->setTitle('Page Title') - ->setMetaTitle('Meta Title') - ->setMetaDescription('Bold text') - ->setMetaKeywords('keyword1, keyword2') - ->setMetaImagePath('/img/social.png'); - - $this->em->persist($article); - $this->em->flush(); - $this->em->clear(); - - $loaded = $this->em->find(TestArticle::class, $article->getId()); - $meta = $loaded->getMeta(); - - self::assertSame('Page Title', $meta['pageTitle']); - self::assertSame('Meta Title', $meta['title']); - self::assertSame('/img/social.png', $meta['image']); - self::assertSame('Bold text', $meta['description']); - self::assertSame('keyword1, keyword2', $meta['keywords']); - } - - public function testUpdateEntity(): void - { - $article = (new TestArticle()) - ->setTitle('Original') - ->setRobotsBehaviour(RobotsBehaviour::IndexFollow); - - $this->em->persist($article); - $this->em->flush(); - - $article->setTitle('Updated'); - $article->setRobotsBehaviour(RobotsBehaviour::NoIndexFollow); - - $this->em->flush(); - $this->em->clear(); - - $loaded = $this->em->find(TestArticle::class, $article->getId()); - - self::assertSame('Updated', $loaded->getTitle()); - self::assertSame(RobotsBehaviour::NoIndexFollow, $loaded->getRobotsBehaviour()); - } - - public function testFormattedRobotsBehaviourAfterReload(): void - { - $article = (new TestArticle()) - ->setRobotsBehaviour(RobotsBehaviour::NoIndexNoFollow); - - $this->em->persist($article); - $this->em->flush(); - $this->em->clear(); - - $loaded = $this->em->find(TestArticle::class, $article->getId()); - - self::assertSame('noindex, nofollow', $loaded->getFormattedRobotsBehaviour()); - } - - public function testNullFieldsPersistCorrectly(): void - { - $article = (new TestArticle()) - ->setTitle('Title') - ->setMetaTitle(null) - ->setMetaDescription(null) - ->setMetaKeywords(null) - ->setMetaImagePath(null); - - $this->em->persist($article); - $this->em->flush(); - $this->em->clear(); - - $loaded = $this->em->find(TestArticle::class, $article->getId()); - - self::assertSame('Title', $loaded->getTitle()); - self::assertNull($loaded->getMetaTitle()); - self::assertNull($loaded->getMetaDescription()); - self::assertNull($loaded->getMetaKeywords()); - self::assertNull($loaded->getMetaImagePath()); - } -} diff --git a/tests/Integrational/TestKernel.php b/tests/Integrational/TestKernel.php deleted file mode 100644 index 5baa458..0000000 --- a/tests/Integrational/TestKernel.php +++ /dev/null @@ -1,72 +0,0 @@ -extension('framework', [ - 'secret' => 'test_secret', - 'test' => true, - ]); - - $dbalConfig = isset($_ENV['DATABASE_URL']) - ? ['url' => $_ENV['DATABASE_URL'], 'server_version' => '17'] - : [ - 'driver' => 'pdo_pgsql', - 'host' => '/var/run/postgresql', - 'user' => \get_current_user(), - 'dbname' => 'meta_bundle_test', - 'server_version' => '17', - ]; - - $container->extension('doctrine', [ - 'dbal' => $dbalConfig, - 'orm' => [ - 'entity_managers' => [ - 'default' => [ - 'mappings' => [ - 'Tests' => [ - 'type' => 'attribute', - 'dir' => '%kernel.project_dir%/tests/Integrational/Entity', - 'prefix' => 'Tests\\Integrational\\Entity', - 'alias' => 'Tests', - ], - ], - ], - ], - ], - ]); - - $container->services() - ->alias(EntityManagerInterface::class, 'doctrine.orm.entity_manager') - ->public(); - } - - public function getProjectDir(): string - { - return \dirname(__DIR__, 2); - } -} diff --git a/tests/Unit/ChamberOrchestraMetaBundleTest.php b/tests/Unit/ChamberOrchestraMetaBundleTest.php deleted file mode 100644 index a61fbd7..0000000 --- a/tests/Unit/ChamberOrchestraMetaBundleTest.php +++ /dev/null @@ -1,34 +0,0 @@ -getContainerExtension()); - } - - public function testBundleName(): void - { - $bundle = new ChamberOrchestraMetaBundle(); - - self::assertSame('ChamberOrchestraMetaBundle', $bundle->getName()); - } -} diff --git a/tests/Unit/DependencyInjection/ChamberOrchestraMetaExtensionTest.php b/tests/Unit/DependencyInjection/ChamberOrchestraMetaExtensionTest.php deleted file mode 100644 index 0cb5a6f..0000000 --- a/tests/Unit/DependencyInjection/ChamberOrchestraMetaExtensionTest.php +++ /dev/null @@ -1,34 +0,0 @@ -load([], $container); - - $serviceIds = \array_filter( - $container->getServiceIds(), - static fn (string $id): bool => \str_starts_with($id, 'ChamberOrchestra\\MetaBundle\\'), - ); - - self::assertSame([], $serviceIds); - } - - public function testExtensionAlias(): void - { - $extension = new ChamberOrchestraMetaExtension(); - - self::assertSame('chamber_orchestra_meta', $extension->getAlias()); - } -} diff --git a/tests/Unit/Entity/Helper/RobotsBehaviourTest.php b/tests/Unit/Entity/Helper/RobotsBehaviourTest.php index 0779b2d..a4e3346 100644 --- a/tests/Unit/Entity/Helper/RobotsBehaviourTest.php +++ b/tests/Unit/Entity/Helper/RobotsBehaviourTest.php @@ -4,7 +4,7 @@ namespace Tests\Unit\Entity\Helper; -use ChamberOrchestra\MetaBundle\Entity\Helper\RobotsBehaviour; +use ChamberOrchestra\Meta\Entity\Helper\RobotsBehaviour; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; diff --git a/tests/Unit/Entity/MetaMappingTest.php b/tests/Unit/Entity/MetaMappingTest.php index 715b2b4..1c1b096 100644 --- a/tests/Unit/Entity/MetaMappingTest.php +++ b/tests/Unit/Entity/MetaMappingTest.php @@ -4,9 +4,8 @@ namespace Tests\Unit\Entity; -use ChamberOrchestra\MetaBundle\Entity\Helper\RobotsBehaviour; -use ChamberOrchestra\MetaBundle\Entity\MetaInterface; -use ChamberOrchestra\MetaBundle\Entity\MetaTrait; +use ChamberOrchestra\Meta\Entity\Helper\RobotsBehaviour; +use ChamberOrchestra\Meta\Entity\MetaTrait; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping\ClassMetadata; @@ -14,7 +13,7 @@ use PHPUnit\Framework\TestCase; #[ORM\Entity] -class MetaMappingTestEntity implements MetaInterface +class MetaMappingTestEntity { use MetaTrait; diff --git a/tests/Unit/Entity/MetaTraitTest.php b/tests/Unit/Entity/MetaTraitTest.php index ef23d6b..d9f5291 100644 --- a/tests/Unit/Entity/MetaTraitTest.php +++ b/tests/Unit/Entity/MetaTraitTest.php @@ -4,18 +4,17 @@ namespace Tests\Unit\Entity; -use ChamberOrchestra\MetaBundle\Entity\Helper\RobotsBehaviour; -use ChamberOrchestra\MetaBundle\Entity\MetaInterface; -use ChamberOrchestra\MetaBundle\Entity\MetaTrait; +use ChamberOrchestra\Meta\Entity\Helper\RobotsBehaviour; +use ChamberOrchestra\Meta\Entity\MetaTrait; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\File\File; final class MetaTraitTest extends TestCase { - private function createEntity(): MetaInterface + private function createEntity(): object { - return new class implements MetaInterface { + return new class { use MetaTrait; public function setTitle(?string $title): void @@ -55,11 +54,6 @@ public function setMetaImage(?File $file): void }; } - public function testEntityImplementsMetaInterface(): void - { - self::assertInstanceOf(MetaInterface::class, $this->createEntity()); - } - public function testDefaultValues(): void { $entity = $this->createEntity(); From eb1dcb90a6cd3872f43d3774149ca20c58bd7865 Mon Sep 17 00:00:00 2001 From: Dev Date: Fri, 20 Feb 2026 00:24:16 +0000 Subject: [PATCH 4/4] update .gitignore and remove tracked agent-memory files Co-Authored-By: Claude Opus 4.6 --- .../code-improvement-reviewer/MEMORY.md | 53 -------- .../code-improvement-reviewer/patterns.md | 113 ------------------ .gitignore | 3 +- 3 files changed, 2 insertions(+), 167 deletions(-) delete mode 100644 .claude/agent-memory/code-improvement-reviewer/MEMORY.md delete mode 100644 .claude/agent-memory/code-improvement-reviewer/patterns.md diff --git a/.claude/agent-memory/code-improvement-reviewer/MEMORY.md b/.claude/agent-memory/code-improvement-reviewer/MEMORY.md deleted file mode 100644 index a3d5d85..0000000 --- a/.claude/agent-memory/code-improvement-reviewer/MEMORY.md +++ /dev/null @@ -1,53 +0,0 @@ -# meta-bundle: Code Improvement Reviewer Memory - -## Project Overview -- Package: `chamber-orchestra/meta-bundle` -- Namespace: `ChamberOrchestra\MetaBundle` (PSR-4 from root, no src/) -- PHP ^8.5, Doctrine ORM ^3.0, Symfony 8.0 -- Bundle class: `ChamberOrchestraMetaBundle`, DI extension: `ChamberOrchestraMetaExtension` - -## Key Architectural Notes (post-round-1 fixes) -- Entity layer is standalone (no CMS deps); CMS/Form layer excluded from autoload and coverage -- `MetaTrait` now uses `enumType: RobotsBehaviour::class` on SMALLINT column — native enum mapping -- `MetaTrait::robotsBehaviour` is now `protected RobotsBehaviour` (not raw int) -- `MetaTrait::getRobotsBehaviour()` returns `RobotsBehaviour` enum case (matches MetaInterface contract) -- `MetaInterface` now includes `getMetaImagePath(): ?string` and `getMeta(): array` -- `getRobotsBehaviour(): RobotsBehaviour` returns enum (not raw int) -- `getMeta()` calls `strip_tags()` on description — intentional sanitization -- `robotsBehaviour` defaults to `IndexNoFollow(1)` — deliberately conservative - -## Round 1 Issues (FIXED) -- CI cache key version mismatch — fixed (now uses `steps.setup-php.outputs.php-version`) -- AGENTS.md stale bundle class name — fixed -- MetaTrait had stray `File` import — removed -- ORM column types now use `Types::STRING` / `Types::SMALLINT` / `Types::TEXT` constants -- MetaInterface missing `getMetaImagePath()` — added -- MetaDto dead `$slug` / `$seoDescription` fields — removed -- MetaTrait now stores enum (not int) - -## Round 2 Issues Found (2026-02-17) -See patterns.md for full detail. Key items: -- `RobotsBehaviour::getFormattedBehaviour(int)` is now dead code — entity stores enum, no caller passes raw int -- `MetaInterface::getMeta()` return type is `array` with no shape documentation -- `composer.json` requires `symfony/http-foundation` for entity-layer but it is only used by CMS DTO (excluded) -- `MetaType` uses a variable `$max` shared across multiple field definitions — confusing -- `MetaType::buildForm()` `metaDescription` uses TextareaType but `attr maxlength` doesn't work on textareas in HTML -- `MetaTranslatableDto` constructor passes `MetaDto::class` hardcoded to `DtoCollection` ignoring translatable-specific DTO -- tag.yml: on the very first run from a clean repo it emits `v0.0.1` correctly; but `$major` from `cut -d. -f1` includes the `v` prefix, so subsequent tags are `v1.2.N` (correct). No bug. -- `services.yaml` body registers no services — misleading but harmless -- `phpunit.xml.dist` has no `` element with thresholds -- `CLAUDE.md` line 29 says `getRobotsBehaviour(): int` — stale after round-1 fix (now returns RobotsBehaviour) -- `OutOfBoundsException` / `ExceptionInterface` are only used by `getFormattedBehaviour(int)` — if that's removed, Exception layer becomes dead code -- `MetaDto::$robotsBehaviour` is `?int` but `RobotsBehaviour::choices()` values are also int — no type mismatch, but should be `?RobotsBehaviour` for type-safety -- `MetaInterface::getRobotsBehaviour()` is not in `getMeta()` output — robots tag is not in the returned array -- `MetaTraitTest` missing: test for `getFormattedRobotsBehaviour()` with every case (not just default+custom) -- No test for `getMeta()` including robots behaviour key (because it isn't there — by design or oversight?) - -## Testing Patterns -- Tests in `tests/Unit/`, namespace `Tests\Unit\` -- Uses anonymous class implementing `MetaInterface` + `MetaTrait` for entity tests -- PHPUnit 13.x with `#[DataProvider]` attributes -- CMS layer excluded from both autoload and coverage (external deps) - -## Links -- Detailed notes: `patterns.md` diff --git a/.claude/agent-memory/code-improvement-reviewer/patterns.md b/.claude/agent-memory/code-improvement-reviewer/patterns.md deleted file mode 100644 index 4686933..0000000 --- a/.claude/agent-memory/code-improvement-reviewer/patterns.md +++ /dev/null @@ -1,113 +0,0 @@ -# meta-bundle: Detailed Review Patterns - -## Bugs & Issues Found (2026-02-17) - -### B1 - CI cache key version mismatch (php.yml L35-37) -Cache key uses `php-8.4` but the workflow sets up PHP 8.5. Cache will never hit. - -### B2 - AGENTS.md references stale bundle class name -Line 6 says `DevMetaBundle.php` — bundle was renamed to `ChamberOrchestraMetaBundle.php`. - -### B3 - MetaTrait implicit symfony/http-foundation dependency -`MetaTrait.php` imports `Symfony\Component\HttpFoundation\File\File` at line 9. -`symfony/http-foundation` is in composer.json `require` so this is technically fine, -but the trait is documented as "standalone entity layer" — tight coupling to File is architectural noise. - -### B4 - services.yaml body is empty — no services registered -This may work (Symfony tolerates empty `services:` block) but is misleading. -Should either register form types or be conditional on CMS packages being present. - -### B5 - MetaInterface missing getMetaImagePath() -`MetaTrait` implements `getMetaImage(): ?File` and `getMetaImagePath(): ?string` -but `MetaInterface` only declares the 5 core getters (title, metaTitle, metaDescription, -metaKeywords, robotsBehaviour). `getMetaImagePath()` is missing from the interface contract. - -### B6 - MetaDto fields not mapped in MetaType -`MetaDto` has `$slug` and `$seoDescription` properties. `MetaType::buildForm()` never -adds form fields for them. These are dead DTO fields or unfinished features. - -### B7 - tag.yml auto-tagging drops the `v` prefix on all but the first tag -Line 34: `echo "next=${major}.${minor}.${next_patch}"` - `$major` is captured from -`cut -d. -f1` which would be `v0`, `v1`, etc. On the first release the tag is `v0.0.1` -(hardcoded correctly), but on subsequent releases `major=v1` so the tag becomes `v1.2.4` -(preserving prefix). Actually this is fine, but the cut on line 31-33 makes `$major` include -the `v`, which means the next tag format is correct. This is fine; no bug. - -## Code Quality Issues - -### Q1 - ORM column type uses legacy string literals -`MetaTrait.php` lines 13, 16, 27: `type: "string"` and `type: 'smallint'` -Should use `Doctrine\DBAL\Types\Types::STRING` and `Types::SMALLINT` for type safety -and IDE navigation. Doctrine deprecated raw string types in ORM 3.x. - -### Q2 - RobotsBehaviour::getFormattedBehaviour() should be an instance method -The static method takes `int $behaviour` and internally does `tryFrom()`. Since this -is an enum, this should be an instance method `getFormatted(): string` — callers -pass the enum case, not a raw int. The static form exists to bridge the raw-int -`robotsBehaviour` property in MetaTrait. If MetaTrait stored the enum case directly -this indirection would disappear. - -### Q3 - MetaTrait stores int not enum -`protected int $robotsBehaviour` instead of `protected RobotsBehaviour $robotsBehaviour`. -Doctrine ORM 3.x supports PHP enum column mapping natively. This would: -- Make `getRobotsBehaviour()` return the enum type -- Eliminate `getFormattedRobotsBehaviour()` needing to call the static bridge method -- Require changing `MetaInterface::getRobotsBehaviour()` signature - -### Q4 - Default robots behaviour is IndexNoFollow(1) — debatable default -New/draft content indexed with nofollow may not be desirable. Most CMS patterns -default to `NoIndexNoFollow` for new records. However this is a design preference. - -### Q5 - getMeta() return type is untyped array -`getMeta(): array` could be typed as `getMeta(): array{pageTitle: ?string, title: ?string, image: ?string, description: ?string, keywords: ?string}` -using PHPDoc, or as a typed struct/DTO in PHP 8.5+. - -### Q6 - MetaTrait::$metaImage is never persisted -`protected ?File $metaImage = null` is a transient property (no ORM column). -It lives alongside persisted `$metaImagePath`. The `getMetaImage()` getter is in the -trait but there is no corresponding setter in the trait — only in the CMS DTO. -This is a valid upload-handling pattern (file object is transient, path is persisted) -but it should be documented with a comment. - -### Q7 - MetaType sets maxlength=127 for title but MetaTrait column is length=255 -`MetaType.php` L42-45: maxlength is 127, Length constraint max is 127. -`MetaTrait.php` L13: column length is 255. The DB can hold 255 chars but the -form rejects anything over 127. This inconsistency means data imported or set -programmatically up to 255 chars would pass the DB layer but fail form validation. -Decide on one canonical max (e.g., 255) and align both. - -### Q8 - MetaType sets maxlength=127 for metaTitle but column is also 255 -Same issue as Q7 for `metaTitle` (L58-62 in MetaType, L16 in MetaTrait). - -### Q9 - MetaType metaDescription column is `text` type (unlimited) but form caps at 255 -`MetaTrait.php` L24: `type: 'text'` has no DB length limit. -`MetaType.php` L66-69: form caps metaDescription at 255 chars. -`MetaType.php` L71-76: same for metaKeywords but metaKeywords column is varchar(255). -For description this is likely intentional (SEO best practice: ~160 chars), but it -should be consistent. At minimum add a code comment explaining why. - -### Q10 - phpunit.xml.dist missing coverage configuration -The `` block excludes `Cms/` and `vendor/` but there is no `` element. -Running with `--coverage-html` would still work but there is no `failOnEmptyCoverage` -or minimum threshold. Consider adding `` with `processUncoveredFiles="true"`. - -## Missing Test Coverage - -### T1 - getFormattedRobotsBehaviour() with invalid stored DB value -`testGetFormattedRobotsBehaviourDefault()` and `testGetFormattedRobotsBehaviourCustom()` exist, -but there is no test for what happens when the DB contains a corrupt value (e.g., 99). -`getFormattedRobotsBehaviour()` would throw `OutOfBoundsException` — this should be tested -via `MetaTraitTest`, not just in `RobotsBehaviourTest`. - -### T2 - getMeta() strip_tags with complex HTML -`testGetMetaStripsHtmlFromDescription` only tests simple nested tags. -Missing: script tags, HTML entities, multiline HTML, empty-after-strip-tags result. - -### T3 - MetaInterface contract test -No test verifies that a class using MetaTrait actually satisfies MetaInterface. -The anonymous class in MetaTraitTest does implement MetaInterface but this is never -asserted explicitly (`self::assertInstanceOf(MetaInterface::class, $entity)`). - -### T4 - RobotsBehaviour::choices() order guarantee -`testChoicesReturnsAllCases()` checks count and specific keys but not that the -ordering matches enum declaration order, which matters for form rendering. diff --git a/.gitignore b/.gitignore index bbd4d2b..3adac4b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ /var/ composer.lock .claude/settings.local.json +.claude/agent-memory/ +.phpunit.cache/ .php-cs-fixer.cache -.phpunit.cache/ \ No newline at end of file