Claude Code for PHP — Laravel, Symfony & WordPress Workflows
PHP powers over 75% of the web. This guide shows how to configure Claude Code for modern PHP development — Laravel Artisan workflows, Pest testing, Eloquent migrations, Symfony console commands, and WordPress plugin development — using CLAUDE.md and automated hooks.
CLAUDE.md Template for Laravel Projects
# CLAUDE.md — Laravel Application
## Key commands
- php artisan serve # Start dev server
- php artisan test # Run PHPUnit suite
- ./vendor/bin/pest --dirty # Run Pest on changed files
- php artisan migrate # Run pending migrations
- php artisan migrate:rollback # Rollback last migration batch
- php artisan make:model Product -mfsc # Model + migration + factory + seeder + controller
- ./vendor/bin/pint # Laravel Pint code style fixer
## PHP / Laravel version
- PHP 8.3 with declare(strict_types=1) in every file
- Laravel 12.x
- Use readonly classes/properties for DTOs and value objects
- Use PHP 8.x enums for status fields (not string constants)
- Constructor property promotion preferred
## Architecture
- Thin controllers — business logic in Action classes (app/Actions/)
- Form Request classes for all validation — never validate in controller
- Resource classes for API responses — never return raw Eloquent models
- Policies for authorization — never check permissions inline
- No facades in tests — use dependency injection
## Database
- Eloquent ORM with zero-downtime migrations only
- Never DROP COLUMN directly — use expand/contract pattern
- Use database transactions for multi-step writes
- N+1 prevention: always eager-load with() for related models
## Testing
- Test framework: Pest PHP
- Feature tests for API endpoints (use RefreshDatabase)
- Unit tests for Action classes and domain logic
- Use factories — no raw DB::table() inserts in tests
Automated Hooks for PHP
# .claude/settings.json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "./vendor/bin/pest --dirty 2>&1 | tail -30"
},
{
"type": "command",
"command": "./vendor/bin/pint --dirty 2>&1 | head -20"
}
]
}
]
}
}
Pest's
--dirty flag uses git to detect which test files are affected by your current changes and only runs those. On large Laravel apps this drops test time from 30s to under 3s for targeted edits.Laravel Workflow Prompts
| Goal | Prompt to Claude Code |
|---|---|
| New API endpoint | "Add POST /api/v1/products. Validate with ProductStoreRequest (name required max:255, price numeric min:0.01, category_id exists:categories,id). Store with CreateProductAction. Return ProductResource. Include Pest feature test for 201, 422, and 403 cases." |
| Eloquent relationship + N+1 fix | "Order hasMany OrderItems, each belongsTo Product. Update OrderController@index to eager-load items.product. Add Pest test that asserts only 2 queries are executed." |
| Zero-downtime migration | "Add 'notes' column to orders table. Nullable varchar(2000). Migration must not lock the table — use ADD COLUMN with DEFAULT null on PostgreSQL." |
| Queue job | "Create a SendOrderConfirmationEmail job. Dispatched after order created. Implements ShouldQueue, uses exponential backoff (3 retries, 60/300/900s delay). Log Mailables sent with ILogger." |
| Artisan command | "Add 'app:sync-products' Artisan command. Accepts --source option (csv|api). Processes in chunks of 100. Outputs progress bar. Retries failed items up to 3 times." |
| Policy + Gate | "Generate ProductPolicy: viewAny (all auth users), view (public), create (admin or editor role), update (admin or owner), delete (admin only). Register in AuthServiceProvider." |
PHP 8.x Patterns Claude Code Follows
// Declare in CLAUDE.md: "PHP 8.3, strict_types, readonly DTOs, enums for status"
// Claude generates — modern PHP patterns automatically
// Enum for status (PHP 8.1+)
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
}
// Readonly DTO (PHP 8.2+)
readonly class CreateOrderData
{
public function __construct(
public readonly string $customerId,
public readonly OrderStatus $status,
public readonly array $lineItems,
) {}
}
// Constructor promotion + named args
$data = new CreateOrderData(
customerId: $request->user()->id,
status: OrderStatus::Pending,
lineItems: $request->validated('items'),
);
Pest Testing Patterns
// Claude generates Pest tests when you ask for tests
// Feature test for API endpoint
it('creates an order and returns 201', function () {
$user = User::factory()->create();
$product = Product::factory()->create(['price' => 29.99]);
$response = $this->actingAs($user)
->postJson('/api/v1/orders', [
'items' => [['product_id' => $product->id, 'quantity' => 2]],
]);
$response
->assertStatus(201)
->assertJsonPath('data.status', 'pending')
->assertJsonStructure(['data' => ['id', 'status', 'total', 'items']]);
$this->assertDatabaseHas('orders', ['user_id' => $user->id, 'status' => 'pending']);
})->uses(RefreshDatabase::class);
// Unit test for Action class
it('calculates order total with quantity discount', function () {
$action = new CalculateOrderTotalAction();
$items = [
['price' => 10.00, 'quantity' => 9], // no discount
['price' => 10.00, 'quantity' => 10], // 10% discount applies
];
expect($action->execute($items[0]))->toBe(90.00);
expect($action->execute($items[1]))->toBe(90.00); // 10 * 10 * 0.9
});
Estimate Claude API costs for your PHP application
Try the Claude Cost Calculator →
Try the Claude Cost Calculator →
WordPress Plugin Development
WordPress requires different conventions from modern PHP. Add a separate CLAUDE.md for WP projects.
# CLAUDE.md — WordPress Plugin
## WordPress conventions (WPCS)
- Follows WordPress Coding Standards — NOT PSR-12
- snake_case for functions and variables
- PascalCase for classes
- Prefix ALL functions, classes, constants with 'myplugin_' to prevent conflicts
- Use wp_kses_post() / esc_html() / esc_attr() for ALL output — never echo raw variables
- Use wpdb->prepare() for ALL database queries — never string interpolation in SQL
- Nonces required for ALL form submissions and AJAX handlers
## Plugin structure
- myplugin.php — main plugin file with headers
- includes/ — class files (class-myplugin-*.php)
- admin/ — admin-facing code
- public/ — frontend code
- languages/ — .pot translation file
# Prompt for WP REST endpoint
"Add a REST API endpoint POST /wp-json/myplugin/v1/leads.
Register in 'rest_api_init' hook. Require authentication
(permission_callback: is_user_logged_in). Validate email field
with is_email(). Sanitize all inputs. Insert with $wpdb->insert().
Return WP_REST_Response with 201 on success."
Symfony Console Commands
# Prompt for Symfony project
"Add a Symfony console command 'app:import-products'.
Argument: filepath (CSV path). Option: --dry-run (no DB writes).
Use ProgressBar helper. Process in chunks of 200 via batch insert.
Inject ProductRepository via constructor DI.
Add PHPUnit test using CommandTester."
# .claude/settings.json hook for Symfony
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "./vendor/bin/phpunit --testdox 2>&1 | tail -25"
}]
}]
}
}
5 PHP Tips for Claude Code Users
- 1. Specify your PHP version explicitly — PHP 8.0, 8.1, 8.2, and 8.3 each added significant features (enums, fibers, readonly classes, typed class constants). Without a version in CLAUDE.md, Claude may generate code using features your runtime doesn't support.
- 2. Tell Claude which test runner you use — Pest and PHPUnit have different syntax and assertion styles. Specify in CLAUDE.md so every generated test is immediately runnable without conversion.
- 3. Use declare(strict_types=1) as a CLAUDE.md rule — this single instruction prevents Claude from generating loose-typed function signatures that mask bugs in production.
- 4. Reference your Action/Service pattern explicitly — Laravel's default fat-controller pattern produces unmaintainable code at scale. Tell Claude "business logic lives in app/Actions/ not controllers" and it will never put business logic in a controller again.
- 5. Paste php artisan route:list output — when asking Claude to add or modify an endpoint, paste the current route list so it can identify naming collisions and middleware groups to inherit from existing routes.
Related Guides
Database & migrations → | Docker containers → | Testing workflows → | Python comparison → | Hook patterns →