Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Symfony Bundle
symkit/metadata-bundle
cursor
symfony-bundle
abstract-bundle
solid
contract-pattern
dependency-injection
final-readonly
naming-conventions

Symfony bundle conventions, SOLID principles, Contract pattern, final readonly architecture

Install
php artisan boost:add-skill symkit/metadata-bundle

Save this content to: .cursor/rules/symfony-bundle.mdc

# Symfony Bundle Conventions

## Architecture

- `final readonly class` for all services, handlers, processors, voters, listeners, DTOs
- Exceptions: Doctrine entities (not `readonly`), bundle class (not `final`), classes documented for inheritance
- Extension points: interfaces, events, compiler passes, decorators — never inheritance

## AbstractBundle (Symfony 7+/8+)

- Always extend `AbstractBundle`, never `Bundle` with separate Extension/Configuration classes
- `configure()` for config tree, `loadExtension()` for container loading, `prependExtension()` for prepending other bundles
- `DependencyInjection/` folder is unnecessary with AbstractBundle — all DI logic lives in the bundle class
- `$this->getPath()` for bundle root path (replaces `\dirname(__DIR__, 2)`)
- Translations auto-discovered from `<BundlePath>/translations/` (XLIFF format)

## SOLID

- **S**: One class, one responsibility. No "god service".
- **O**: Extensible via events/interfaces/decorators. `final readonly` forces composition.
- **L**: Interfaces are substitutable. No `instanceof` on concrete implementations.
- **I**: Segregated interfaces. One interface = one precise contract.
- **D**: Inject interfaces, never concrete implementations.

## Contract Pattern

- `src/Contract/`: public interfaces of the bundle (BC-safe API)
- Services implement Contract interfaces. Host app type-hints on interfaces.
- Only `Contract/`, events, and DTOs are part of the public semver API.

## Naming

- Bundle class: `{Vendor}{Name}Bundle`, namespace `{Vendor}\{Name}Bundle`
- Config alias (auto-derived, snake_case): service prefix `alias.`, route prefix `alias_`
- Translation domain: `{Vendor}{Name}Bundle` in XLIFF

## Structure

- `src/Contract/`, `src/Event/` always present when applicable
- XML for service definitions when needed (no Yaml dependency). Defaults: `autowire=true autoconfigure=true public=false`
- `translations/` at bundle root for XLIFF files (auto-discovered by Symfony)
- Never extend `AbstractController` — inject specific services

## README

The `README.md` must always include the following badges at the top (replace `{vendor}/{repo}` with the actual repository name):

```markdown
[![CI](https://github.com/{vendor}/{repo}/actions/workflows/ci.yml/badge.svg)](https://github.com/{vendor}/{repo}/actions)
[![Latest Version](https://img.shields.io/packagist/v/{vendor}/{repo}.svg)](https://packagist.org/packages/{vendor}/{repo})
[![PHPStan Level 9](https://img.shields.io/badge/PHPStan-level%209-brightgreen.svg)](https://phpstan.org/)
```

## Services (contexte bundle)

- Dans un bundle, **ne pas utiliser les attributs** pour déclarer les services (tags, listeners, composants Twig, etc.).
- Tout enregistrement de services, tags et options doit se faire **en configuration** : dans `loadExtension()` du bundle (AbstractBundle) ou dans les fichiers XML de définition de services.
- Exemples à éviter dans le code du bundle : `#[AsEventListener]`, `#[AsTwigComponent]`, `#[AsLiveComponent]`, tags sur les classes. À la place : appels à `$container->register()`, `->addTag()`, et déclarations explicites dans l’extension ou le XML.

## Dependencies

- Symfony packages: always `^7.0 || ^8.0` (semver, never `7.*|8.*`)
- Only declare actual dependencies. Optional deps in `suggest`.
- Never embed vendor code in `src/`
- Composer `allow-plugins`: configure `infection/extension-installer` and `phpro/grumphp` in `composer.json`

## Configurability

Every feature of a bundle must be **activatable, deactivatable, and configurable** via the bundle's `configure()` tree. The host app must be able to:

- Enable/disable any feature (Doctrine, controllers, Twig components, listeners, etc.) via a boolean config key
- Override Doctrine **entity classes** and **repository classes** in config — the bundle must never hardcode its own entity/repository classes in services or queries; always resolve them from configuration
- Customize any default value (table prefix, translation domain, route prefix, etc.)

Example pattern in `configure()`:

```php
->arrayNode('entity')
    ->addDefaultsIfNotSet()
    ->children()
        ->scalarNode('article_class')->defaultValue(Article::class)->end()
        ->scalarNode('article_repository_class')->defaultValue(ArticleRepository::class)->end()
    ->end()
->end()
```

Then in `loadExtension()`, bind these classes as parameters and inject them into services. Never use `Article::class` directly in service code — always use the configured class.

