Модель Активной Записи

Введение

В OctoberCMS Вы можете использовать красивую и простую реализацию шаблона Active Record для работы с базой данных, которая основана на Eloquent ORM. Каждая таблица имеет соответствующий класс-модель, который используется для работы с этой таблицей. Модели позволяют запрашивать данные из таблиц, а также вставлять в них новые записи.

Класс модели находится в папке плагина в подпапке models. Пример:

plugins/
  acme/
    blog/
      models/
        user/             <=== Model config directory
          columns.yaml    <=== Model config files
          fields.yaml     <==^
        User.php          <=== Model class
      Plugin.php

Папка с настройками модели может содержать файлы с описанием столбцов списка и полей формы. Название этой папки совпадает с названием класса модели и должно быть написано строчными буквами.

Определение модели

В большинстве случаев, для каждой таблице в базе должен существовать класс модели, который должен расширять класс Model. Пример:

namespace Acme\Blog\Models;

use Model;

class Post extends Model
{
    /**
     * Название таблицы.
     *
     * @var string
     */
    protected $table = 'acme_blog_posts';
}

Свойство $table указывает на таблицу, соответствующей данной модели. Название таблицы состоит из имени автора, названия плагина и произвольного названия (которое должно быть осмысленным).

Стандартные свойства

Вы можете использовать следующие стандартные свойства в модели:

class User extends Model
{
    protected $primaryKey = 'id';

    public $exists = false;

    protected $dates = ['last_seen_at'];

    public $timestamps = true;

    protected $jsonable = ['permissions'];

    protected $guarded = ['*'];
}
Свойство Описание
$primaryKey первичный ключ с именем id.
$exists указывает на то, что модель существует.
$dates указанные атрибуты преобразуются в экземпляр объекта Carbon/DateTime после получения.
$timestamps автоматически устанавливает поля created_at и updated_at.
$jsonable указанные атрибуты кодируются в JSON перед сохранением и преобразуются в массивы после получения.
$fillable определяет, для каких атрибутов модели разрешено массовое назначение.
$guarded определяет, для каких атрибутов модели запрещено массовое назначение.
$visible определяет атрибуты, которые можно показать в преобразованном массиве модели.
$hidden скрывает атрибуты из преобразованного массива модели — например, пароль у модели User.

Первичные ключи

Модель предполагает, что каждая таблица имеет первичный ключ с именем id. Вы можете определить свойство $primaryKey для указания другого имени. Пример:

class Post extends Model
{
    /**
     * Первичный ключ модели.
     *
     * @var string
     */
    protected $primaryKey = 'id';
}

Отметки времени

По умолчанию модель ожидает наличия в ваших таблицах столбцов updated_at и created_at. Если вы не хотите, чтобы они автоматически обрабатывались, установите свойство $timestamps класса модели как false:

class Post extends Model
{
    /**
     * Определяет необходимость отметок времени для модели.
     *
     * @var bool
     */
    public $timestamps = false;
}

Если вы хотите изменить формат отметок времени, задайте свойство $dateFormat вашей модели. Это свойство определяет, как атрибуты времени будут храниться в базе данных, а также задаёт их формат при сериализации модели в массив или JSON:

class Post extends Model
{
    /**
     * @var string
     */
    protected $dateFormat = 'U';
}

JSON атрибуты

Значения указанных в свойстве $jsonable атрибутов кодируются в JSON перед сохранением и преобразуются в массивы после получения из базы данных:

class Post extends Model
{
    /**
     * @var array
     */
    protected $jsonable = ['data'];
}

Получение нескольких моделей

После создания модели и связанной с ней таблицы, вы можете начать получать значения из вашей базы данных. Каждая модель представляет собой мощный конструктор запросов, позволяющий удобно выполнять запросы к связанной таблице. Например:

$flights = Flight::all();

Доступ к значениям столбцов

Если у вас есть экземпляр модели, Вы можете обращаться к значениям столбцов модели, обращаясь к соответствующим свойствам. Например, давайте пройдёмся по каждому экземпляру Flight, возвращённому нашим запросом, и выведем значение столбца name:

foreach ($flights as $flight) {
    echo $flight->name;
}

Добавление дополнительных ограничений

Метод all возвращает все результаты из таблицы модели. Поскольку каждая модель работает как конструктор запросов, Вы можете также добавить ограничения в запрос, а затем использовать метод get для получения результатов:

$flights = Flight::where('active', 1)
    ->orderBy('name', 'desc')
    ->take(10)
    ->get();

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

Коллекции

Такие методы, как all и get, которые получают несколько результатов, возвращают экземпляр Collection. Этот класс предоставляет большое количество полезных методов для работы с результатами запроса. Само собой, Вы можете просто перебирать такую коллекцию в цикле как массив:

foreach ($flights as $flight) {
    echo $flight->name;
}

Разделение результата на блоки

