Модели Eloquent

Сегодня посмотрим как работать с нашими новыми таблицами, созданными в прошлом уроке.

Модели - это PHP классы, лежащие в core/src/Models, расширяющие Illuminate\Database\Eloquent\Model и представляющие собой записи в соответствующей таблице базы данных. В отличие от MODX и его xDPO, здесь не нужно писать никаких схем и генерировать непонятные map файлы. Одна модель - это всегда один класс и одна таблица в БД, всё очень просто и понятно. Если миграции меняют таблицу, то эти изменения нужно будет отразить и в модели.

Vesp устанавливает 4 модели по умолчанию: File, UserRole, User, UserToken - они отражают записи в таблицах files, user_roles, users и user_tokens соответственно.

Как видно, имена моделей чётко соотносятся с таблицами согласно правилам английского языка - и это не случайно. В Eloquent приняты определённые соглашения по многим ключевым моментам, включая имена моделей и таблиц.

Устройство моделей

Давайте посмотрим основные свойства модели на примере User:

<?php
// Наше пространство имён для моделей
namespace App\Models;

// Импорт нужных классов для удобства
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

/**
 * Здесь мы перечисляем все колонки таблицы с их типами 
 * для автодополнения IDE
 * @property int $id
 * @property string $username
 * @property string $password
 * @property int $role_id
 * @property bool $active
 * @property Carbon $created_at
 * @property Carbon $updated_at
 *
 * Здесь перечислены связи таблицы, их нельзя перезаписывать, только чтение
 * Роль у пользователя может быть только одна
 * @property-read UserRole $role
 * А вот токенов несколько, поэтому в типе указан массив моделей
 * @property-read UserToken[] $tokens
 */
class User extends Model
{
    // Основные свойства модели
    protected $table = 'users'; // имя таблицы без префикса
    protected $primaryKey = 'id'; // Имя первичного ключа
    protected $keyType = 'int'; // Тип ключа - число
    public $incrementing = true; // Ключ автоматически увеличивается на 1
    public $timestamps = true; // У модели есть колонки created_at и updated_at

    // Массив с колонками, которые можно массово менять у модели через метод ->fill()
    protected $fillable = ['username', 'password', 'role_id', 'active'];
    // Скрытые колонки, которые прячутся при распечатке модели через ->toArray()
    protected $hidden = ['password'];

    // Массив с приведением типов колонок, например
    // active станет не int как в MySQL, а именно boolean
    protected $casts = ['active' => 'boolean'];

    // Дальше связи с другими моделями, помните мы указывали ключи в миграциях? 
    // Это вот они, да.

    // Данная модель является дочерней по отношениею к UserRole
    // то есть, принадлежит (belongs) к роли
    public function role(): BelongsTo
    {
        // 2й и 3й параметр указаны для примера, 
        // Eloquent и сам прекрасно понимает, что связь role должна использовать 
        // колонку role_id, и присоединяться к id у UserRole
        return $this->belongsTo(UserRole::class, 'role_id', 'id');
    }

    // А здесь связь в обратную сторону - много токенов принадлежат одному User
    // То есть юзер имеет много (has many) токенов
    public function tokens(): HasMany
    {
        // Принадлежащими считаются все UserToken, 
        // где колонка user_id равна id модели User
        // Второй параметр здесь я тоже написал для примера
        return $this->hasMany(UserToken::class, 'user_id');
    }
}

В этом примере я соединил базововый Vesp\Models\User и расширяющий её App\Models\User для наглядности. Так же я специально вывел несколько параметров, которые обычно не пишут, потому что они работают по умолчанию. Например имя таблицы прекрасно определяется автоматически множественным числом слова user -> users.

В обычной типовой модели перичный ключ всегда id, и он всегда является числом (int). но давайте посмотрим на немного необычную модель UserToken:

<?php

namespace App\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

/**
 * @property string $token
 * @property int $user_id
 * @property Carbon $valid_till
 * @property string $ip
 * @property bool $active
 * @property Carbon $created_at
 * @property Carbon $updated_at
 *
 * @property-read User $user
 */
