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/.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. 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/.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 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 @@ -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), - ], - ]); - } -} diff --git a/DependencyInjection/ChamberOrchestraMetaExtension.php b/DependencyInjection/ChamberOrchestraMetaExtension.php deleted file mode 100644 index a8cd2db..0000000 --- a/DependencyInjection/ChamberOrchestraMetaExtension.php +++ /dev/null @@ -1,15 +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();