laravel-php

Idiomatic Laravel/PHP patterns — Eloquent, service layer, PHPUnit/Pest testing, Laravel Pint formatting, and build commands. Covers request validation, jobs/queues, events, and common anti-patterns. Use when working on Laravel PHP projects.

Laravel / PHP Patterns — dog_stack

Idiomatic Laravel and PHP conventions covering Eloquent, service layer, testing, and build toolchain.

Idiomatic Laravel

Service layer

// ✓ Thin controllers, fat service classes
class UserController extends Controller
{
    public function __construct(private UserService $userService) {}

    public function store(CreateUserRequest $request): JsonResponse
    {
        $user = $this->userService->create($request->validated());
        return response()->json(new UserResource($user), 201);
    }
}

class UserService
{
    public function create(array $data): User
    {
        return DB::transaction(fn () => User::create($data));
    }
}

Request validation

// ✓ Form Request classes for validation
class CreateUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name'  => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'unique:users'],
        ];
    }
}

Eloquent patterns

// ✓ Scopes for reusable query constraints
class User extends Model
{
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('active', true);
    }
}

// Usage: User::active()->get()

// ✓ Eager loading to prevent N+1
$users = User::with(['posts', 'roles'])->active()->get();

Jobs and queues

// ✓ Queued jobs for heavy work
class SendWelcomeEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public readonly User $user) {}

    public function handle(Mailer $mailer): void
    {
        $mailer->to($this->user)->send(new WelcomeMail($this->user));
    }
}

// Dispatch: SendWelcomeEmail::dispatch($user);

Testing with Pest / PHPUnit

// Pest (recommended)
test('user can be created', function () {
    $data = ['name' => 'Alice', 'email' => '[email protected]', 'password' => 'password'];

    $response = $this->postJson('/api/users', $data);

    $response->assertCreated()->assertJsonPath('data.email', '[email protected]');
    $this->assertDatabaseHas('users', ['email' => '[email protected]']);
});

test('email must be unique', function () {
    User::factory()->create(['email' => '[email protected]']);

    $this->postJson('/api/users', ['email' => '[email protected]', 'name' => 'Alice', 'password' => 'pw'])
         ->assertUnprocessable()
         ->assertJsonValidationErrors(['email']);
});

Run tests:

php artisan test                   # all tests
php artisan test --filter=UserTest # specific test
vendor/bin/pest                    # pest directly
vendor/bin/pest --coverage         # with coverage

Build Commands

php artisan serve                  # local dev server
php artisan migrate                # run migrations
php artisan migrate:fresh --seed   # reset + seed
php artisan queue:work             # process queue
php artisan cache:clear            # clear all caches
composer install                   # install dependencies
composer require <package>         # add package

Laravel Pint

vendor/bin/pint                    # format all files
vendor/bin/pint --test             # CI check (non-destructive)
vendor/bin/pint app/               # specific directory

Common Pitfalls

PitfallFix
N+1 queriesUse with() eager loading
Logic in controllersMove to service classes
Raw SQL without bindingUse query builder with ? placeholders
Missing authorized() in Form RequestsImplement authorize() method
env() in config files onlyNever call env() outside config/*.php

Related

  • Rules: rules/php/coding-style.md
  • Agent: language reviewers (general-purpose for PHP)