PHP Rules
These rules come from app/rules/php/ in ai-toolkit. They cover
the project's standards for coding style, frameworks, patterns,
security, and testing in PHP. Apply them when writing or
reviewing PHP code.
PHP Coding Style
Standards
- Follow PSR-12 extended coding style.
- Use
declare(strict_types=1)at the top of every file. - Use PHP 8.1+ features: enums, fibers, readonly properties, intersection types.
- Use PHP CS Fixer or Pint for automated formatting.
Naming
- PascalCase: classes, interfaces, traits, enums.
- camelCase: methods, functions, variables.
- UPPER_SNAKE: class constants (
public const MAX_RETRIES = 3). - snake_case: not used for methods. PSR convention is camelCase.
- Suffix interfaces with
Interfaceor prefix with contract name (project convention).
Type System
- Use typed properties:
private readonly string $name;. - Use union types:
string|int. Use intersection types:Countable&Iterator. - Use
enum(PHP 8.1) for fixed sets of values. Use backed enums for persistence. - Use
readonlyclasses (PHP 8.2) for immutable DTOs. - Use constructor promotion:
public function __construct(private string $name). - Use
neverreturn type for functions that throw or exit.
Functions
- Use typed parameters and return types on all functions/methods.
- Use named arguments for readability:
new User(name: 'Ada', age: 36). - Use null-safe operator:
$user?->address?->city. - Use match expression over switch for value mapping.
- Use first-class callable syntax:
array_map($this->transform(...), $items).
Imports and Namespaces
- Use PSR-4 autoloading via Composer.
- Group
usestatements: classes, functions, constants. - Never use
require/includefor class loading. Use Composer autoloader. - Use one class per file. File name matches class name.
Error Handling
- Use exceptions for error conditions. Never return error codes.
- Create domain exception hierarchies extending
RuntimeExceptionorLogicException. - Use
matchwiththrowfor exhaustive error mapping. - Log exceptions with context using PSR-3 logger.
Configuration
- Use PHPStan at level 8+ for static analysis.
- Use Rector for automated code upgrades and refactoring.
- Use
.php-cs-fixer.dist.phpfor formatting rules. - Run
composer analyse(PHPStan) andcomposer format(Pint) in CI.
PHP Frameworks
Laravel
- Use route model binding:
Route::get('/users/{user}', ...). - Use Form Requests for validation:
class StoreUserRequest extends FormRequest. - Use Eloquent scopes for reusable query constraints:
scopeActive(). - Use API Resources for response transformation:
UserResource::collection($users). - Use
config()helper for configuration. Never accessenv()outside config files. - Use middleware groups for auth, throttling, and CORS.
Eloquent ORM
- Use relationships:
hasMany,belongsTo,belongsToMany,morphMany. - Use eager loading:
User::with('posts.comments')->get()to prevent N+1. - Use
$fillableor$guardedon models. Prefer$fillable(explicit whitelist). - Use model events or observers for lifecycle hooks.
- Use
upsert()for bulk insert-or-update operations. - Use
cursor()for memory-efficient iteration over large result sets.
Symfony
- Use attributes for route definitions:
#[Route('/api/users', methods: ['GET'])]. - Use autowiring for dependency injection. Register services in
services.yaml. - Use Symfony Forms for complex validation and data mapping.
- Use Messenger component for async message handling (commands, events).
- Use Doctrine ORM with repository pattern and query builders.
Doctrine ORM
- Use entity classes with annotations or attributes for mapping.
- Use repositories for data access:
$em->getRepository(User::class). - Use DQL for type-safe queries. Use QueryBuilder for dynamic queries.
- Use migrations:
bin/console doctrine:migrations:diffandmigrate. - Use lifecycle callbacks (
@PrePersist,@PostUpdate) for entity events.
Symfony Serializer
- Default behavior uses property names as-is. Combined with PSR-12
camelCaseproperty names, JSON output iscamelCasewith zero configuration. - Avoid adding
api_platform.name_converter: CamelCaseToSnakeCaseNameConverterglobally. Known side-effect (api-platform/core #6101): overrides the project-wideMetadataAwareNameConverter, affecting Messenger serializers, custom normalizers, and CLI JSON output — not just the HTTP API. - Use
#[SerializedName]only when justified: legacy field alias during rename, external contract mapping, ObjectNormalizer cross-version stabilization. Community practice (Symfony docs, Sylius, SymfonyCasts): prefer clean property/getter naming over aliases. When using, document the reason next to the attribute. - Symfony 7.3.5+
ObjectNormalizerproducesisActivenatively for aisActive(): boolgetter (symfony/symfony #62353). Older#[SerializedName('isActive')]aliases added for pre-7.3.5ObjectNormalizer(which producedactive) are redundant after upgrade — remove them. - Avoid duplicate getters like
isActive()+getIsActive()on the same property —ObjectNormalizertreats them as two fields and serializes ambiguously. Keep one (isXxx()for booleans,getXxx()otherwise).
API Platform
- Use API Platform for rapid REST/GraphQL API generation from entities.
- Use
#[ApiResource]attribute for automatic CRUD endpoint generation. - Use custom state providers and processors for business logic.
- Use serialization groups for controlling response shape.
- Use filters for query parameter support: pagination, search, ordering.
- Property names on
ApiResourceDTOs drive JSON keys directly (see Symfony Serializer above). Write them incamelCase— that is both the Symfony default and the dominant JSON API convention. - Use
operation_nameinextraPropertiesfor dispatch metadata (e.g.,extraProperties: ['operation_name' => 'club_activate']). The keyoperation_nameand itssnake_casevalues are framework metadata, not JSON wire keys — keeping themsnake_caseis expected.
Livewire (Laravel)
- Use Livewire components for reactive UI without JavaScript.
- Use
wire:modelfor two-way data binding on form inputs. - Use
$rulesproperty for inline validation on component properties. - Use component actions for server-side event handling.
- Use
wire:loadingfor loading state indicators.
Queues and Workers
- Use Laravel Horizon for Redis queue monitoring and management.
- Use Symfony Messenger with transports (Redis, AMQP, Doctrine).
- Use dead letter queues for failed job inspection and replay.
- Use rate limiting on queue workers to prevent downstream overload.
PHP Patterns
Error Handling
- Use custom exception hierarchies:
class DomainException extends RuntimeException. - Add context to exceptions:
throw new UserNotFoundException(userId: $id). - Use
matchwithdefault => throwfor exhaustive error mapping. - Use
set_exception_handler()for global uncaught exception handling. - Log exceptions with PSR-3 logger and structured context.
Enums and Value Objects
- Use backed enums for database-persisted values:
enum Status: string. - Use
from()for strict conversion,tryFrom()for nullable safe conversion. - Implement methods on enums for behavior:
public function label(): string. - Use readonly classes for value objects:
readonly class Money { ... }. - Use constructor promotion for concise value object definitions.
Repository Pattern
- Abstract data access behind repository interfaces.
- Repositories return domain entities, not Eloquent models or arrays.
- Use constructor injection for repository dependencies.
- Use specifications or criteria objects for complex query building.
- Keep repository methods focused: one query per method.
Service Layer
- Use service classes for business logic. Keep controllers thin.
- Use action classes (single-method services) for discrete operations.
- Use DTOs for data transfer between layers. Never pass request objects to services.
- Use command/query separation: commands mutate, queries read.
- Inject dependencies via constructor. Never use
app()helper in services.
Collections and Iterators
- Use Laravel Collections or standalone
illuminate/collectionsfor data manipulation. - Chain
map(),filter(),reduce()for declarative data transformation. - Use
LazyCollectionfor memory-efficient processing of large datasets. - Use generators (
yield) for lazy iteration over large result sets. - Prefer
collect()pipeline over nested loops.
Async Patterns
- Use Laravel Queues for background job processing.
- Use
dispatch()for fire-and-forget. UseBus::chain()for sequential jobs. - Use
ShouldQueueinterface on jobs, listeners, and mailables. - Set
$tries,$timeout,$backoffon job classes. - Use
batch()for parallel job execution with completion callback.
Event-Driven
- Use events and listeners for decoupled side effects.
- Use domain events for cross-boundary communication.
- Use
ShouldQueueon listeners for async event handling. - Use event subscribers for grouping related listeners.
- Keep event payloads minimal: IDs and timestamps, not full objects.
Anti-Patterns
- Fat controllers: move logic to services/actions.
- God models: split into focused models with traits or separate classes.
- Using
DB::raw()without parameterization: SQL injection risk. - Static method calls for testable dependencies: use DI instead.
- Returning mixed types: use typed returns or Result objects.
PHP Security
SQL Injection
- Use PDO prepared statements with bound parameters for all queries.
- Use Eloquent/Doctrine ORM for type-safe query building.
- Never concatenate user input into SQL strings. Never use
DB::raw($input). - Use
whereIn()with arrays, not string interpolation for IN clauses. - Audit raw queries:
DB::select(DB::raw(...))must use?placeholders.
XSS Prevention
- Blade templates auto-escape with
{{ }}. Never use{!! !!}with user data. - Use
htmlspecialchars()withENT_QUOTESwhen outputting outside Blade. - Set
Content-Security-Policyheaders to restrict inline scripts. - Sanitize rich-text input with HTMLPurifier before storage.
- Use
strip_tags()only as a secondary measure, not primary defense.
CSRF Protection
- Use
@csrfdirective in all Blade forms. - Use
VerifyCsrfTokenmiddleware (enabled by default in Laravel). - Use
X-CSRF-TOKENheader for AJAX requests from SPA frontends. - Exclude only webhook endpoints from CSRF verification (with careful validation).
Authentication
- Use
password_hash()withPASSWORD_ARGON2IDorPASSWORD_BCRYPT. - Use Laravel Sanctum for SPA/mobile API authentication.
- Use Laravel Passport for full OAuth2 server implementation.
- Implement rate limiting on login endpoints:
ThrottleRequestsmiddleware. - Use multi-factor authentication for admin accounts.
Authorization
- Use Laravel Gates and Policies for authorization logic.
- Use
$this->authorize('update', $post)in controllers. - Check resource ownership in policies, not just role membership.
- Default deny: use
Gate::before()for super-admin bypass, nothing else. - Use middleware
can:permissionfor route-level authorization.
File Upload
- Validate file MIME type server-side. Do not trust
Content-Typeheader. - Store uploads outside the web root. Use
storage/withStorage::disk(). - Generate random filenames. Never use original user-provided filenames.
- Set maximum file size limits in validation and PHP
upload_max_filesize. - Scan uploaded files for malware in production environments.
Mass Assignment
- Use
$fillableon Eloquent models. Never use$guarded = []. - Use Form Requests to whitelist fields before model assignment.
- Use DTOs for data transfer. Never pass
$request->all()tocreate(). - Audit
forceFill()andforceCreate()usage (bypasses guarding).
Secrets and Configuration
- Use
.envfiles for local secrets. Use Vault or SSM for production. - Never commit
.envto version control. Commit.env.exampleas template. - Use
config()helper, neverenv()outside of config files (caching issue). - Never log request content containing passwords or tokens.
- Use
APP_DEBUG=falsein production. Debug mode leaks sensitive data.
PHP Testing
Framework
- Use PHPUnit 10+ as the primary test framework.
- Use Pest PHP for expressive, minimal-boilerplate testing (built on PHPUnit).
- Use Mockery for flexible mocking. Use PHPUnit built-in mocks for simple cases.
- Use Testcontainers (via Docker) for integration tests with databases.
File Naming
- Test files:
FooTest.phpintests/mirroringsrc/namespace structure. - Unit tests:
tests/Unit/. Integration tests:tests/Integration/ortests/Feature/. - PHPUnit config:
phpunit.xml.distat project root. - Use
@groupannotations for test categorization.
Structure (PHPUnit)
- Use
#[Test]attribute (PHP 8) ortestprefix for test methods. - Use
setUp()/tearDown()for per-test initialization and cleanup. - Use
#[DataProvider('dataMethodName')]for parameterized tests. - Name tests:
testMethodName_Scenario_ExpectedResultor descriptive snake_case.
Structure (Pest)
- Use
test('description', function () { ... })for test cases. - Use
it('should do something', ...)for BDD-style descriptions. - Use
beforeEach()/afterEach()for setup and teardown. - Use
dataset()for shared test data across multiple tests. - Use
->with([...])for inline parameterized tests.
Assertions
- Use
$this->assertSame()for strict equality (type + value). - Use
$this->assertInstanceOf(Foo::class, $result)for type checks. - Use
$this->expectException(FooException::class)before the throwing call. - Use
$this->assertCount(),$this->assertContains()for collections. - Pest: use
expect($value)->toBe(),->toBeInstanceOf(),->toThrow().
Mocking (Mockery)
- Create mocks:
$mock = Mockery::mock(UserRepository::class). - Stub:
$mock->shouldReceive('find')->with(1)->andReturn($user). - Verify:
$mock->shouldHaveReceived('save')->once(). - Use
Mockery::close()intearDown()orafterEach(). - Use
spy()to verify interactions without stubbing.
Laravel Testing
- Use
RefreshDatabasetrait for database test isolation. - Use
$this->actingAs($user)for authenticated request testing. - Use
$this->getJson('/api/users')->assertOk()->assertJsonCount(3). - Use factories:
User::factory()->create()for test data. - Use
Bus::fake(),Event::fake(),Mail::fake()for side-effect assertion.
Best Practices
- Test behavior, not implementation. Do not test private methods.
- Use in-memory SQLite for fast database tests when schema is compatible.
- Run
php artisan test --parallelfor faster Laravel test execution. - Use
--coverage-htmlfor visual coverage reports. - Keep tests fast: mock external HTTP calls with
Http::fake().