Laravel. Отношения в Eloquent


Будем экспериментировать со структурой таблиц блога: Post, Category, Tag и Comment.

Нужно придерживаться определенных правил. В этом случае не нужно будет указывать названия связывающих полей. Например один пост может иметь несколько тегов, а один тег несколько постов.

// название таблицы поста
posts
// название таблицы тега
tags
// название связывающей таблицы
post_tag
// названия полей в этой таблицы
post_id - связь с постом
tag_id - связь с тегом

Если придерживаться этих правил, то не нужно будет указывать в методах hasOne(), belongsTo(), hasMany() и belongsToMany() нужные названия связывающей таблицы или названия полей.

Модели и миграции

// Создание модели Post
php artisan make:model Post
// Миграция для Post
php artisan make:migration create_posts_table
// Модель+миграция Category
php artisan make:model Category -m
// Модель+миграция Tags
php artisan make:model Tag -m
// Связывающая таблица постов и тегов
php artisan make:migration create_post_tag_table
// Модель+миграция Comment
php artisan make:model Comment -m
// Post
class CreatePostsTable extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('text');
            $table->unsignedBigInteger('category_id')->nullable();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

// Category
class CreateCategoriesTable extends Migration
{
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->timestamps();
        });

        Schema::table('posts', function (Blueprint $table) {
            $table->foreign('category_id')->references('id')->on('categories')->onDelete('set null');
        });
    }

    public function down()
    {
        Schema::table('posts', function (Blueprint $table) {
            $table->dropForeign(['category_id']);
        });

        Schema::dropIfExists('categories');
    }
}

// Tag
class CreateTagsTable extends Migration
{
    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('tags');
    }
}

// post_tag
class CreatePostTagsTable extends Migration
{
    public function up()
    {
        Schema::create('post_tags', function (Blueprint $table) {
            $table->unsignedBigInteger('post_id');
            $table->unsignedBigInteger('tag_id');
            $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
            $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
        });
    }

    public function down()
    {
        Schema::dropIfExists('post_tags');
    }
}

// Comment
class CreateCommentsTable extends Migration
{
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->id();
            $table->string('text');
            $table->unsignedBigInteger('commentable_id');
            $table->string('commentable_type');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('comments');
    }
}

Заполнение таблиц

Для заполнения создал один seeder BlogSeeder

php artisan make:seeder BlogSeeder

Код файла

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use App\Models\Post;
use App\Models\Comment;

class BlogSeeder extends Seeder
{
    public function run()
    {
        // Категории
        DB::table('categories')->insert([
            [
                'id' => 1,
                'title' => 'HTML/CSS'
            ],
            [
                'id' => 2,
                'title' => 'Javascript'
            ],
            [
                'id' => 3,
                'title' => 'PHP'
            ]
        ]);

        // Теги
        DB::table('tags')->insert([
            [
                'id' => 1,
                'title' => 'Разработка'
            ],
            [
                'id' => 2,
                'title' => 'Frontend'
            ],
            [
                'id' => 3,
                'title' => 'Backend'
            ]
        ]);

        // Посты
        Post::create([
            'title' => 'PHP',
            'text' => 'PHP - очень популярный язык программирования',
            'category_id' => 3
        ]);

        Post::create([
            'title' => 'Верстка',
            'text' => 'Для верстки веб-страниц используют HTML, CSS и Javascript.',
            'category_id' => 1
        ]);

        Post::create([
            'title' => 'Разработка сайтов',
            'text' => 'Для разработки сайта нужны знания в области frontend и backend.',
            'category_id' => 3
        ]);

        Post::create([
            'title' => 'Laravel',
            'text' => 'Laravel - Очень популярный фреймворк.',
            'category_id' => 3
        ]);

        Post::create([
            'title' => 'HTML',
            'text' => 'HTML - это не язык программирования, а язык гипертекстовой разметки веб-страниц.',
            'category_id' => 1
        ]);

        // Связка постов и тегов
        DB::table('post_tag')->insert([
            [
                'post_id' => 1,
                'tag_id' => 1
            ],
            [
                'post_id' => 1,
                'tag_id' => 3
            ],
            [
                'post_id' => 2,
                'tag_id' => 2
            ],
            [
                'post_id' => 3,
                'tag_id' => 1
            ],
            [
                'post_id' => 3,
                'tag_id' => 3
            ],
            [
                'post_id' => 4,
                'tag_id' => 1
            ],
            [
                'post_id' => 4,
                'tag_id' => 3
            ],
            [
                'post_id' => 5,
                'tag_id' => 2
            ],
        ]);

        // Комментарии
        Comment::create([
            'text' => 'PHP - лучший язык',
            'commentable_id' => 1,
            'commentable_type' => Post::class,
        ]);

        Comment::create([
            'text' => 'Laravel - лучший фреймворк',
            'commentable_id' => 4,
            'commentable_type' => Post::class,
        ]);
    }
}

