
Это - вторая статья из цикла Laravel Simple Blog. В прошлый раз мы создали структуру для наших данных в БД. Сегодня я расскажу о том, как правильно начать использовать их в Laravel.
Модели - основа фреймворка. Они используются для создания, удаления, чтения, редактирования данных из хранилища, в нашем случае - MySQL. Простым языком - это класс, который используется для работы с данными.
В Laravel все модели являются наследниками Illuminate\Database\Eloquent\Model
. Название таблицы в базе определяется как множественное число от название модели: для Post
это posts
, для Tag
- tags
и далее. Это стандарт, которого следует придерживаться.
Под все ранее созданные таблицы мы напишем модели. Исключим только post_tag
, которая является промежуточной для связи и явно использовать которую мы не будем.
Дисклеймер: все файлы, с которыми мы работаем хранятся в app
, а неймспейсы всегда начинаются с App\
. Для хранения моделей мы создадим папку app/Models
, таким образом общий неймспейс будет App\Models
.
Описание моделей
Post
Модель для записей. Сразу вставлю готовый код, а потом расскажу обо всем подробней.
// app/Models/Post.php
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Class Post
* @package App\Models
*
* @property-read int $id
* @property string $name
* @property string $slug
* @property bool $publish
* @property string|null $image
* @property int $category_id
* @property string $preview_text
* @property string $detail_text
* @property Carbon $created_at
* @property Carbon $updated_at
*
* @property Category $category
* @property array|Collection|Tag[] $tags
* @property array|Collection|Comment $comments
* @property Meta $meta
* @method static Builder publish()
* @method Builder whereSlug(string $slug)
*/
class Post extends Model
{
protected $fillable = [
'name', 'slug', 'publish', 'image', 'category_id', 'preview_text', 'detail_text'
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function category()
{
return $this->belongsTo(Category::class);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function tags()
{
return $this->belongsToMany(Tag::class);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function comments()
{
return $this->hasMany(Comment::class);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function meta()
{
return $this->hasOne(Meta::class);
}
/**
* @param Builder $query
*/
public function scopePublish(Builder $query)
{
$query->where('publish', 1);
}
/**
* @param Builder $query
* @param string $slug
*/
public function scopeWhereSlug(Builder $query, string $slug)
{
$query->where('slug', $slug);
}
/**
* @param $value
* @return string
*/
public function getImageAttribute($value)
{
return !$value ?: \Storage::url($value);
}
}
Первое, и самое важное на мой взгляд правило - всегда используйте PHPDoc. Получить/изменить атрибуты у модели возможно как через методы getAttribute
/setAttribute
, так и обращаясь к ним как к свойствам класса (__get
/__set
). Описание магических свойств и методов сильно облегчит жизнь в дальнейшем.
Защищенное свойство $fillable
- массив с атрибутами, эдакий белый список атрибутов, которые возможно устанавливать во время массового добавления и изменения (методы create
и update
, о них позже). Антоним - свойство $guarded
, значения которого никогда не попадут в хранилище. Выбирайте сами что больше подходит, но как показывает практика - $fillable
является более надежным вариантом.
Методы category
, tags
, comments
, metas
- отношения, предназначены для связки текущей модели с другими - категорией и тегами соответсвенно. При обращении к ним как к методам будет возвращен конструктор запроса, как к свойствам - полученные данные. Так как названия моделей и колонок в таблице не отличаются от стандартных, ожидаемых отношением, кроме указания класса модели ничего больше не требуется.
Например, отношение category
, один к одному , если категория существует вернет экземпляр модели Category
, у которой id
равен category_id
поста. Вызов category
как метода вернет экземпляр Illuminate\Database\Eloquent\Relations\HasOne
, работающей как конструктор запроса.
Отношение tags
, многие ко многим, вернет коллекцию (очень мощная штука, как многие говорят - "массивы на стероидах") с Tag
, у которых в таблице post_tag
существует отношение с данной записью (при обращении как к методу - экземпляр Illuminate\Database\Eloquent\Relations\BelongsToMany
).
Следуя паттерну ключи для поиска отношений формируются из названия метода. Для метода category
это category_id
, для posts
- post_id
и так далее. Существует возможность указать ключи вручную.
В фреймворке доступны следующие виды отношений: один к одному hasOne
/belongsTo
, один ко многим hasMany
/belongsTo
, многие ко многим belongsToMany
, многие ко многим через промежуточную таблицу hasManyThrough
, полиморфные morphTo
/morphMany
, полиморфные многие ко многим morphToMany
/morphedByMany
. Это все очень интересно но углубляться пока что не будем, так что просто оставлю ссылку на документацию.
Скоупы - еще одна мощная фишка моделей. Вместо того, чтобы каждый раз в контроллере при работе с данными указывать части запроса, можно указать их прямо в модели в виде скоупа. Например, в видимой части нам везде нужны только опубликованные записи (publish = 1
). Написание в каждом контроллере условия where('publish', 1)
мы заменим на вызов метода publish
, а в модели укажем скоуп
public function scopePublish(Builder $query)
{
$query->where('publish', 1);
}
Другой вариант - скоуп с аргументами. Конструкцию where('slug', $slug)
мы заменим на более красивую whereSlug($slug)
public function scopeWhereSlug(Builder $query, string $slug)
{
$query->where('slug', $slug);
}
Это поможет заменить кучу правок на изменения одной строки в случае когда структура таблице будет изменена. Например, если нам захочется иметь несколько статусов (publish, draft, awaiting) мы просто изменим содержимое скоупа вместо правок в публичных контроллерах.
Еще один мощный инструмент - мутаторы. Предположим, мы будем хранить одно значение, а на выходе нам потребуется совершенно другое. Часть ссылки на изображение сохранена в базе, а полную получаем при помощи Storage::url
. В этом нам и помогут мутаторы. Создадим метод get{AttributeName}Attribute
, и при получении атрибута получим конечное значение
public function getImageAttribute($value)
{
return $value ?: \Storage::url($value);
}
Кроме изменения существующих мы можем создавать новые. Например, для части операций необходимо ограничить максимальный размер preview_text
, не изменяя исходное значение. Мутатор решит эту задачу
public function getShortPreviewTextAttribute()
{
return str_limit($this->preview_text, 30);
}
И при обращении к $model->short_preview_text
мы получим необходимый результат.
Category
// app/Models/Category.php
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Class Category
* @package App\Models
*
* @property-read int $id
* @property string $name
* @property string $null
* @property string|null $description
* @property Carbon $created_at
* @property Carbon $updated_at
*
* @property array|Collection|Post[]
* @method Builder whereSlug(string $slug)
*/
class Category extends Model
{
protected $fillable = [
'name', 'slug', 'description'
];
/**
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function posts()
{
return $this->hasMany(Post::class);
}
/**
* @param Builder $query
* @param string $slug
*/
public function scopeWhereSlug(Builder $query, string $slug)
{
$query->where('slug', $slug);
}
}
В $fillable
указываем доступные для заполнения атрибуты, в posts
- обратное отношение, получающее коллекцию записей с данным тегом.
Tag
// app/Models/Tag.php
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Class Tag
* @package App\Models
*
* @property-read int $id
* @property string $name
* @property string $slug
* @property Carbon $created_at
* @property Carbon $updated_at
*
* @property array|Collection|Post[] $posts
* @method Builder whereSlug(string $slug)
*/
class Tag extends Model
{
protected $fillable = [
'name', 'slug'
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function posts()
{
return $this->belongsToMany(Post::class);
}
/**
* @param Builder $query
* @param string $slug
*/
public function scopeWhereSlug(Builder $query, string $slug)
{
$query->where('slug', $slug);
}
}
Meta
// app/Models/Meta.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* Class Meta
* @package App\Models
*
* @property-read int $id
* @property int $post_id
* @property string|null $description
* @property string|null $keywords
*
* @property Post $post
*/
class Meta extends Model
{
public $timestamps = false;
protected $fillable = [
'post_id', 'description', 'keywords'
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function post()
{
return $this->belongsTo(Post::class);
}
}
В модели появилось новое свойство - $timstamps
. Возможно вы не заметили, но в миграциях в таблицах metas
и post_tag
мы не добавляли колонки с временными метками. Во втором случае, который является лишь связующим звеном, нам они и не нужны. metas
, будучи лишь "продолжением" основной таблицы с записями, тоже не имеет в них нужды.
Но по-умолчанию при создании или изменении данных фреймворк добавляет в модель колонку created_at
и обновляет updated_at
. Для управления данным механизмом существует публичное свойство $timestamp
, равное true
по умолчанию. Мы отключили его, добавив значение false
.
Comment
// app/Models/Comment.php
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Class Comment
* @package App\Models
*
* @property-read int $id
* @property string $name
* @property string $email
* @property bool $status
* @property int $post_id
* @property int|null $reply_id
* @property string $comment
* @property string $ip
* @property Carbon $created_at
* @property Carbon $updated_at
*
* @property Post $post
* @property Comment|null $reply
* @method static Builder publish()
*/
class Comment extends Model
{
protected $fillable = [
'name', 'email', 'status', 'post_id', 'reply_id', 'comment', 'ip'
];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function post()
{
return $this->belongsTo(Post::class);
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function reply()
{
return $this->belongsTo(self::class);
}
/**
* @param Builder $query
*/
public function scopePublish(Builder $query)
{
$query->where('publish', 1);
}
}
Получение данных
Все доступные методы вряд ли имеет смысл описывать, так как всегда есть документация. Чуть более подробно мы столкнемся с этим в одной из следующий записей, когда будем настраивать контроллеры. Пока что хватит минимальной информации.
Для генерации запроса существует множество методов, позволяющих использовать как конструктор так и чистые RAW данные. Например, данный запрос получит первый публичный пост, опубликованный в течении последнего года:
$post = App\Models\Post::publish()
->where('created_at', '>', Carbon\Carbon::now()->subYear())
->whereRaw('1 = 1')
->orderBy('created_at')
->firstOrFail();
// select * from `posts` where `publish` = ? and `created_at` > ? and 1 = 1 order by `created_at` asc limit 1
Тот же самый запрос, но получающий 20 постов, созданных за последний год в порядке от старого к новому:
$posts = App\Models\Post::publish()
->where('created_at', '>', Carbon\Carbon::now()->subYear())
->orderBy('created_at')
->limit(20)
->get();
// select * from `posts` where `publish` = ? and `created_at` > ? order by `created_at` asc limit 20
При использовании метода firstOrFail
мы получим модель с записью, в случае с get
- коллекцию со списком моделей.
Основные методы:
where($column, $operator = null, $value = null, $boolean = 'and')
/orWhere($column, $operator = null, $value = null)
/whereIn($column, $values, $boolean = 'and', $not = false)
и т.д. - добавлет условия для фильтрацииorderBy($column, $direction = 'asc')
/orderByDesc($column)
- сортирует результатselect($columns = ['*'])
- указывает, какие колонки загружатьgroupBy(...$groups)
- группировка результатовfind($id, $columns = ['*'])
- получение строки по ее ид.findOrFail
в случае, если ничего не найдено вызывает соответствующую ошибкуget($columns = ['*'])
- получает все подходящие строкиpaginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null)
- метод, инициирующий постраничный вывод результатовfirst($columns = ['*'])
выводит первую подходящую строку.firstOrFail
, аналогично с вариантом выше, вызывает ошибку если ничего не найдено
Создание/редактирование/удаление данных
Создать запись можно несколькими способами:
// Если данные в виде массива
$post = App\Models\Post::create([
'name' => 'lorem ipsum',
'slug' => str_slug('lorem ipsum'),
'publish' => 1,
'image' => 'posts/lipsum.png',
'category_id' => 1,
'preview_text' => 'li',
'detail_text' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
]);
// Присваиваем каждое значение каждого атрибута отдельно
$post = new App\Models\Post();
$post->name = 'lorem ipsum';
$post->slug = str_slug($post->name);
$post->publish = 1;
$post->image = 'posts/lipsum.png';
$post->category_id = 1;
$post->preview_text = 'li';
$post->detail_text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
$post->save();
Изменение аналогично созданию:
// Для массового обновления
App\Models\Post::publish()
->update([
'publish' => 0
]);
// Или обновления уже полученного элемента
$post = App\Models\Post::publish()
->firstOrFail();
$post->publish = 0;
$post->save();
За удаление отвечает метод delete
:
// Для массового удаления
App\Models\Post::where('publish', 0)
->delete();
// Удаляем уже полученный элемент
$post = App\Models\Post::where('publish', 0)
->firstOrFail();
$post->delete();
На этом все. Если возникли вопросы - добро пожаловать в комментарии :) Найти остальные записи можно по тегу Laravel Simple Blog.
Тут никто еще ничего не написал. Хочешь быть первым?