Eloquent Models
Use Model::query()
We generally don’t use short and magic syntax for queries:
// GOOD
Member::query()->firstWhere('id', 42);
// BAD
Member::firstWhere('id', 42);
Mass assignment
Don’t use mass assignment. We use the Repository pattern package and those function give you alot of benefits from queries to create and delete, if you pair the package with DTO classes it is a winning combination.
Mass assignment SHOULD not be used when it’s easily possible. When it's used in a wrong way, it can add security vulnerabilities, it also allows creating Models with a wrong state.
The preferred way to create or update models is to assign is to use the repository pattern package and with DTO:
// PREFERRED WAY:
$member = new Member();
$member->name = $request->input('name');
$member->email = $request->input('email');
$member->save();
// OR (with pacakge & DTO)
$member = $memberService->create(new \App\DTO\Members\MemberDetail(
name: 'Ettienne'
cell: 0820820821,
status_id: 1
));
// AVOID THIS:
$member->forceFill([
'name' => $request->input('name'),
'email' => $request->input('email'),
])->save();
// NEVER DO THIS:
$member->forceFill($request->all())
->save();
Minimize magic
Don’t use magic where{Something} methods.
Document magic
Document all magic using PHPDoc
When you add a relationship or scope, add the appropriate PHPDoc block to the Model:
// Models/Member.php
/**
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Modules\Permission\Models\Role> $roles Member’s Roles (added by a Member::roles() relationship)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Modules\Member\Models\Member canceled() Cancelled Member state (added by a Member::scopeCanceled())
*/
Safe defaults
Use safe defaults for attributes
Model’s attributes should not rely on DB’s default values. Instead, we should duplicate defaults in the model by filling the $attributes array. It helps us to be more independent of the DB and simplifies Model’s Factories as well as testing.
Scopes method
Use scopes method instead of using magic methods
User::query()->scopes(['trial'])->...
For scopes with parameters, we recommend to use tappable scopes:
// Models/Member.php
$unverifiedUsers = User::query()
->tap(new Unverified())
->get();
Custom EloquentBuilder
Use custom EloquentBuilder classes to simplify models
To simplify models & enable better type-hint by IDE for big Models, we should extract a custom query builder class.
It's a recommendation for Models with 4+ query scopes and a requirement for Models with 10+ query scopes.
Here's how we can add the builder to the model class.
class User extends Model
{
/**
* @inheritDoc
* @param \Illuminate\Database\Query\Builder $query
* @return \App\Models\UserEloquentBuilder<self>
*/
public function newEloquentBuilder($query): UserEloquentBuilder
{
return new UserEloquentBuilder($query);
}
}
This is the builder with one custom query:
/** @extends \Illuminate\Database\Eloquent\Builder<\App\Models\User> */
final class UserEloquentBuilder extends Builder
{
public function confirmed(): self
{
return $this->whereNotNull('confirmed_at');
}
}
And here's how we can use it:
$confirmedUsers = User::query()->confirmed()->get();
Make sure to wrap orWhere clauses inside another where clause to make a query safe for other "where" conditions:
final class UserEloquentBuilder extends Builder
{
public function publishedOrCanceled(): self
{
- return $this->withTrashed()->whereNotNull('published_at')->orWhereNotNull('deleted_at');
+ return $this->withTrashed()->where(static function (\Illuminate\Database\Eloquent\Builder $builder): void {
+ $builder->whereNotNull('published_at');
+ $builder->orWhereNotNull('deleted_at');
+ })
}
}
N+1 Issues
Ever heard about N+1 problems? Eager loading is a great solution to avoid them. To ensure you don’t have N+1 problems, you can trigger exceptions whenever you lazy load any relationship. This restriction should be applied to your local environment only.
Typically, this method should be invoked in the boot method of your application's AppServiceProvider.php:
use Illuminate\Database\Eloquent\Model;
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}
Eloquent’s strict mode is a blessing for debugging. It helps developers catch potential issues during the development phase by throwing exceptions in the following cases:
Lazy loading relationships:lazy loading can lead to performance issues, especially when dealing with large datasets. It occurs when related models are not retrieved from the database until they are explicitly accessed. In strict mode, an exception will be thrown if a relationship is lazily loaded, encouraging developers to use eager loading instead.Assigning non-fillable attributes:the $fillable property on Eloquent models protects against mass assignment vulnerabilities. An exception will be thrown when trying to assign a non-fillable attribute, ensuring that developers handle mass assignment carefully.Accessing attributes that don’t exist (or weren’t retrieved):accessing non-existent attributes or attributes that haven’t been retrieved from the database can lead to unexpected behavior or bugs. Strict mode will throw an exception in these cases, helping developers identify and fix such issues.
Read More:
