Модели 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' ....
Заключение
Теперь вы должны чётко понимать алгоритм создания новых сущностей в системе:
- сначала миграция с описанием всех колонок и их связей
- затем модель с описанием тех же колонок и связей
Если же нам нужно изменить таблицу, то
- миграция с изменениями таблицы
- отражение изменений в модели: новые связи или колонки
И это не двойная работа, как может показаться в начале, это именно что работа на 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. Мы же будем постоянно её использовать в наших контроллерах, о которых я расскажу на следующием уроке.
0
👍
👎
❤️
🔥
😮
😢
😀
😡
438
02.06.2022, 10:37:06
18 комментариев
bezumkin.ru
Личный сайт Василия Наумкина
Прямой эфир
Василий Наумкин
01.07.2024, 11:56:41
Да, верно, именно так.
А в контроллере, скорее всего, ловить данные методом post.
Василий Наумкин
26.06.2024, 09:38:15
О, точно, вылезает если не залогинен.
Спасибо, исправил!
Василий Наумкин
09.04.2024, 04:45:01
> Ошибка 500
Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи.
...
russel gal
09.03.2024, 20:17:18
> А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал...
Александр Наумов
27.01.2024, 03:06:18
Василий, спасибо!
Извини, тупанул.
Просто авторизовался в админке. После этого новый пользователь появлялся в базе, но повторно зайти в админку не получалось - появлялись эти ошибки. Но если в файле core/src/Models/User.php удалить только что добавленный конструктор - $user = new User(); - ошибки пропадают, пользователь в базе сохраняется и все работает.
Либо выполнял команды composer db:migrate, composer db:seed - после этого пользователь также появлялся в базе и зайти в админку сразу не получается. Опять же удаление добавленного в файле core/src/Models/User.php кода приводит к тому, что ошибки пропадают и все работает потом.