Every line of code in this project MUST follow these principles:
// YES — reads like English
$page = Page::with('blocks')->published()->findOrFail($id);
// NO — verbose, mechanical
$page = Page::where('status', '=', 'published')
->where('id', '=', $id)
->with(['blocks'])
->firstOrFail();- Use Eloquent scopes for reusable query logic
- Use accessors and mutators for data transformation
- Use API Resources for JSON serialization — never return raw models
- Prefer fluent interfaces and method chaining
- Follow Laravel's default directory structure — do NOT invent custom layouts
- Use
artisan make:*generators for all scaffolding - Use Route Model Binding — never manually find models in controllers
- Use Form Requests for validation — never validate in controllers
- Use Resource Controllers with standard method names (index, store, show, update, destroy)
- Use snake_case for database columns, camelCase for JSON API output (via Resources)
// Controller — thin, delegates everything
class PageController extends Controller
{
public function store(StorePageRequest $request)
{
$page = Page::create($request->validated());
return new PageResource($page->load('blocks'));
}
}- Controllers: receive request, delegate, return response. Nothing else.
- Models: relationships, scopes, accessors, business logic
- Services: complex business logic that spans multiple models
- Form Requests: validation rules and authorization
- Resources: JSON transformation
Always prefer Laravel's built-in solutions over third-party packages:
| Need | Use | NOT |
|---|---|---|
| Validation | Form Requests | manual validation |
| JSON output | API Resources | ->toArray() |
| Auth | Sanctum (API) + Session (web) | JWT packages |
| File storage | Storage facade | raw filesystem |
| Cache | Cache facade | manual caching |
| Queue jobs | Laravel Queues | raw process spawning |
| Events | Laravel Events/Listeners | custom pub/sub |
| Testing | PHPUnit + Laravel TestCase | external testing libs |
| DB queries | Eloquent + Query Builder | raw SQL |
| Rate limiting | RateLimiter facade | custom middleware |
// Every feature gets a test. No exceptions.
test('can create a page', function () {
$response = $this->postJson('/api/v1/pages', [
'title' => 'Home',
'slug' => 'home',
]);
$response->assertCreated()
->assertJsonPath('data.title', 'Home');
$this->assertDatabaseHas('pages', ['slug' => 'home']);
});- Use Pest PHP for tests (Taylor's preferred testing framework)
- Feature tests for every API endpoint
- Unit tests for models, services, validators
- Use factories and seeders for test data
- Run
php artisan testbefore every commit
- Table prefix:
cms_for all tables - Use
bigIncrements('id')for primary keys - Use
timestamps()on all tables - Use
jsoncolumns for flexible structured data (FieldSchema, content, meta) - Use
enumcolumns sparingly — prefer string with validation - Use foreign key constraints with
cascadeOnDelete()where appropriate - Migrations must be idempotent — check before altering
// Single resource
{
"data": {
"id": 1,
"title": "Home",
"slug": "home",
"blocks": [...]
}
}
// Collection
{
"data": [...],
"meta": {
"current_page": 1,
"per_page": 20,
"total": 45,
"last_page": 3
}
}
// Error
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Content validation failed",
"details": [...]
}
}- All responses wrapped in
datakey - Pagination metadata in
metakey - Consistent error format with error codes
- Use HTTP status codes correctly (201 Created, 204 No Content, 422 Validation Error)
- PSR-12 coding standard
- Strict types declaration in every PHP file:
declare(strict_types=1); - Use PHP 8.4 features: readonly properties, enums, match expressions, named arguments, first-class callables
- Use constructor property promotion
- Short closures (
fn()) where appropriate - No unnecessary comments — code should be self-documenting
- PHPDoc only for complex return types or when adding context beyond the type system