php artisan boost:add-skill symkit/form-bundle
Save this content to: .cursor/rules/php-bundle-code.mdc
# PHP Bundle Code Style
## Mandatory Header
Every PHP file starts with `declare(strict_types=1)`.
## Classes
- `final readonly class` for: services, handlers, processors, voters, listeners, DTOs
- Exceptions: Doctrine entities (not `readonly`), bundle class (not `final`), documented inheritance
- Constructor injection only with promoted properties (`private readonly`)
## AbstractBundle Pattern
The bundle class extends `AbstractBundle` (never `Bundle`):
```php
class VendorNameBundle extends AbstractBundle
{
public function configure(DefinitionConfigurator $definition): void { ... }
/** @param array{key: type, ...} $config */
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { ... }
public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void { ... }
}
```
- `loadExtension()` receives processed config: always add `@param array{...}` PHPDoc for PHPStan level 9
- No separate `Configuration.php` or `*Extension.php` — everything in the bundle class
- Use `$this->getPath()` for paths, never `\dirname(__DIR__, N)`
## Typing
- All parameters, return types, and properties must be typed
- No `mixed` except with a comment justifying why
- PHPStan level 9: use `@param array{key: type}` for shaped arrays (especially `loadExtension()`)
- Doctrine collections: `/** @var Collection<int, Entity> */`
- Repository returns: `@return Entity[]` or exact union type, never `mixed[]`
## Language Features
- PHP 8 attributes for routing/validation: `#[Route]`, `#[Assert\...]`. Never `@annotations`.
- **No attributes for service/tag registration** (`#[AsEventListener]`, `#[AutoconfigureTag]`, `#[AsTwigComponent]`, etc.) — in a bundle, all service registration goes through `loadExtension()` or XML config (see symfony-bundle rule).
- Native function prefix: `\count()`, `\sprintf()`, `\in_array()`, `\array_map()`
- `match` over `switch` when applicable
- Early return to reduce nesting
## Dependency Injection
- Constructor injection only. No setter, no property injection.
- Never `new Service()` — always DI
- No hardcoded parameters — use `configure()` + env vars
- Prefix service IDs and interface aliases with bundle alias
- `registerForAutoconfiguration()` for custom interfaces
## Forbidden
- `Bundle` with `DependencyInjection/Extension.php` and `Configuration.php` (use `AbstractBundle`)
- `new Configuration()` in extension classes
- Business logic in controllers or entities
- `@phpstan-ignore` without explanatory comment
- Implicit `void` return types
- Anonymous classes for services (untestable)
## After Every Change
Run `make cs-fix` then `make phpstan` to validate style and types.
Every PHP file starts with declare(strict_types=1).
final readonly class for: services, handlers, processors, voters, listeners, DTOsreadonly), bundle class (not final), documented inheritanceprivate readonly)The bundle class extends AbstractBundle (never Bundle):
class VendorNameBundle extends AbstractBundle
{
public function configure(DefinitionConfigurator $definition): void { ... }
/** @param array{key: type, ...} $config */
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { ... }
public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void { ... }
}
loadExtension() receives processed config: always add @param array{...} PHPDoc for PHPStan level 9Configuration.php or *Extension.php — everything in the bundle class$this->getPath() for paths, never \dirname(__DIR__, N)mixed except with a comment justifying why@param array{key: type} for shaped arrays (especially loadExtension())/** @var Collection<int, Entity> */@return Entity[] or exact union type, never mixed[]#[Route], #[Assert\...]. Never @annotations.#[AsEventListener], #[AutoconfigureTag], #[AsTwigComponent], etc.) — in a bundle, all service registration goes through loadExtension() or XML config (see symfony-bundle rule).\count(), \sprintf(), \in_array(), \array_map()match over switch when applicablenew Service() — always DIconfigure() + env varsregisterForAutoconfiguration() for custom interfacesBundle with DependencyInjection/Extension.php and Configuration.php (use AbstractBundle)new Configuration() in extension classes@phpstan-ignore without explanatory commentvoid return typesRun make cs-fix then make phpstan to validate style and types.
How can I help you explore Laravel packages today?