All posts

Use Eloquent Relationships to Write Cleaner Code

Relationships are not only for reading data. Creating records through them removes hand-wired foreign keys and a whole class of bugs.

Marijan Kopčić••3 min read
Use Eloquent Relationships to Write Cleaner Code

Eloquent relationships are usually introduced as a way to read related data: $post->comments, $user->orders. They are just as useful for writing it. Creating records through a relationship removes a whole class of boilerplate and a whole class of bugs.

Wiring foreign keys by hand

$article = new Article;
$article->title = $request->input('title');
$article->content = $request->input('content');
$article->verified = $request->boolean('verified');
$article->category_id = $category->id;
$article->save();

Six lines to create one record, and one of them is the most error-prone line in the snippet: category_id = $category->id. Forget it and you get an orphaned article or a database error. Copy it into another place with the wrong variable and you get an article in the wrong category, silently.

Let the relationship set the key

$category->articles()->create(
    $request->safe()->only(['title', 'content', 'verified'])
);

$category->articles() already knows that the foreign key is category_id and that its value is $category->id. Calling create() on the relationship fills it in for you. safe()->only() takes the validated input from the Form Request and passes through only the fields you name.

The same pattern covers the other relationship types:

$user->posts()->create($data);                 // hasMany
$post->tags()->attach($tagIds);                // belongsToMany
$post->tags()->sync($tagIds);                  // replace the full set
$comment->author()->associate($user);          // belongsTo

Why this pays off

Foreign keys are handled once. The relationship definition on the model is the single source of truth for how two tables connect. Nobody has to remember the column name at the call site.

The code says what it means. $category->articles()->create(...) reads as "create an article in this category". Six property assignments read as "set some fields", and you have to work out the intent yourself.

Scoping comes for free. Relationships compose with scopes and constraints. $user->posts()->published()->latest()->get() cannot accidentally return another user's posts, because the relationship always adds the user_id condition.

Safer authorization. Creating through $request->user()->posts() guarantees the post belongs to the authenticated user. Setting user_id from the request is how people end up writing posts on behalf of someone else.

Where to be careful

create() uses mass assignment, so the fields you pass must be listed in the model's fillable attributes. That is a feature: it forces you to decide which fields may be set from input. Combined with safe()->only(), it gives you two layers of protection against unexpected columns.

Relationships also make it easy to trigger queries without noticing. Accessing $article->category inside a loop runs one query per article. When you read through relationships in a list, eager load them with with() up front.

A useful rule of thumb:

  • Creating a child record → create it through the parent's relationship.
  • Linking many-to-many → attach(), detach() or sync(), never manual pivot inserts.
  • Reading related data in a loop → eager load with with() first.

The practical test

Search your codebase for _id = $. Every hit is a foreign key being assigned by hand. Most of them can become a single call through a relationship, and each one you convert is one less place where the wrong ID can slip in.

Keep reading