Добавляем в файл DatabaseSeeder.php:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $this->call(BlogSeeder::class);
    }
}

И выполняем команду:

php artisan db:seed

А теперь можно перейти к теме поста.

Один к одному

У поста может быть только одна категория. Причем поле category_id содержится в таблице постов. Поэтому нужно использовать метод belongsTo()

class Post extends Model
{
    // ...

    public function category()
    {
        return $this->belongsTo(Category::class);
    }
}

Посты и категории можно связывать по другому. Если из таблицы posts убрать category_id и добавить поле post_id в таблицу categories, то нужно будет использовать метод hasOne() (вместо belongsTo()).

Примеры

// получение названия категории
$post = Post::find(2);
echo $post->category->title;

// список постов
$posts = Post::get();
foreach ($posts as $post) {
    dump([
        'title' => $post->title,
        'category' => [
            'title' => $post->category->title
        ]
    ]);
}

Здесь используется отложенная (ленивая) загрузка, т.е. в каждом цикле будет выполнятся запрос для получения информации о категории. Чтобы этого не было, нужно применить безотлагательную (жадную) загрузку:

$posts = Post::with('category')->get();

Один ко многим

У одной категории может быть много постов. Поэтому воспользуемся методом hasMany().

class Category extends Model
{
    // ...

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

Пример

$categories = Category::with('posts')->get();
foreach ($categories as $category) {
    dump([
        'title' => $category->title,
        // получаем список постов текущей категории
        'posts' => Arr::pluck($category->posts, 'title')
    ]);
}

Многие ко многим

В модели Post достаточно добавить следующий код:

class Post extends Model
{
    // ...

    public function tags()
    {
        return $this->belongsToMany(Tag::class);
    }
}

Пример

$posts = Post::with('tags')->get();
foreach ($posts as $post) {
    dump([
        'title' => $post->title,
        'tags' => Arr::pluck($post->tags, 'title')
    ]);
}

Полиморфные связи

Рассмотрим этот способ на основе популярного практического примера. Почти на каждом сайте есть комментарии (или отзывы). И хотелось бы использовать одну таблицу комментариев для разного контента. Например posts, news, articles, products и т.д. Как раз эту задачу решает полиморфные связи.

В таблицу комментариев нужно добавить 2 поля:

  • commentable_id (int) - id контента
  • commentable_type (string) - имя класса моделя (например Post::class)

Допустим мы добавили еще одну таблицу articles. Вот как будет выглядет модели:

class Comment extends Model
{
    // ...

    public function commentable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    // ...

    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class Article extends Model
{
    // ...

    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

Примеры

// комментарии поста
$posts = Post::get();
foreach ($posts as $post) {
    dump([
        'title' => $post->title,
        'comments' => Arr::pluck($post->comments, 'text')
    ]);
}

// комментарии статей
$articles = Article::get();
foreach ($articles as $article) {
    dump([
        'title' => $article->title,
        'comments' => Arr::pluck($article->comments, 'text')
    ]);
}

Прежде чем написать комментарий, нужно авторизаться на сайте