class UserToken extends Model
{
    // Первичный ключ уже не увеличивается автоматически
    public $incrementing = false;
    // Да и колонка для него не id
    protected $primaryKey = 'token';
    // А тип ключа - строка
    protected $keyType = 'string';

    // Вместо fillable, который разрешает массово обновлять колонки, указан guarded,
    // который запрещает обновлять колонки. Это значит, что все колонки, 
    // не указанные здесь, обновлять можно. 
    // Можно указывать или fillable, или guarded, оновременно - нельзя.
    protected $guarded = ['created_at', 'updated_at'];

    // В свойство колонок с датами добавлена и наша - valid_till. 
    // Это значит, что в PHP эта колонка будет экземпляром 
    // библиотеки для работы с датами Carbon
    protected $dates = ['valid_till'];

    protected $casts = ['active' => 'boolean'];

    // Каждый токен принадлежат своему User
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

Основное отличие этой модели в том, что у неё вовсе нет id, её первичный ключ - это уникальный token, который используется для авторизации пользователя в админке. Эту модель нельзя сохранить без явного указания колонки token. На следующем уроке мы поговорим о проверке прав доступа в контроллерах.

Работа с моделями

Сразу нужно опеределиться, что есть 2 способа работы: на уровне БД, когда вы строите и выполняете SQL запрос, и на уровне собственно модели - когда вы создаёте экземпляр класса модели и вызываете его методы.

Продемонстрирую разницу на примерах:

// Здесь мы генерируем и выполняем SQL запрос без получения модели
// UPDATE app_users SET fullname = 'test' WHERE id = 1;
User::query()->where('id', 1)->update(['fullname' => 'test']);


// А здесь мы получаем модель
// SELECT * FROM app_users WHERE id = 1;
// Создаём новый экземпляр класса User и забиваем его полученными значениями
if ($user = User::query()->find(1)) {
    // Потом меняем атрибут внутри класса
    $user->fullname = 'test';
    // И сохраняем данные обратно в таблицу
    // UPDATE app_users SET fullname = 'test' WHERE id = 1;
    $user->save();
}

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

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

// Получаем юзера с id = 1
if ($user = User::query()->find(1)) {
    // И удаляем всего его токены через SQL запрос
    // DELETE FROM app_user_tokens WHERE user_id = 1;
    $user->tokens()->delete();
}

Обратите внимание, что $user->tokens() создаёт SQL запрос, выбирающий все токены конкретного пользователя, а $user->tokens вернёт коллекцию моделей токенов:

// Получаем юзера с id = 1
if ($user = User::query()->find(1)) {
    // И удаляем всего его токены через перебор моделей UserToken
    foreach ($user->tokens as $token) {
        $token->delete();
    }
}

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

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

$user = new User();
$user->fill([
    'username' => 'new_user',
    'password' => 'new_password',
    'role_id' => 2,
    'fullname' => 'New User',
]);
$user->save();

И вот еще один пример разницы в методах работы. Давайте посмотрим, что получится, если мы создадим юзера через SQL:

User::query()->insert([
    'username' => 'new_user2',
    'password' => 'new_password2',
    'role_id' => 2,
    'fullname' => 'New User 2',
]);

Вот результат: пароль не захэширован, колонки с датами пустые.

Это произошло потому, что мы раз мы не создавали экземпляр модели User, то и всякие хитрости, прописанные в ней, не работали. Мы просто сделали INSERT INTO app_users VALUES username = 'new_user2' ....

Заключение

Теперь вы должны чётко понимать алгоритм создания новых сущностей в системе:

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

Если же нам нужно изменить таблицу, то

