Best Practices

Code Style

How code is formatted in this repo — Pint enforces the mechanical rules, plus three project-specific overrides you need to remember.

A consistent code style means a file written this week reads the same way as a file written three years ago. The cost of agreement is low — most decisions get made once, in pint.json, and the formatter applies them automatically — and the benefit compounds across thousands of files and many contributors.

The project leans on Pint (the Laravel-flavoured PHP-CS-Fixer wrapper) for the mechanical parts and layers three project-specific rules on top:

  1. Method signatures live on one line.
  2. Comments and PHPDoc are minimised — typed signatures already document the code.
  3. DTO properties are snake_case (a deliberate carve-out from PHP convention).

The first two are not enforced by Pint. You have to remember them. CI will reject the PR if you don't.

The shape — what a finished file looks like

A typical Action shows every rule in one screenful:

<?php

namespace Domain\Order\Actions;

use Domain\Order\DataTransferObjects\CreateOrderData;
use Domain\Order\Models\Entities\Order;
use Illuminate\Support\Facades\DB;

final class CreateOrder
{
    public static function handle(CreateOrderData $data): Order
    {
        return DB::transaction(function () use ($data) {
            $order = Order::create($data->toArray());
            $order->lines()->createMany($data->lines);

            return $order;
        });
    }
}

Note the small things: alphabetised use imports, final class, one-line signature, no docblock (the types say everything), short array syntax inside the method body, single quotes throughout.

Pint enforces formatting

./vendor/bin/pint is the formatter. PSR-12 with Laravel-specific modifications configured in pint.json. Run it before every commit — CI rejects diffs that Pint would change. Don't hand-format around a rule; if it's wrong, change pint.json.

./vendor/bin/pint
./vendor/bin/pint --test    # dry-run, exits non-zero if changes would be made
./vendor/bin/pint --dirty   # only files you've changed since the last commit
Wire ./vendor/bin/pint --dirty into a pre-commit hook so you never push a Pint-failing branch. The --dirty flag keeps the run fast.

Short array syntax

[], never array(). Single quotes for strings unless interpolation or escape sequences are needed.

✅ Good

$data = ['key' => 'value'];
$interpolated = "User {$user->name} created";   // double quotes justified

❌ Bad

$data = array('key' => "value");

The array() form costs an extra five characters and tells the reader nothing extra. Double quotes around a literal string with no interpolation does the same.

One-line method signatures

Method signatures live on a single line. No exceptions, even when the line gets long. The benefit is predictable structure: every Contract method, every Action handle(), every Service method scans the same way.

❌ Bad

public function getProvider(
    ?int $id = null,
    ?FilterProviderData $filters = null,
    array $with_relations = []
): ?ProviderData;

✅ Good

public function getProvider(?int $id = null, ?FilterProviderData $filters = null, array $with_relations = []): ?ProviderData;

The bad version reads as if every parameter is equally significant. The good version reads as one method — the cognitive shape matches the runtime shape.

Pint does not reformat multi-line signatures into single lines. You have to fix this by hand. The reviewer agent flags it as anti-pattern #3.

This rule applies to Contracts, Actions, Services, Controllers, and any other class method. The one exception is Data class constructors, which stay multi-line for readability — see the example below.

final class CreateOrderData
{
    use Arrayable;

    public function __construct(
        public readonly int $provider_id,
        public readonly int $customer_id,
        public readonly array $lines,
    ) {}
}

Minimal comments and PHPDoc

A typed signature tells the reader more than a stale docblock. Only add a comment when capturing a non-obvious why — a constraint, an invariant, a workaround, an integration quirk. Never explain what the code does. Skip top-of-class and top-of-method docblocks unless the signature genuinely can't express something. Never use @inheritdoc.

❌ Bad — docblock restates the signature

/**
 * Get an employee by id.
 * @param int $id The id of the employee
 * @return Employee|null The employee, or null
 */
public function getEmployee(int $id): ?Employee
{
    return GetEmployee::handle($id);
}

✅ Good — signature is the documentation

public function getEmployee(int $id): ?Employee
{
    return GetEmployee::handle($id);
}

✅ Good — comment captures a non-obvious why

// Bayan API returns 502 intermittently on weekends; treat as soft-fail.
if ($response->status() === 502) {
    return null;
}

The bad docblock is worse than no docblock — it duplicates the signature, gets stale the moment someone renames a parameter, and trains readers to skip docblocks because they rarely add information.

When PHPDoc is needed

Three cases earn PHPDoc, nowhere else:

  1. Array element typearray parameters or properties whose elements are typed.
  2. Generic collection typeCollection<int, User> for static analysis.
  3. @throws — only when the exception is unchecked and meaningful to the caller.
/** @param OrderLineData[] $lines */
public function __construct(
    public readonly int $provider_id,
    public readonly array $lines,
) {}

/** @return Collection<int, Provider> */
public function getActiveProviders(): Collection
{
    return Provider::active()->get();
}

Anything else — @param int $id, @return Employee|null, @inheritdoc, top-of-class summaries — is noise. Remove it on touch.

Naming

  • Classes: PascalCaseCreateOrder, ProviderContract, FilterOrderData.
  • Methods and variables: camelCasegetProvider(), $providerId, $withRelations.
  • Constants and enum cases: UPPER_SNAKE_CASESTATUS_ACTIVE, MAX_RETRIES.
  • File name matches the class name exactly — CreateOrder.php contains class CreateOrder.

DTO properties are snake_case — this is a project-wide carve-out so DTO properties map 1:1 to database columns. See DTOs. Plain PHP variables, method parameters, and local variables stay camelCase.

✅ Good — DTO property is snake_case, local variable is camelCase

final class CreateOrderData
{
    public function __construct(
        public readonly int $provider_id,
    ) {}
}

$providerId = $request->input('provider_id');
$data = new CreateOrderData(provider_id: $providerId);

The reviewer agent treats camelCase properties on a DTO as anti-pattern #18 — silent data loss when $data->toArray() lands in Model::create() with the wrong key.

Type hints and return types

Required on every parameter and every return type. Use ?Type for nullable, not Type|null. Genuine unions (string|array|null on filter DTOs) are fine; nullable-as-union is not.

❌ Bad

public function getProvider($id) { /* ... */ }
public function findProvider(int $id): Provider|null { /* ... */ }

✅ Good

public function getProvider(int $id): ?Provider
{
    return Provider::find($id);
}

Readonly properties

Mark properties readonly whenever they don't change after construction. DTOs always. Value objects almost always. Controllers and Services with only injected dependencies benefit too.

final class CreateOrderData
{
    public function __construct(
        public readonly int $provider_id,
        public readonly int $customer_id,
    ) {}
}

class OrderController extends Controller
{
    public function __construct(
        protected readonly OrderContract $orderContract,
    ) {}
}

Imports

One class per use statement. Pint sorts them alphabetically — don't fight it. No unused imports; CI will flag them.

❌ Bad — not alphabetical, mixed grouping

use Illuminate\Support\Facades\DB;
use Domain\Provider\Contracts\ProviderContract;
use App\Models\User;

✅ Good — alphabetical, one per line

use App\Models\User;
use Domain\Provider\Contracts\ProviderContract;
use Illuminate\Support\Facades\DB;
  • Anti-patterns — items #3 (multi-line signatures), #18 (DTO casing), #10 (missing PHPDoc shape).
  • DTOs — the carve-out for snake_case properties.
  • Actions — the final class X { public static function handle() } skeleton that this style is built around.
  • Code review — how reviewers cite these rules in PR comments.