Если вам нужно обработать тысячи записей, используйте команду chunk (блок — прим. пер.). Метод chunk получает модель частями, передавая их в Closure для обработки. Использование этого метода уменьшает используемый объём оперативной памяти:

Flight::chunk(200, function ($flights) {
    foreach ($flights as $flight) {
        //
    }
});

Первый передаваемый в метод аргумент — число записей, получаемых в одном "блоке". Передаваемая в качестве второго аргумента функция будет вызываться для каждого блока, получаемого из БД.

Получение одиночных моделей / агрегатные функции

Кроме получения всех записей указанной таблицы Вы можете также получить конкретные записи при помощи методов find и first. Вместо коллекции моделей эти методы возвращают один экземпляр модели:

// Retrieve a model by its primary key
$flight = Flight::find(1);

// Retrieve the first model matching the query constraints
$flight = Flight::where('active', 1)->first();

Исключения «Не найдено»

Иногда вам нужно возбудить исключение, если определённая модель не была найдена. Например в маршрутах или контроллерах. Методы findOrFail и firstOrFail получают первый результат запроса. А если результатов нет, то происходит исключение Illuminate\Database\Eloquent\ModelNotFoundException:

$model = Flight::findOrFail(1);

$model = Flight::where('legs', '>', 100)->firstOrFail();

Если исключение не поймано, пользователю автоматически посылается HTTP-отклик 404, поэтому нет необходимости писать явные проверки для возврата откликов 404 при использовании этих методов:

Route::get('/api/flights/{id}', function ($id) {
    return Flight::findOrFail($id);
});

Агрегатные функции

Вы также можете использовать агрегатные функции конструктора запросов, такие как count, max, sum и другие. Эти методы возвращают соответствующее скалярное значение вместо полного экземпляра модели:

$count = Flight::where('active', 1)->count();

$max = Flight::where('active', 1)->max('price');

Вставка и изменение моделей

Простые вставки

Для создания новой записи в БД просто создайте экземпляр модели, задайте атрибуты модели и вызовите метод save: $flight = new Flight; $flight->name = 'Sydney to Canberra'; $flight->save();

В этом примере мы просто создали экземпляр модели Flight и присвоили значение параметру name. При вызове метода save запись будет вставлена в таблицу. Отметки времени created_at и updated_at будут автоматически установлены, поэтому их не надо указывать вручную.

Простые изменения

Метод save можно использовать и для изменения существующей модели в БД. Для изменения модели сначала Вам нужно получить её, далее изменить необходимые атрибуты и вызвать метод save. Отметка времени updated_at будет установлена автоматически, поэтому её не надо задавать вручную:

$flight = Flight::find(1);
$flight->name = 'Darwin to Adelaide';
$flight->save();

Изменения можно выполнить для нескольких моделей, которые соответствуют указанному запросу. В этом примере все рейсы, которые отмечены как active и имеют destination равное San Diego, будут отмечены как delayed:

Flight::where('is_active', true)
    ->where('destination', 'Perth')
    ->update(['delayed' => true]);

Метод update ожидает массив пар столбец/значение, обозначающий, какие столбцы нужно изменить.

Массовое заполнение

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

Уязвимость массового заполнения проявляется, когда пользователь передаёт с помощью запроса неподходящий HTTP-параметр, и вы не ожидаете, что этот параметр изменит столбец в вашей БД. Например, злоумышленник может послать в HTTP-запросе параметр is_admin, который затем применяется к методу create вашей модели, позволяя пользователю повысить свои привилегии до администратора.

Поэтому, для начала нужно определить, для каких атрибутов разрешить массовое заполнение. Это делается с помощью свойства модели $fillable. Например, давайте разрешим массовое назначение атрибута name нашей модели Flight:

class Flight extends Model
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['name'];
}

Теперь мы можем использовать метод create для вставки новой записи в базу данных. Метод create возвращает сохранённый экземпляр модели:

$flight = Flight::create(['name' => 'Flight 10']);

Параметр $fillable служит «белым списком» атрибутов, для которых разрешено массовое назначение. А параметр $guarded служит «чёрным списком». Параметр $guarded должен содержать массив атрибутов, для которых будет запрещено массовое назначение. Атрибутам, не вошедшим в этот массив, будет разрешено массовое назначение. Само собой, вы должны использовать только один из этих параметров:

class Flight extends Model
{
    /**
     * The attributes that aren't mass assignable.
     *
     * @var array
     */
    protected $guarded = ['price'];
}

В этом примере всем атрибутам кроме price разрешено массовое назначение.

Вы также можете запретить массовое заполнение всем атрибутам, используя символ *.

Другие методы создания

Возможно, когда-нибудь Вам понадобится создать модель без ее сохранения. Для этого можно использовать метод make.

$flight = Flight::make(['name' => 'Flight 10']);

// Functionally the same as...
$flight = new Flight;
$flight->fill(['name' => 'Flight 10']);

