Skip to content

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 @property in 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 $casts property for cast declarations.
  • Datetime casting: Always cast datetime fields (for example, created_at, updated_at, deleted_at, and fields ending in _at or _date) to datetime.
  • If CarbonImmutable is registered in AppServiceProvider via Date::use(CarbonImmutable::class), a datetime cast returns a CarbonImmutable instance.
  • 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 @property to 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 final unless 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\LazyLoadingViolationException if you access a relation that was not eager-loaded.
php
$user = User::query()->first();
$user->posts; // ✗ throws LazyLoadingViolationException

preventSilentlyDiscardingAttributes($shouldBeStrict)

  • When enabled, mass assignment with attributes outside $fillable throws \Illuminate\Database\Eloquent\MassAssignmentException instead 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 MissingAttributeException

Potential 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.