Relations
Ralating models is describing family connections between models, e.g. claiming that a user can have any number of posts and a post can have not more then one author. Related models can be easily retrieved from connections.
Defining relations
One to many
Add a method to a model class to tell the ORM that a model instance has many related instances of one type:
use Finesse\Wired\Model;
use Finesse\Wired\Relations\HasMany;
class User extends Model
{
public $id;
// ...
public static function posts()
{
return new HasMany(Post::class, 'user_id');
}
// ...
}
The posts
method name is the relation name. You can set any name. The relation above tells that the Post
model has
the user_id
field which contains a user instance identifier.
If you need the user_id
field to point to another User
field, pass it's name to the third HasMany
argument:
return new HasMany(Post::class, 'user_email', 'email');
One to many (inverted)
Add a method to a model class to tell the ORM that a model instance belongs to one related instance:
use Finesse\Wired\Model;
use Finesse\Wired\Relations\BelongsTo;
class Post extends Model
{
public $user_id;
// ...
public static function author()
{
return new BelongsTo(User::class, 'user_id');
}
// ...
}
The author
method name is the relation name. You can set any name. The relation above tells that the Post
model has
the user_id
field which contains an author instance identifier.
If you need the user_id
field to point to another User
field, pass it's name to the third BelongsTo
argument:
return new BelongsTo(User::class, 'user_email', 'email');
Many to many
Add a method to a model class to tell the ORM that a model instance belongs to many related instances and vice versa:
use Finesse\Wired\Model;
use Finesse\Wired\Relations\BelongsToMany;
class Post extends Model
{
// ...
public static function tags()
{
return new BelongsToMany(Tag::class, 'post_id', 'post_tags', 'tag_id');
}
}
The tags
method name is the relation name. You can set any name. The relation above tells that there is a post_tags
table (called "pivot") that has two fields: post_id
which contains a post identifier and tag_id
which contains a tag
identifier.
If the pivot table fields point to specific models fields (not the identifier fields), pass the field names as arguments:
return new BelongsToMany(Tag::class, 'post_uuid', 'post_tags', 'tag_name', 'uuid', 'name');
Getting related models
Load all related models:
$user = $orm->model(User::class)->find(14);
$orm->load($user, 'posts');
$posts = $user->posts;
The 'posts'
value is the relation name defined in the User
model class. The posts
property is added automatically
to a User
object, you don't need to specify it in the model class.
Eager load related models for many models:
$users = $orm->model(User::class)->get();
$orm->load($users, 'posts');
foreach ($users as $user) {
foreach ($user->posts as $post) {
// ...
}
}
All the related models are loaded using a single SQL query like this SELECT * FROM posts WHERE id IN (1, 2, 3, 4)
.
Load related models with a constraint or an order:
$orm->load($users, 'posts', function ($query) {
$query
->where('date', '<', '2015-01-01')
->orderBy('date', 'desc');
});
Load relative models only for the models that don't have loaded relatives:
$orm->load($posts, 'author', null, true);
// ...
$orm->load($posts, 'author', null, true); // Doesn't load the second time
Load relative models with relative submodels:
$orm->load($post, 'author.posts.category'); // Relations are divided by dot
foreach ($post->author->posts as $sameAuthorPost) {
$category = $sameAuthorPost->category;
}
Relations in the query builder
Query all models having at least one related instance:
$usersWithPosts = $orm
->model(User::class)
->whereRelation('posts') // The relation name specified in the User model class
->get();
// Or ->delete() or ->update(...)
Query all models related with a model instance:
$user = $orm->model(User::class)->find(12);
$userPosts = $orm
->model(Post::class)
->whereRelation('author', $user)
->get();
Query all models related with the given models:
$specificUsers = $orm->model(User::class)->find([5, 15, 16]);
$specifitUsersPosts = $orm
->model(Post::class)
->whereRelation('author', $specificUsers)
->get();
Query all models having at least one related instance which fits a clause:
$usersWithOldPosts = $orm
->model(User::class)
->whereRelation('posts', function ($query) {
$query->where('date', '<', '2015-01-01');
})
->get();
You can even filter using a complex relation chain:
// All users having a post belonging to a category named "News" or "Events" (BTW, this is an example of many-to-many relation)
$reporters = $orm
->model(User::class)
->whereRelation('posts.category', function ($query) { // Relations are divided by dot
$query->where('name', 'News')->orWhere('name', 'Events');
})
->get();
You can also use the orWhereRelation
, whereNoRelation
and orWhereNoRelation
methods.
Attaching related models
Many to many
There is the attach
method to add attachments between models to the database:
$mapper->attach($post, 'tags', $tags);
Where tags
is a relation name of the Post
model.
It adds records to the pivot table even if the models are already attached.
If you need to replace all the $post
tags with the given tags, call the setAttachments
method:
$mapper->setAttachments($post, 'tags', $tags);
If you need to attach only such tags that are not attached, use the setAttachments
with the corresponding argument:
$mapper->setAttachments($post, 'tags', $tags, true);
It will make the database have all the previous and the new post tags and won't add duplicates.
You can set additional fields for the pivot table records (both methods support it):
$mapper->setAttachments($post, 'tags', $tags, false, function ($post, $tag, $postIndex, $tagIndex) {
return ['order' => $tagIndex];
});
Use the detach
method to detach models:
$mapper->detach($post, 'tags', $tags);
You can also detach all the post tags:
$mapper->detachAll($post, 'tags');
One to many
It doesn't support changing records in a database because there is an ambiguity, a detachment can be done multiple ways:
- "Many" side models are deleted
- "Many" side models get
null
as the foreign key field value
The decision depends on the business logic.
There are methods to help you do it manually. Set a foreign key field value:
$user = $orm->model(User::class)->find(16);
$post = new Post();
$post->title = 'The Post';
// ...
$post->associate('author', $user); // 'author' is the relation name defined in the Post class
$orm->save($post);
Unset a foreign key field value:
$post->dissociate('author'); // Or $post->associate('author', null);
$orm->save($post);
Warning! The associate
and dessociate
methods don't save models to the database, you need to do it manually.
Cyclic relations
Suppose there is a self related model like this:
use Finesse\Wired\Model;
use Finesse\Wired\Relations\HasMany;
class Category extends Model
{
public $id;
public $parent_id;
public $name;
// ...
public static function subcategories()
{
return new HasMany(self::class, 'parent_id');
}
public static function parent()
{
return new BelongsTo(self::class, 'parent_id');
}
}
Getting related models recursively
You can load all the categories tree:
$category = $orm->model(Category::class)->find(1);
$orm->loadCyclic($category, 'subcategories');
/*
$category->subcategories = [
Category(subcategories = [
Category(subcategories = [
...
]),
...
]),
Category(subcategories = [
...
]),
...
]
*/
Or the category parents chain:
$category = $orm->model(Category::class)->find(35);
$orm->loadCyclic($category, 'parent');
/*
$category->parent = Category(parent = Category(parent = ...))
*/
The loadCyclic
method supports all the other arguments supported by the load
method.