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
<?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:
php
// ❌ 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
php
// ❌ 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
php
// ❌ 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
bash
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 - formatting - •
str()helper - UUID, slugs, strings - •
illuminate/validation- validation - •
Bus/Queue- commands/jobs
Folder Structure (Preserve Laravel Boilerplate)
code
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
<?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
php
// ✅ 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
php
// 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 |
| Validation patterns | VALIDATION.md |
| Spatie Data objects | DATAOBJECTS.md |
| Value objects | VALUEOBJECTS.md |
| Helper functions | HELPERS.md |
| CLI commands | CLI.md |
Key Patterns
Functional Controller
See REFERENCE.md for full controller patterns with Result monad, validation, and error handling.
Functional Jobs
See REFERENCE.md for pure job pattern with batch processing and Laravel native commands.
Functional Services
See REFERENCE.md 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