Существует ещё два метода, которые можно использовать для создания моделей с помощью массового заполнения: firstOrCreate и firstOrNew. Метод firstOrCreate пытается найти запись БД, используя указанные пары столбец/значение. Если модель не найдена в БД, запись будет вставлена в БД с указанными атрибутами.

Метод firstOrNew как и firstOrCreate пытается найти в БД запись, соответствующую указанным атрибутам. Однако если модель не найдена, будет возвращён новый экземпляр модели. Учтите, что эта модель ещё не помещена в БД. Вам надо вызвать метод save вручную, чтобы сохранить её:

// Получить рейс по атрибутам или создать его, если он не существует
$flight = Flight::firstOrCreate(['name' => 'Flight 10']);

// Получить рейс по атрибутам, или создать новый экземпляр
$flight = Flight::firstOrNew(['name' => 'Flight 10']);

Удаление моделей

Используйте метод delete, чтобы удалить модель:

$flight = Flight::find(1);

$flight->delete();

Удаление модели по ключу

В предыдущем примере мы получили модель из БД перед вызовом метода delete. Но если вы знаете первичный ключ модели, вы можете удалить модель, не получая её. Для этого вызовите метод destroy:

Flight::destroy(1);

Flight::destroy([1, 2, 3]);

Flight::destroy(1, 2, 3);

Удаление модели запросом

Вы также можете выполнить запрос на удаление на наборе моделей. В следующем примере мы удалим все неактивные рейсы:

$deletedRows = Flight::where('active', 0)->delete();

Примечание: Важно отметить, что при таком способе удаления события не сработают.

Ограничения запросов

Ограничения позволяют Вам определить набор условий, который Вы можете использовать в приложении. Например, если Вам часто требуется получать пользователей, которые сейчас «популярны». Для создания заготовки просто начните имя метода с префикса scope:

class User extends Model
{
    /**
     * Scope a query to only include popular users.
     */
    public function scopePopular($query)
    {
        return $query->where('votes', '>', 100);
    }

    /**
     * Scope a query to only include active users.
     */
    public function scopeActive($query)
    {
        return $query->where('is_active', 1);
    }
}

Использование ограничений запросов

Когда Вы определили ограничения, то можете вызывать нужный метод при запросах к модели. Но теперь Вам не нужно использовать префикс scope, например:

$users = User::popular()->active()->orderBy('created_at')->get();

Вы даже можете сцеплять вызовы разных ограничений.

Динамические ограничения

Иногда Вам может потребоваться определить ограничения, которые принимают параметры. Для этого, просто, добавьте нужные параметры в метод после аргумента $query:

class User extends Model
{
    /**
     * Пример запроса пользователей определённого типа.
     */
    public function scopeApplyType($query, $type)
    {
        return $query->where('type', $type);
    }
}

А затем передайте их при вызове метода:

$users = User::applyType('admin')->get();

События

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

Event Description
beforeCreate перед сохранением модели (при создании).
afterCreate после сохранения модели (при создании).
beforeSave перед сохранением модели (при создании или обновлении).
afterSave после сохранения модели (при создании или обновлении).
beforeValidate перед валидацией модели.
afterValidate после валидации модели.
beforeUpdate перед обновлением модели.
afterUpdate после обновления модели.
beforeDelete перед удалением модели.
afterDelete после удаления модели.
beforeRestore перед восстановлением модели.
afterRestore после восстановления модели.
beforeFetch перед заполнением модели.
afterFetch после заполнения модели.

Пример:

/**
 * Генерируем URL
 */
public function beforeCreate()
{
    $this->slug = Str::slug($this->name);
}

Основы использования

Когда новая модель сохраняется впервые, возникают события beforeCreate и afterCreate. Если модель уже существовала на момент вызова метода save, вызываются события beforeUpdate / afterUpdate. В обоих случаях также возникнут события beforeSave / afterSave.

Например, давайте определим слушателя событий, заполняющий атрибут slug при создании модели:

/**
 * Генерируем URL
 */
public function beforeCreate()
{
    $this->slug = Str::slug($this->name);
}

Если обработчики creating, updating, saving или deleting вернут значение false, то действие будет отменено:

public function beforeCreate()
{
    if (!$user->isValid()) {
        return false;
    }
}

Вы можете использовать метод bindEvent, чтобы связать локальные события с экземпляром модели. Название метода должно быть таким же, как и название переопределяемого метода с префиксом model..

$flight = new Flight;
$flight->bindEvent('model.beforeCreate', function() use ($model) {
    $model->slug = Str::slug($model->name);
})

Расширение моделей

Так как модели имеют поведение, то они могут быть расширены при помощи метода extend(). Пример:

User::extend(function($model) {
    $model->hasOne['author'] = ['Author', 'key' => 'user_id'];
});

User::extend(function($model) {
    $model->bindEvent('model.beforeSave', function() use ($model) {
        // ...
    });
});