Strict Types
Every PHP file must declare strict types:
<?php
declare(strict_types=1);
namespace App\Actions\Tickets;
Type Hints
Parameters
All method parameters must have type hints:
// Good
public function execute(TicketData $data): Ticket
public function update(int $id, array $attributes): void
public function find(string $hashId): ?User
// Bad
public function execute($data)
public function update($id, $attributes)
Return Types
All methods must declare return types:
// Good
public function execute(TicketData $data): Ticket
public function delete(int $id): void
public function isActive(): bool
protected function casts(): array
// Bad
public function execute(TicketData $data)
public function delete(int $id)
Union Types
Use union types for flexible parameters:
// Good
public function execute(int|string|Item $item): void
{
if (!$item instanceof Item) {
$item = Item::findOrFail((int) $item);
}
}
// Nullable with union
public function setAddress(string|null $address): void
Nullable Types
Use ?Type or union with null:
// Good
public function find(int $id): ?User
public function getDescription(): string|null
// Bad
public function find(int $id): User|null|false // Avoid false returns
Class Declarations
Final Classes
Classes should be final by default:
// Good
final class CreateTicket
{
public function execute(TicketData $data): Ticket
{
return Ticket::create($data->toArray());
}
}
final class User extends Authenticatable
{
use HasFactory, HasHashIds;
}
// Bad - only omit final when inheritance is intentional
class CreateTicket
{
}
Visibility
Always declare explicit visibility:
// Good
final class TicketService
{
private int $maxRetries = 3;
public function create(TicketData $data): Ticket
{
return $this->persist($data);
}
private function persist(TicketData $data): Ticket
{
return Ticket::create($data->toArray());
}
}
// Bad
class TicketService
{
$maxRetries = 3; // Missing visibility
function create($data) // Missing visibility
{
}
}
Constructor Property Promotion
Use constructor promotion for DTOs and simple classes:
// Good
final class TicketData extends Data
{
public function __construct(
public string $title,
public string $body,
public int $user_id,
public ?int $category_id = null,
) {}
}
// Acceptable for complex initialization
final class PaymentProcessor
{
private Gateway $gateway;
private Logger $logger;
public function __construct(Gateway $gateway, Logger $logger)
{
$this->gateway = $gateway;
$this->logger = $logger;
}
}
Enums
Use backed enums with explicit values:
// Good - Integer backed
enum Status: int
{
case Pending = 0;
case Active = 1;
case Completed = 2;
case Cancelled = 3;
}
// Good - String backed
enum WeightTypes: string
{
case Kilograms = 'kg';
case Pounds = 'lbs';
}
// Usage
$ticket->status = Status::Active;
if ($ticket->status === Status::Pending) {
// ...
}
Attributes
Use PHP 8 attributes for metadata:
// Livewire components
#[Title('Dashboard')]
#[Layout('components.layouts.auth')]
final class Dashboard extends Component
{
}
// Validation
#[Validate('required|string|email')]
public string $email = '';
#[Validate('required|string|min:8')]
public string $password = '';
// Query scopes
#[Scope]
protected function active(Builder $query): void
{
$query->where('is_active', true);
}
Modern PHP Features
Null Coalescing
// Good
$name = $user->name ?? 'Guest';
$settings = $user->settings ?? [];
// Assignment
$this->cache ??= new Cache();
Named Arguments
// Good for many parameters
$ticket = Ticket::create(
title: $data->title,
body: $data->body,
user_id: $data->user_id,
);
Match Expressions
// Good
$label = match ($status) {
Status::Pending => 'Waiting',
Status::Active => 'In Progress',
Status::Completed => 'Done',
default => 'Unknown',
};
Arrow Functions
// Good for simple callbacks
$names = $users->map(fn (User $user) => $user->name);
$active = $items->filter(fn (Item $item) => $item->is_active);
Comparisons
Strict Comparison
Always use strict comparison:
// Good
if ($status === Status::Active) {
}
if ($count === 0) {
}
if ($name !== null) {
}
// Bad
if ($status == Status::Active) {
}
if ($count == 0) {
}
Null Checks
// Good
if ($user !== null) {
}
if ($user === null) {
}
// Also acceptable
if ($user) {
}
if (!$user) {
}
Imports
Order
Imports should be ordered alphabetically, grouped by type:
<?php
declare(strict_types=1);
namespace App\Actions\Tickets;
use App\Data\TicketData;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
// No unused imports
No Aliases (Unless Necessary)
// Good
use App\Models\User;
use App\Models\Ticket;
// Only when conflicts exist
use App\Models\User;
use App\External\User as ExternalUser;
Formatting
Array Syntax
// Good - Short array syntax
$items = ['one', 'two', 'three'];
$config = [
'key' => 'value',
'nested' => [
'item' => 'data',
],
];
// Bad
$items = array('one', 'two', 'three');
New Without Parentheses
// Good (per Pint config)
$user = new User;
$collection = new Collection;
// When passing arguments, parentheses required
$user = new User($data);
Trailing Commas
Use trailing commas in multi-line arrays and parameters:
// Good
$data = [
'name' => $name,
'email' => $email,
'role' => $role, // Trailing comma
];
public function __construct(
public string $title,
public string $body,
public int $user_id, // Trailing comma
) {}