Files
Andreas Reinhold / reiniandClaude Opus 5 92b3b3de56 Regenerate Boost guidelines and skills
Generated by boost:update for Boost 2.8, which replaces the pest-testing
skill with testing-best-practices and adds infer-conventions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XYnWFt9pJEwvAmNFN38XD
2026-09-10 10:42:22 +02:00

6.1 KiB

Architecture Best Practices

Extract Focused Business Operations

Extract a discrete business operation into an action class when doing so makes the operation easier to reuse or test. An action class has no special meaning to Laravel; follow the project's naming and invocation conventions.

class CreateOrderAction
{
    public function __construct(private InventoryService $inventory) {}

    public function handle(array $data): Order
    {
        $order = Order::create($data);
        $this->inventory->reserve($order);

        return $order;
    }
}

Inject Required Dependencies

Prefer constructor injection for dependencies required throughout an object's lifetime. Method injection is appropriate for dependencies needed by one controller action, listener, job handler, or other container-invoked method. Avoid app() and resolve() when normal injection can make a dependency explicit.

Hidden dependency:

class OrderController extends Controller
{
    public function store(StoreOrderRequest $request)
    {
        $service = app(OrderService::class);

        return $service->create($request->validated());
    }
}

Injected dependency:

class OrderController extends Controller
{
    public function store(StoreOrderRequest $request, OrderService $service)
    {
        return $service->create($request->validated());
    }
}

Depend on Contracts at Boundaries

Depend on contracts at system boundaries, such as payment gateways, notification channels, and external services, when testability or interchangeable implementations justify the abstraction.

Concrete boundary dependency:

class OrderService
{
    public function __construct(private StripeGateway $gateway) {}
}

Contract boundary dependency:

interface PaymentGateway
{
    public function charge(int $amount, string $customerId): PaymentResult;
}

class OrderService
{
    public function __construct(private PaymentGateway $gateway) {}
}

Bind in a service provider:

$this->app->bind(PaymentGateway::class, StripeGateway::class);

Specify a Deterministic Sort Order

Without an explicit ORDER BY, row order is undefined. Choose an order that matches the feature, and add a unique tie-breaker when stable pagination matters.

Unspecified order:

$posts = Post::paginate();

Newest first with a stable tie-breaker:

$posts = Post::query()
    ->orderByDesc('created_at')
    ->orderByDesc('id')
    ->paginate();

Use Atomic Locks for Race Conditions

Use a lock when concurrent execution must be serialized. Cache::lock() provides an atomic lock when the configured cache store supports locks. lockForUpdate() locks selected database rows and must run inside a database transaction. These mechanisms solve different coordination problems.

Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) {
    $order->process();
});

// Or at query level, inside a transaction
DB::transaction(function () use ($id) {
    $product = Product::where('id', $id)->lockForUpdate()->first();

    // Read and update the product while the database lock is held.
});

Use mb_* String Functions

When no Laravel helper exists, prefer multibyte-aware functions such as mb_strlen() and mb_strtolower() for UTF-8 text. For example, strlen() counts bytes, while strtolower() is not multibyte-aware.

Incorrect:

strlen('José');          // 5 bytes, not 4 characters
strtolower('MÜNCHEN');  // Does not lowercase Ü

Correct:

mb_strlen('José');         // 4 characters
mb_strtolower('MÜNCHEN'); // 'münchen'

// Prefer Laravel's Str helpers when available
Str::length('José');       // 4
Str::lower('MÜNCHEN');     // 'münchen'

Use defer() for Post-Response Work

For lightweight work that does not need retries or crash durability, consider defer() instead of dispatching a job. During an HTTP request, the callback normally runs after the response has been sent but remains in the same PHP process.

Queued and durable:

dispatch(new LogPageView($page));

Deferred in the current process:

defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()]));

Use a queued job when the work needs retries, queue controls, or durability across process failures.

Use Context for Request-Scoped Data

The Context facade makes contextual data available across the current execution lifecycle without manually passing arguments through every layer.

// In middleware
Context::add('tenant_id', $request->header('X-Tenant-ID'));

// Later in the same execution lifecycle
$tenantId = Context::get('tenant_id');

Visible context is added to log context, and both visible and hidden context are captured and restored for queued jobs. Use Context::addHidden() for data that should propagate to queued jobs without appearing in logs. Do not place secrets in context unless that propagation is intended.

Use Concurrency::run() for Parallel Execution

Run independent operations concurrently through Laravel's configured concurrency driver.

use Illuminate\Support\Facades\Concurrency;

[$users, $orders] = Concurrency::run([
    fn () => User::count(),
    fn () => Order::where('status', 'pending')->count(),
]);

With a process-based driver, each closure runs in a separate PHP process that boots the application. Use concurrency when independent database queries, HTTP client calls, or computations benefit enough to offset process and serialization overhead. The sync driver executes closures sequentially and is useful primarily during testing.

Follow Framework Conventions

Follow Laravel conventions unless the domain or an existing schema requires an override.

Customized schema:

class Customer extends Model
{
    protected $table = 'Customer';
    protected $primaryKey = 'customer_id';

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id');
    }
}

Conventional schema:

class Customer extends Model
{
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class);
    }
}