SKILL.md
Laravel Functional Programming Guide
This skill guides agents to write Laravel applications using functional programming (FP) principles while respecting Laravel's conventions.
Core Principles
- Immutability: Never mutate data; create new instances instead
- Pure Functions: Functions should have no side effects and return consistent outputs for same inputs
- Higher-Order Functions: Use map, filter, reduce, and other collection operations
- Function Composition: Combine small functions to build complex behavior
- Avoid Global State: Minimize use of facades and static helpers in core logic
- Type Safety: Use PHP type declarations and value objects
Best Practices
Declare Strict Types
Always enable strict typing in PHP files:
<?php
declare(strict_types=1);
namespace App\Data;
// ...
Why:
- Type coercion is disabled (1 !== '1')
- Better type safety
- Clearer error messages
- Prevents silent bugs
Single Responsibility
Each function/class should do one thing:
// ❌ Bad - does too much
function createUser(array $data): array
{
$validated = validate($data);
$user = User::create($validated);
sendWelcomeEmail($user);
return $user->toArray();
}
// ✅ Good - single responsibility
function validateUserData(array $data): Result { ... }
function persistUser(array $data): User { ... }
function notifyUser(User $user): void { ... }
Prefer Composition Over Inheritance
// ❌ Inheritance - tightly coupled
class AdminUser extends User
{
public function deleteEverything() { ... }
}
// ✅ Composition - flexible
class UserService
{
public function __construct(
private AuthorizationService $auth,
private NotificationService $notify,
) {}
public function deleteUser(User $user): Result { ... }
}
Avoid Side Effects in Pure Functions
// ❌ Side effect - modifies global state
function registerUser(array $data): User
{
$user = User::create($data);
session()->flash('success', 'User created'); // Side effect!
return $user;
}
// ✅ Pure - no side effects
function createUser(array $data): Result
{
return Result::success(User::create($data));
}
// Controller handles side effects
$result = $this->userService->createUser($data);
$result->then(fn($user) => session()->flash('success', 'User created'));
Recommended Packages
composer require spatie/laravel-data # Data transfer objects (RECOMMENDED)
composer require fp4php/functional # Monads (Option, Either, Result) - NOT for Collection operations
Use Laravel built-ins for:
Collection- map, filter, reduce, etc.Numberfacade - formattingstr()helper - UUID, slugs, stringsilluminate/validation- validationBus/Queue- commands/jobs
Folder Structure (Preserve Laravel Boilerplate)
app/
├── Console/ # Keep existing - use FP in commands
├── Data/ # Spatie Laravel Data objects (NEW)
├── Exceptions/ # Keep existing - functional exception handling
├── Http/
│ ├── Controllers/ # Functional controllers
│ ├── Middleware/ # Keep existing - compose middleware functionally
│ └── Requests/ # Functional form requests
├── Jobs/ # Pure job classes
├── Models/ # Keep existing - enhance with FP patterns
├── Providers/ # Keep existing
├── Repositories/ # Functional repositories
├── Services/ # Functional services
├── Support/ # Helpers, Result, Maybe monads
└── Traits/ # FP traits
Quick Reference Patterns
Result Monad Pattern (using fp4php/functional)
<?php declare(strict_types=1);
namespace App\Support;
use Fp4php\Functional\Result\Result;
use Fp4php\Functional\Result\Success;
use Fp4php\Functional\Result\Failure;
// Usage in controller:
return $this->validateRequest($request)
->andThen(fn($data) => User::create($data))
->map(fn($user) => response()->json($user, 201))
->getOrElse(fn($error) => response()->json(['error' => $error], 500));
Why fp4php/functional?
- Laravel doesn't have a Result monad
- Provides
map,filter,flatMap,getOrElse, etc. - Composable error handling without exceptions
Collection Pipeline
// ✅ Functional - prefer
User::where('is_active', true)
->get()
->filter(fn($u) => $u->role === 'admin')
->map(fn($u) => ['id' => $u->id, 'name' => $u->name])
->toArray();
Data Objects
// app/Data/UserData.php
class UserData extends Data
{
public function __construct(
public string $email,
public string $name,
public ?int $id = null,
) {}
}
// In controller:
return UserData::from(User::create($data->toArray()));
Progressive Disclosure
Load detailed references when needed:
| Topic | File |
|---|---|
| Models, Controllers, Services | [REFERENCE.md](references/REFERENCE.md) |
| Validation patterns | [VALIDATION.md](references/VALIDATION.md) |
| Spatie Data objects | [DATAOBJECTS.md](references/DATAOBJECTS.md) |
| Value objects | [VALUEOBJECTS.md](references/VALUEOBJECTS.md) |
| Helper functions | [HELPERS.md](references/HELPERS.md) |
| CLI commands | [CLI.md](references/CLI.md) |
Key Patterns
Functional Controller
See [REFERENCE.md](references/REFERENCE.md#controllers) for full controller patterns with Result monad, validation, and error handling.
Functional Jobs
See [REFERENCE.md](references/REFERENCE.md#jobs) for pure job pattern with batch processing and Laravel native commands.
Functional Services
See [REFERENCE.md](references/REFERENCE.md#services) for service layer with dependency injection and pure functions.
Things to Avoid
- Mutating collections in place - Use
map,filter,rejectwhich return new collections - Direct facade calls in business logic - Inject services instead
- Global state - Pass dependencies explicitly
- Procedural code in controllers - Extract to services
- Side effects in models - Keep models anemic (data only)
- Static helpers - Use dependency injection
Never Remove Existing Classes
When refactoring existing Laravel code:
- Add new classes alongside existing ones
- Extend rather than replace
- Deprecate with warning comments
- Use traits to inject FP patterns into existing classes
- Never delete models, controllers, or other core classes