## Translations

Every user-facing string must be translated — no hardcoded text in templates, controllers, forms, validators, or flash messages.

- Extract **all** strings: Twig templates (`{% trans %}`, `|trans`), form labels/help/placeholders, constraint messages, controller flash messages, error pages, email subjects/bodies
- XLIFF format in `translations/` at bundle root, domain `{Vendor}{Name}Bundle`
- Always provide at least `en` and `fr` locales
- Run `translation:extract` regularly to catch missing keys
- Form labels: always explicit `'label' => 'form.field_name'`, never rely on auto-generated labels
- Validator messages: always use translation keys (`'message' => 'validation.field.constraint'`)

## Anti-Patterns

- Using `Bundle` instead of `AbstractBundle` with separate Extension/Configuration classes
- **Using attributes for service/tag registration in bundle code** — use config/loadExtension() instead
- `new Configuration()` or `new Service()` — always use DI or AbstractBundle built-in methods
- Public services by default, missing service ID prefix, YAML services
- Setter injection, business logic in controller/entity
- Hardcoded parameters instead of `configure()` + env vars
- Symfony constraints `7.*|8.*` instead of `^7.0 || ^8.0`
- Hardcoded entity/repository classes — always configurable via bundle config
- Non-translatable user-facing strings — everything must go through the translation system
- Features that cannot be disabled — every feature must have a config toggle

## Commits

When committing, never reference the agent or any co-author (no Co-authored-by, Signed-off-by from the AI, or similar). Commit messages and metadata must describe only the change, with no self-attribution. Never use `git commit --trailer` or add Co-authored-by to commits.

## Cleanup — Legacy commit-msg hook

If the project contains any of the following legacy artefacts, remove them:

- `scripts/git-hooks/commit-msg` — delete the file (and `scripts/git-hooks/` directory if empty)
- `install-hooks` target in the Makefile — remove the target and its `.PHONY` entry
- Any `make install-hooks` mention in README or other documentation

These artefacts are obsolete and must be cleaned up when encountered.

## After Every Change — Makefile First

Always use Makefile targets. Never call `vendor/bin/` directly.

- `make cs-fix` — fix code style
- `make phpstan` — static analysis
- `make lint` — composer validate --strict
- `make test` — run tests
- `make quality` — full pipeline (cs-check + phpstan + deptrac + lint + test + infection)
- `make ci` — security-check + quality

## Available Skills

- `symfony-bundle-core`: architecture, scaffolding
- `symfony-bundle-ux`: AssetMapper, Stimulus, Twig/Live Components
- `symfony-bundle-doctrine`: entities, XML mapping
- `symfony-bundle-flex`: Flex recipes
- `symfony-bundle-quality`: Makefile, PHPStan, GrumPHP, Infection, Deptrac, CI

Symfony Bundle Conventions

Architecture

  • final readonly class for all services, handlers, processors, voters, listeners, DTOs
  • Exceptions: Doctrine entities (not readonly), bundle class (not final), classes documented for inheritance
  • Extension points: interfaces, events, compiler passes, decorators — never inheritance

AbstractBundle (Symfony 7+/8+)

  • Always extend AbstractBundle, never Bundle with separate Extension/Configuration classes
  • configure() for config tree, loadExtension() for container loading, prependExtension() for prepending other bundles
  • DependencyInjection/ folder is unnecessary with AbstractBundle — all DI logic lives in the bundle class
  • $this->getPath() for bundle root path (replaces \dirname(__DIR__, 2))
  • Translations auto-discovered from <BundlePath>/translations/ (XLIFF format)

SOLID

  • S: One class, one responsibility. No "god service".
  • O: Extensible via events/interfaces/decorators. final readonly forces composition.
  • L: Interfaces are substitutable. No instanceof on concrete implementations.
  • I: Segregated interfaces. One interface = one precise contract.
  • D: Inject interfaces, never concrete implementations.

Contract Pattern

  • src/Contract/: public interfaces of the bundle (BC-safe API)
  • Services implement Contract interfaces. Host app type-hints on interfaces.
  • Only Contract/, events, and DTOs are part of the public semver API.

Naming

  • Bundle class: {Vendor}{Name}Bundle, namespace {Vendor}\{Name}Bundle
  • Config alias (auto-derived, snake_case): service prefix alias., route prefix alias_
  • Translation domain: {Vendor}{Name}Bundle in XLIFF

Structure

  • src/Contract/, src/Event/ always present when applicable
  • XML for service definitions when needed (no Yaml dependency). Defaults: autowire=true autoconfigure=true public=false
  • translations/ at bundle root for XLIFF files (auto-discovered by Symfony)
  • Never extend AbstractController — inject specific services

README

The README.md must always include the following badges at the top (replace {vendor}/{repo} with the actual repository name):

