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');

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->post 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 on of 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.

If you need to attach a parent model to a child model you can use the helper method instead of setting a foreign key value manually:

$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);

It works only for BelongsTo relations. There is a method for detaching a parent model:

$post->dissociate('author');
$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');
    }
}

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 other load method arguments are supported by the loadCyclic method.