Loading...

This is taking longer than expected.

Coding standards

Laravel Models

These are the standards the analyzer applies. Every rule states what it checks and why it exists.

All standards

No $fillable

Eloquent models use $guarded, never $fillable.

Model Section Order

Keep model sections in this relative order when those sections exist: Static Functions → Functions → Scopes → Relationships → Attributes.
Properties are declared at the top of the class, before any method (Static Functions included).
When a model contains methods from more than one section, the section comments must be present (the division). Comments are optional only when the model has a single section. Report out-of-order methods and missing divisions.
The wording of a section comment is free: what is checked is that a comment sits above the first method of each section, not what it says, so Factory helpers divides as well as Static Functions. Free wording is still a name, not a sentence: at most four words, no closing period, which is what Comments Earn Their Place recognizes as a division. A docblock never divides, because it documents the method below it. Write the division in the multi-line /* */ form the rest of these standards use. A comment in that form naming Static Functions, Functions, Scopes, Relationships or Attributes is read as a promise about what follows it as well, and a method from another section written under it is reported.

Meaningful Scopes

A local scope must encapsulate a reusable domain constraint. Do not create a scope whose only body forwards one argument to one where() call; write that where() directly at the call site instead.
The automated rule reports only a scope with exactly one return statement in the form return $query->where('column', $value) or return $query->where('column', '=', $value). It does not report scopes that normalize a value, read a property from a domain object, combine conditions, or otherwise add behavior.

// Wrong
public function scopeForInterval($query, string $interval)
{
    return $query->where('interval', $interval);
}

// Correct
return BillingPriceVariation::where('interval', $interval)->get();

Attributes

Accessors and mutators use the modern Attribute::get() / Attribute::set() form. Legacy getFooAttribute() / setFooAttribute() methods are deprecated in favor of it: convert them when the file is touched. Accessors and mutators belong in the Attributes section.

// Correct — modern attribute form
protected function fullName(): Attribute
{
    return Attribute::get(function () {
        return $this->first_name . ' ' . $this->last_name;
    });
}

// Incorrect — legacy accessor method
public function getFullNameAttribute()
{
    return $this->first_name . ' ' . $this->last_name;
}

Model Naming & Organization

  • Relationship names must describe exactly what they return. Add qualifiers only when there are multiple relationships of the same type.
  • Static query/finder methods must name the returned subject. Filters may appear after the subject, e.g. getPricesByInterval.
  • Inside the model, a comment is a label of four words or fewer — a section division, or a step marker inside a body — or a block holding only an annotation a tool reads. A comment describing what a method does is reported by Comments Earn Their Place; when the domain reason has to be recorded, write it in the docs/{project} document that owns the subject.

Rich Domain Models

When a domain action belongs to a model, expose it through a model method. Livewire components and controllers call the model method, not the service/job directly.
Pattern: Livewire / Controller → Model Method → Service / Job

// Correct
$user->toggleAddOn($addOn);

// Incorrect
AddOnService::toggle(auth()->user(), $addOn);

Extract a trait when the same model behavior is reused across multiple models, or when a model carries two or more methods about a feature the model has but is not. Name the trait after the feature — HasReferrals for the referral feature — and use it in the model.
A feature is separable when it can belong to another model too: User has referrals, so referrals become HasReferrals. Do not extract a trait when the methods are the model's own domain: Subscription is the subscription, so its methods stay on the model; a HasSubscriptions trait would move the model's own logic elsewhere without reuse.
One related method is vocabulary and stays on the model.

What Belongs in a Model

A model method or accessor describes what the record is, or what the application can ask of it: $user->markLoggedIn(), User::emailExists($email), $plan->can_be_deleted.
Generic utilities do not belong, even when the model is their only caller today. Formatting a string, normalizing user input, parsing a date, or building an identifier for a third-party service describe a value, not the record.
The test: would the method still make sense if the model did not exist? normalizePhoneNumber() would — it is about phone numbers, not about users, and nobody would ask a user to normalize a phone. phoneExists() would not — it is a question about stored users, so it belongs on the model.