[![CI](https://github.com/{vendor}/{repo}/actions/workflows/ci.yml/badge.svg)](https://github.com/{vendor}/{repo}/actions)
[![Latest Version](https://img.shields.io/packagist/v/{vendor}/{repo}.svg)](https://packagist.org/packages/{vendor}/{repo})
[![PHPStan Level 9](https://img.shields.io/badge/PHPStan-level%209-brightgreen.svg)](https://phpstan.org/)

Services (contexte bundle)

  • Dans un bundle, ne pas utiliser les attributs pour déclarer les services (tags, listeners, composants Twig, etc.).
  • Tout enregistrement de services, tags et options doit se faire en configuration : dans loadExtension() du bundle (AbstractBundle) ou dans les fichiers XML de définition de services.
  • Exemples à éviter dans le code du bundle : #[AsEventListener], #[AsTwigComponent], #[AsLiveComponent], tags sur les classes. À la place : appels à $container->register(), ->addTag(), et déclarations explicites dans l’extension ou le XML.

Dependencies

  • Symfony packages: always ^7.0 || ^8.0 (semver, never 7.*|8.*)
  • Only declare actual dependencies. Optional deps in suggest.
  • Never embed vendor code in src/
  • Composer allow-plugins: configure infection/extension-installer and phpro/grumphp in composer.json

Configurability

Every feature of a bundle must be activatable, deactivatable, and configurable via the bundle's configure() tree. The host app must be able to:

  • Enable/disable any feature (Doctrine, controllers, Twig components, listeners, etc.) via a boolean config key
  • Override Doctrine entity classes and repository classes in config — the bundle must never hardcode its own entity/repository classes in services or queries; always resolve them from configuration
  • Customize any default value (table prefix, translation domain, route prefix, etc.)

Example pattern in configure():

->arrayNode('entity')
    ->addDefaultsIfNotSet()
    ->children()
        ->scalarNode('article_class')->defaultValue(Article::class)->end()
        ->scalarNode('article_repository_class')->defaultValue(ArticleRepository::class)->end()
    ->end()
->end()

Then in loadExtension(), bind these classes as parameters and inject them into services. Never use Article::class directly in service code — always use the configured class.

Translations

Every user-facing string must be translated — no hardcoded text in templates, controllers, forms, validators, or flash messages.

  • Extract all strings: Twig templates ({% trans %}, |trans), form labels/help/placeholders, constraint messages, controller flash messages, error pages, email subjects/bodies
  • XLIFF format in translations/ at bundle root, domain {Vendor}{Name}Bundle
  • Always provide at least en and fr locales
  • Run translation:extract regularly to catch missing keys
  • Form labels: always explicit 'label' => 'form.field_name', never rely on auto-generated labels
  • Validator messages: always use translation keys ('message' => 'validation.field.constraint')

Anti-Patterns

  • Using Bundle instead of AbstractBundle with separate Extension/Configuration classes
  • Using attributes for service/tag registration in bundle code — use config/loadExtension() instead
  • new Configuration() or new Service() — always use DI or AbstractBundle built-in methods
  • Public services by default, missing service ID prefix, YAML services
  • Setter injection, business logic in controller/entity
  • Hardcoded parameters instead of configure() + env vars
  • Symfony constraints 7.*|8.* instead of ^7.0 || ^8.0
  • Hardcoded entity/repository classes — always configurable via bundle config
  • Non-translatable user-facing strings — everything must go through the translation system
  • Features that cannot be disabled — every feature must have a config toggle

Commits

When committing, never reference the agent or any co-author (no Co-authored-by, Signed-off-by from the AI, or similar). Commit messages and metadata must describe only the change, with no self-attribution. Never use git commit --trailer or add Co-authored-by to commits.

Cleanup — Legacy commit-msg hook

If the project contains any of the following legacy artefacts, remove them:

  • scripts/git-hooks/commit-msg — delete the file (and scripts/git-hooks/ directory if empty)
  • install-hooks target in the Makefile — remove the target and its .PHONY entry
  • Any make install-hooks mention in README or other documentation

These artefacts are obsolete and must be cleaned up when encountered.

After Every Change — Makefile First

Always use Makefile targets. Never call vendor/bin/ directly.

  • make cs-fix — fix code style
  • make phpstan — static analysis
  • make lint — composer validate --strict
  • make test — run tests
  • make quality — full pipeline (cs-check + phpstan + deptrac + lint + test + infection)
  • make ci — security-check + quality

Available Skills

  • symfony-bundle-core: architecture, scaffolding
  • symfony-bundle-ux: AssetMapper, Stimulus, Twig/Live Components
  • symfony-bundle-doctrine: entities, XML mapping
  • symfony-bundle-flex: Flex recipes
  • symfony-bundle-quality: Makefile, PHPStan, GrumPHP, Infection, Deptrac, CI
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings