Appearance
Using Models
Introduction
PHPDocs are essential when working with Eloquent models in Laravel. Accurate, complete PHPDocs do more than document code—they improve day-to-day development, especially type safety and IDE support.
Basic PHPDocs structure for a model
Example PHPDocs for a User model:
php
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\CarbonImmutable;
/**
* Class User
* @package App\Models
*
* @property int $id
* @property string $name
* @property string $email
* @property string|null $password
* @property CarbonImmutable|null $email_verified_at
* @property string|null $remember_token
* @property CarbonImmutable $created_at
* @property CarbonImmutable $updated_at
* @property Collection<int, Post> $posts
* @property Collection<int, Role> $roles
* @property Profile|null $profile
*/
final class User extends Model
{
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
}Important rules and guidelines
1. Always document @property
- Required: Declare every column from the corresponding database table as a
@propertyin the model's PHPDocs. - Type safety and type hinting: This helps IDEs (such as PhpStorm) suggest code, check types when accessing attributes, and helps static analysis tools like PHPStan catch potential errors.
- Correct types: Use precise types. For nullable fields, use a union type (for example,
string|null).
2. Use casts() for typing
- Laravel 10+: Prefer the
casts()method over the$castsproperty for cast declarations. - Datetime casting: Always cast datetime fields (for example,
created_at,updated_at,deleted_at, and fields ending in_ator_date) to datetime. - If
CarbonImmutableis registered inAppServiceProviderviaDate::use(CarbonImmutable::class), adatetimecast returns aCarbonImmutableinstance. - Cast other fields when needed (for example,
boolean,integer,float,array,object,collection, Enum).
Example casts() method on a WorkOrder model:
php
<?php
declare(strict_types=1);
namespace App\Models;
use App\Enums\WorkOrderStatus;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Model;
/**
* Class WorkOrder
*
* @property int $id
* @property CarbonImmutable $start_time
* @property CarbonImmutable $end_time
* @property bool $has_oqc
* @property WorkOrderStatus $status
* @property CarbonImmutable|null $forced_stop_at
*/
final class WorkOrder extends Model
{
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'start_time' => 'datetime',
'end_time' => 'datetime',
'forced_stop_at' => 'datetime',
'has_oqc' => 'boolean',
'status' => WorkOrderStatus::class,
'options' => 'array',
];
}
}3. Keep business logic out of models
- Models should define data structure, casts, relationships, and local scopes.
- Avoid placing complex business logic such as price calculation, order processing, or email sending directly on the model.
- Move business logic into dedicated classes such as Actions, Jobs, or Services under an action-based architecture.
4. Document relationships in PHPDocs
- Use
@propertyto declare relationships defined on the model. - For HasMany, BelongsToMany, MorphMany, and MorphToMany, use a typed
Collection, for example:@property Collection<int, Post> $posts. - For HasOne, BelongsTo, MorphOne, and MorphTo, return a model or null, for example:
@property Profile|null $profile.
5. Keep PHPDocs in sync when models change
Keep PHPDocs aligned with the database table structure and model changes (adding, removing, or updating attributes, relationships, and scopes). Tools such as barryvdh/laravel-ide-helper can generate basic PHPDocs automatically; then add complex relationships manually.
Type hinting and strict types
- Add
declare(strict_types=1);at the top of every PHP file. - Declare explicit return types on every method.
- Provide PHPStan-compatible PHPDoc for all properties and methods.
Example model with a custom builder:
php
<?php
declare(strict_types=1);
namespace Modules\Masterdata\Product\Infrastructure\Models;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Modules\Masterdata\Product\Infrastructure\Builders\ProductBuilder;
use Modules\Masterdata\Product\Infrastructure\Enums\ProductType;
/**
* Class Product
*
* @property string $id
* @property string $name
* @property ProductType $type
* @property string $product_category_id
* @property CarbonImmutable $created_at
* @property CarbonImmutable $updated_at
* @property-read Collection<int, Spec> $specs
*
* @method static ProductBuilder query()
*/
final class Product extends Model
{
public $incrementing = false;
protected $fillable = [
'id',
'name',
'type',
'product_category_id',
];
protected $keyType = 'string';
protected $table = 'products';
public function newEloquentBuilder($query): ProductBuilder
{
return new ProductBuilder($query);
}
protected function casts(): array
{
return [
'type' => ProductType::class,
];
}
}Class design
- Mark classes as
finalunless extension is genuinely required. - Avoid inline comments; rely on clear variable and method names.
php
<?php
declare(strict_types=1);
namespace App\Services;
final class OrderService
{
public function getOrderItems(): Collection
{
return $this->fetchOrderItems();
}
}Strict model
To catch Eloquent model issues early—such as unintended lazy loading, or silently overriding or discarding attributes—Laravel provides strict mode. Enabling it throws exceptions when:
- Lazy loading: Automatically querying a relation that was not eager-loaded with
->with(), which often leads to N+1 problems. - Silently discarding attributes: Ignoring attributes that are not mass-assignable without notice.
- Accessing missing attributes: Reading attributes that do not exist on the model.
Register in AppServiceProvider
php
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::shouldBeStrict(true);
}
}Details of the three methods
preventLazyLoading($shouldBeStrict)
- When enabled, Eloquent throws
\Illuminate\Database\Eloquent\LazyLoadingViolationExceptionif you access a relation that was not eager-loaded.
php
$user = User::query()->first();
$user->posts; // ✗ throws LazyLoadingViolationExceptionpreventSilentlyDiscardingAttributes($shouldBeStrict)
- When enabled, mass assignment with attributes outside
$fillablethrows\Illuminate\Database\Eloquent\MassAssignmentExceptioninstead of silently discarding them.
php
$data = ['id' => '123', 'name' => 'Test'];
User::query()->create($data);preventAccessingMissingAttributes($shouldBeStrict)
- When enabled, accessing a missing attribute throws
\Illuminate\Database\Eloquent\MissingAttributeException.
php
$user = User::query()->first();
$user->nonExisting; // ✗ throws MissingAttributeExceptionPotential model-related failure modes are surfaced during development, which reduces bugs and improves application stability. Require Laravel >= 9.0 to use these strict methods.
In short, accurate model PHPDocs keep code clear, strengthen type safety, and improve automated test quality. Combine them with strict types, PSR-12, and Laravel Pint for a consistent codebase.