// Correct — vocabulary of the record
User::phoneExists($phone);

// Incorrect — a phone formatting utility parked in the model
User::normalizePhoneNumber($phone);

Where such a utility goes instead is covered by "Shared Utilities Belong in Helpers" in the PHP standard.

Dates, Timezones, and Casts

  • Store dates in UTC.
  • Cast model-owned date, boolean, array/JSON, and enum attributes in $casts.
  • Do not replace casts with accessors or manual transformations.
  • Report missing casts only for attributes the reviewed model clearly owns.
  • Report manual date handling only when the code parses, formats, or timezone-converts a model-owned date instead of relying on the cast.
// Wrong
'expires_at' => Carbon::parse($value)->format('Y-m-d'),

// Correct
protected $casts = [
    'expires_at' => 'datetime',
];

Enums

Use enums for persisted multi-state attributes.

  • Enum class names must not include Enum, e.g. AddonStatus, not AddonStatusEnum.
  • Enum case names use PascalCase.
  • This automated rule does not validate enum case values; string, integer, and expression values are allowed.

This rule applies to enum-backed state, not simple booleans, null checks, or presentation-only labels.

// Correct
enum AddonStatus: string
{
    case Active = 'ACTIVE';
    case SuspendedAccount = 'account-suspended';
}

// Incorrect
enum AddonStatusEnum: string
{
    case active = 'active';
}

Enum Section Order

Enum files follow the same section order as models, minus the sections an enum cannot have: Static Functions → Functions → Attributes. When an enum file contains methods from more than one section, the section comments must be present, with the same free wording models allow. This applies to enum declarations and to enum support files such as an IsEnum trait living under app/Enums/ (or a package's src/Enums/).

Pending Collections — Mutator + Observer Pattern

When a form edits a related collection through arrays and that collection must be synchronized on save, use a model mutator plus observer. Do not put the sync loop in Livewire or a controller.
Use this pattern only for write flows that insert, update, or delete related rows from incoming state arrays.
Model: declare pending state, capture it with a mutator, and expose form-ready data with an accessor.

public array $pendingItems = [];

public function setItemsDataAttribute(array $items)
{
    $this->pendingItems = $items;
}

public function getItemsDataAttribute()
{
    return $this->items->map(fn ($item) => [
        'id' => $item->id,
        'key' => $item->key,
        'value' => $item->value,
    ])->values()->all();
}

Observer: in saved, read $pendingItems and sync deletes, updates, and inserts.
Caller: assign the array and call save().

Domain Logic Reuse (Single Source of Truth)

Each calculation lives in the model that owns the data. Other models and services reuse that method.

// Correct
$total = $project->tasks->sum('cost');

// Incorrect — duplicates the formula
$total = $project->tasks->sum(fn ($task) => $task->hours * $task->rate);

Accessor or Method

A method on a model that takes no arguments and only computes a value from the record is an attribute, and it reads like one at the call site. Declare it as an accessor.

// Correct
protected function fullName(): Attribute
{
    return Attribute::get(function () {
        return $this->first_name . ' ' . $this->last_name;
    });
}

// Incorrect
public function getFullName()
{
    return $this->first_name . ' ' . $this->last_name;
}

Four things keep a method a method, and none of them is a style preference:

  • It takes arguments, so it answers a question about something else.
  • It persists or changes state, so calling it twice is not the same as calling it once.
  • It returns a query the caller keeps shaping, so the caller decides when it runs.
  • It satisfies a contract — a parent, an interface, or a framework predicate that must stay a method.

Relationships and scopes are neither, and are left alone.

Relationships Are Pure Queries

A relationship method returns a query the caller keeps shaping; it must not persist anything. Do not call firstOrCreate(), create(), updateOrCreate(), or save() inside a relationship method. When a record must exist before the relation can be read, materialize it in an explicit named method (e.g. ensureBillingAccount()) that callers invoke when they are about to write, and keep the relationship itself a pure query. When a package is absent, return a neutral empty relationship rather than writing a fallback record.

Weblabor Logo Developed by Weblabor