Code Quality

PHP Standards

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
) {}