Laravel Relatable

Usage

Add the InteractsWithRelated trait to each model that relates to others. The models it relates to don't need the trait, unless they relate back.

use Foxws\Relatable\Concerns\InteractsWithRelated;

class Tag extends Model
{
    use InteractsWithRelated;
}

Attaching

$action->attachRelated($fastpace);            // score and boost default to 1.0
$action->attachRelated($genre, score: 0.5);
$action->attachRelated($chase, score: 0.4, boost: 2.0, options: ['source' => 'manual']);

Attaching a model that is already related updates that relation — a score or boost left null keeps its current value. It returns the Foxws\Relatable\Models\Relatable row.

Mutual relations

Relations are directed, so $action->attachRelated($genre) only writes Action → Genre: $genre->relates doesn't include Action. Pass mutual: true to write the relation back as well, in the same transaction:

$action->attachRelated($genre, score: 1.0, mutual: true, mutualScore: 0.5);

// Action → Genre: score 1.0
// Genre → Action: score 0.5

The relation back gets the same score and boost, unless you give it its own with mutualScore and mutualBoost. This lets two models relate to each other with a different weight each way — a genre may matter a lot to an action, while the action is just one of many for the genre. The options are the same for both.

Only the model you call the method on has its loaded relations refreshed. If $genre already has relatables or relates loaded, reload it to see the relation back:

$genre->refresh();

Detaching

$action->detachRelated($genre);               // only Action → Genre
$action->detachRelated($genre, mutual: true); // and Genre → Action

Syncing

syncRelated() relates exactly the given models: missing relations are created, existing ones updated, and the rest removed. Each item is a model, or an array with a model and an optional score, boost and options:

$action->syncRelated([
    $fastpace,
    ['model' => $genre, 'score' => 0.5],
]);

$action->syncRelated([]); // remove all

A plain model keeps the score of an existing relation.

With mutual: true, the relations back are kept in sync too:

$action->syncRelated([$fastpace, $genre], mutual: true);
  • Each given model is related back to this one, with the same score and boost as its item (a plain model keeps the scores of an existing relation back). Use attachRelated() to give a relation back its own score.
  • A relation back is only removed along with the relation that sync removes. So when Genre → Action exists without Action → Genre, syncing Action without Genre keeps it.
$action->relates;                  // cached attribute, highest weight first
$action->getRelates();             // same, uncached
$action->getRelates(Video::class); // only videos (a class or morph alias)

Related models are eager loaded per type, so this is one query per related model type. To eager load them for many models at once:

Tag::with('relatables.related')->get();

Relations

$action->relatables();     // MorphMany: Action → others
$action->relatablesFrom(); // MorphMany: others → Action

Deleting models

Deleting a model deletes its relations in both directions. Soft-deleted models keep them until they're force-deleted. Disable this with relatable.delete_on_model_delete.