Eloquent model conventions for mass assignment, casts, relationship naming, activity logging, and mandatory model tests (CRUD + relations).
compatible_agents
["architect","implement","refactor","review"]
Models
When to Apply
When creating a new Eloquent model in app/Models/.
When refactoring existing models to align with mass-assignment, casting, and logging conventions.
When reviewing models for consistency in relationships, helpers, activity logging, and model tests.
When NOT to Apply
For DTOs, value objects, and classes that are not persisted with Eloquent.
For pivot-only structures that do not use a dedicated business model.
Preconditions
Laravel application and Eloquent are configured.
Database schema and migrations for the model’s table exist or are being designed.
spatie/laravel-activitylog is installed and configured for activity logging.
"Business models" means models representing core domain records with audit value (for example invoices, orders, payments). Apply LogsActivity to these models by default.
A factory exists (or is created) for the model and any related models used in tests.
Process
1. Configure Mass Assignment and Casting
Set protected $guarded = []; on all business models; do not use $fillable.
Implement a casts() method instead of a $casts property.
In casts(), configure:
Enums, dates, decimals, and JSON fields with explicit types (e.g., decimal:2).
2. Add Activity Logging
Use the LogsActivity trait on all business models that should be audited.
Implement getActivitylogOptions() to:
Call logAll().
Call logOnlyDirty().
Call dontSubmitEmptyLogs().
3. Define Relationships and Helpers
Use typed return types on all relationship methods (HasMany, BelongsTo, etc.).
Follow Laravel relationship naming conventions:
Use singular names for single-record relations (belongsTo, hasOne, morphOne).
Use plural names for multi-record relations (hasMany, belongsToMany, morphMany).
Method names must use camelCase based on the related model name (for example, pipelineSteps() for PipelineStep).
Avoid generic relation names like steps(), runs(), items(), or attachments() when they hide model intent.
Group related sections of the model with comment headers such as:
// --- Relationships ---
// --- Status Helpers ---
// --- Activity Log ---
Keep domain-specific helper methods focused and clearly named (e.g., isDraft(), isPaid()).
Model uses $guarded = [] and does not define $fillable.
Casting is implemented via a casts() method, not a $casts property.
Enums, dates, decimals, and JSON fields are explicitly cast.
LogsActivity trait is added where auditing is required.
getActivitylogOptions() is configured with logAll(), logOnlyDirty(), and dontSubmitEmptyLogs().
All relationship methods have correct typed return types.
Relationship method names use camelCase(RelatedModelName) with correct singular/plural form.
Existing generic relation names are renamed to explicit model-based names (for example, steps() -> pipelineSteps()).
A matching factory exists in database/factories/.
A model test exists and covers Create, Read, Update, Delete behavior.
Every relationship method has at least one assertion for relation type and one for relation data retrieval.
At least one cast/helper assertion validates domain behavior (for example enum or status helper).
Business logic is extracted to Actions or Services instead of living directly in the model.
Safety / Things to Avoid
Using $fillable arrays instead of $guarded = [].
Defining $casts as a property instead of a casts() method.
Omitting the LogsActivity trait on business models that should be audited.
Omitting return types on relationship methods.
Using ambiguous relationship names that do not reflect the related model class.
Creating a model without a corresponding factory.
Creating or updating a model without adding/updating CRUD + relation tests.
Testing only relation existence but not relation behavior (or vice versa).
Putting complex business logic directly in the model — prefer Actions or Services.
Defining model shape with protected array $fillable = ['name']; and protected array $casts = ['status' => 'string']; instead of $guarded = [] and casts()