  1. миграция с изменениями таблицы
  2. отражение изменений в модели: новые связи или колонки

И это не двойная работа, как может показаться в начале, это именно что работа на 2х разных уровнях: SQL и PHP. Миграции создают и меняют таблицы в базе данных, а модели работают с этими таблицами из вашего кода.

Многие сейчас наверное задумались, что можно же автоматизировать здесь что-то, например генерировать модели по готовым таблицам? Я тоже так думал, но подходящих решений не нашёл. Если они и есть, то привязаны ко всему Laravel, а не только Eloquent.

А учитывая, что в наших моделях могут быть не только колонки и связи, а еще и собственные функции - смысл в подобной автоматизации теряется.

В той же модели User, например, обязательно хэшируется пароль:

public function setAttribute($key, $value)
{
    if ($key === 'password') {
        $value = password_hash($value, PASSWORD_DEFAULT);
    }

    return parent::setAttribute($key, $value);
}

Более подробно про работу с Eloquent можно почитать в документации - там много интересного, библиотека не зря стала фактическим стандартом работы с БД в PHP. Мы же будем постоянно её использовать в наших контроллерах, о которых я расскажу на следующием уроке.

← Предыдущая заметка
Разбираемся с миграциями
Следующая заметка →
Контроллеры Vesp
Комментарии (18)
bezumkinВасилий Наумкин
02.06.2022 16:57

Да, конечно.

Пока общая теория, дальше будем писать своё - и всё станет проще.

bezumkinВасилий Наумкин
02.06.2022 17:06

Стараюсь писать почаще - мне и самому интересно рассказывать!

bezumkinВасилий Наумкин
10.06.2022 15:18

Очень рад, что тебе нравится -)

bezumkinВасилий Наумкин
30.10.2022 06:59

Если ты используешь сокращённое имя модели, то в начале файла должено быть use App\Models\Post;

PhpStorm не просто так выделяет модель жёлтым цветом, а потому что видит эту ошибку. Еще советую поднять версию PHP в проекте, чтобы он не подчёркивал : void у функций.

bezumkinВасилий Наумкин
03.11.2022 17:57

Поискать в исходниках ссылки на её адрес и поменять - скорее всего только nuxt.config.js.

А зачем это надо? Если речь про безопасность, то не нужно прятать, нужно защищать через web-сервер, потому что любой адрес админки найдут очень быстро, простым перебором.

bezumkinВасилий Наумкин
26.04.2023 16:29

Текст ошибки ты читать не планируешь, правильно понимаю?

Там почему-то вместо авторизации идёт создание нового юзера и ошибка говорит о том, что такой юзер уже есть.

Даже представить не могу, как вместо авторизации у тебя получился запрос на создание нового юзера.

bezumkinВасилий Наумкин
27.04.2023 07:54

Очевидно, что я делаю что-то неправильно

Очевидно, да.

Где-то в моих заметках написано, что нужно писать произвольный код в модели юзера, после класса? Это настолько неправильно, что у меня даже слов цензурных нет.

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

Либо выполнял команды composer db:migrate, composer db:seed

Только так, да - создание новых юзеров через db:seed.

после этого пользователь также появлялся в базе и зайти в админку сразу не получается.

Почему?

bezumkinВасилий Наумкин
27.04.2023 15:16

Т.е. где должен запускаться этот код, если использовать его внутри модели не правильно?

В сиде. Или в каком-то любом другом логически обоснованном месте, но не в модели точно.

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

bezumkin
Василий Наумкин
09.04.2024 01:45
Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. Во...
futuris
Futuris
04.04.2024 05:56
Я просто немного запутался. Когда в абзаце &quot;Vesp/Core&quot; ты пишешь про &quot;новый trait Fil...
bezumkin
Василий Наумкин
20.03.2024 18:21
Volledig!
Андрей
14.03.2024 10:47
Василий! Как всегда очень круто! Моё почтение!
russelgal
russel gal
09.03.2024 17:17
А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал ...
inetlover
Александр Наумов
27.01.2024 00:06
Василий, спасибо! Извини, тупанул.
bezumkin
Василий Наумкин
22.01.2024 04:43
Давай-давай!
bezumkin
Василий Наумкин
24.12.2023 11:26
Спасибо!
bezumkin
Василий Наумкин
27.11.2023 02:43
Ура!
bezumkin
Василий Наумкин
25.11.2023 08:30
Vesp тянет 2 зависимости: vesp-frontent для фронта и vesp-core для бэкенда. Их можно обновлять, но э...