Блог на Laravel: Часть 2. Работаем с хранилищем. Модели

Опубликовано 0 комментариев 7620 просмотров
Блог на Laravel: Часть 2. Работаем с хранилищем. Модели

Это - вторая статья из цикла 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.

В ответ на сообщение

Доступна разметка Markdown. А еще вы можете использовать крутой пак эмоций.

Нажимая на кнопку «Отправить» вы даете свое согласие на обработку персональных данных в соответствии с законом №152-ФЗ «О персональных данных» от 27.07.2006 и принимаете условия Политики конфеденциальности.