Будем экспериментировать со структурой таблиц блога: 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')
]);
}
Прежде чем написать комментарий, нужно авторизаться на сайте