Code Style
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:
- Method signatures live on one line.
- Comments and PHPDoc are minimised — typed signatures already document the code.
- 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
./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.
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:
- Array element type —
arrayparameters or properties whose elements are typed. - Generic collection type —
Collection<int, User>for static analysis. @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:
PascalCase—CreateOrder,ProviderContract,FilterOrderData. - Methods and variables:
camelCase—getProvider(),$providerId,$withRelations. - Constants and enum cases:
UPPER_SNAKE_CASE—STATUS_ACTIVE,MAX_RETRIES. - File name matches the class name exactly —
CreateOrder.phpcontainsclass 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;
What to read next
- Anti-patterns — items #3 (multi-line signatures), #18 (DTO casing), #10 (missing PHPDoc shape).
- DTOs — the carve-out for
snake_caseproperties. - 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.
Anti-Patterns
The numbered list of patterns we reject in code review, grouped by severity, with the fix for each.
Security
The handful of security patterns that cause most Laravel bugs in this codebase — Form Requests, authorization gates, `$fillable`, escaping, and what not to return in API responses.