Eloquent: введение

Введение

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

Прежде чем начать настройте ваше соединение с БД в файле config/database.php. Инструкция по настройке БД.

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

Давайте создадим Eloquent-модель. Модели обычно лежат в корне папки app, но вы вольны располагать их в любой папке, которая участвует в автозагрузке классов. Модель должна расширять класс Illuminate\Database\Eloquent\Model.

Самый простой способ создать модель - воспользоваться артизан-командой make:model:

php artisan make:model User

Если вы хотите вместе с моделью создать миграцию для соответственного изменения БД, добавьте опцию --migration или -m:

php artisan make:model User --migration

php artisan make:model User -m

Принятые соглашения именования

Для удобства разработки в Laravel существуют определённые правила названий таблиц и столбцов, принятые по умолчанию.

Возьмем, например, модель Flight, которая оперирует данными в таблице flights:

<?php 

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    //
}

Название таблицы

По умолчанию считается, что название таблицы представляет собой множественное число от названия модели, записанное в "snake case", т.е. неведущие большие буквы снабжаются префиксным символом подчеркивания и все слово переводится в прописную (нижнюю) раскладку. Например: SomeModel -> some_models.

Чтобы изменить это, задайте явно название таблицы в свойстве table:

<?php 

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
	/**
	 * The table associated with the model.
	 *
	 * @var string
	 */
	protected $table = 'my_flights';
}

Primary Keys

По умолчанию primary key называется id. Вы можете изменить имя в свойстве $primaryKey.

Кроме того, предволагается, что primary key является автоинкрементным полем, поэтому значение в нем фреймворк автоматически приводит к целому числу (int). Если вы хотите использовать строковые ключи, поставьте в вашей модели свойство $incrementing в false/

Timestamps

По умолчанию Eloquent ожидает, что в таблице будут столбцы created_at и updated_at, куда записывается записывается дата (timestamp) создания и обновления соответственно. Если вам не нужна эта фича и в таблице нет этих столбцов, то установите свойство $timestamps в false:

<?php 

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
	/**
	 * Indicates if the model should be timestamped.
	 *
	 * @var bool
	 */
	public $timestamps = false;
}

Изменить формат даты в этих столбцах вы можете при помощи свойства $dateFormat. Оно определяет, в каком формате дата будет хранится в БД и выглядеть в массиве или json данных, полученных из БД.

<?php 

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
	/**
	 * The storage format of the model's date columns.
	 *
	 * @var string
	 */
	protected $dateFormat = 'U';
}

Соединение с базой данных

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

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Flight extends Model
{
    /**
     * The connection name for the model.
     *
     * @var string
     */
    protected $connection = 'connection-name';
}

Получение нескольких экземпляров моделей

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

<?php 

namespace App\Http\Controllers;

use App\Flight;
use App\Http\Controllers\Controller;

class FlightController extends Controller
{
	/**
	 * Show a list of all available flights.
	 *
	 * @return Response
	 */
	public function index()
	{
		$flights = Flight::all();

		return view('flight.index', ['flights' => $flights]);
	}
}

Получение значений столбцов

Чтобы получить значение столбца, обратитесь к свойству модели с таким же именем. Например, пройдемся по всем полученным строкам таблицы и выведем содержимое столбца name:

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

Корректировка запроса

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

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

Примечание: Хорошо изучите конструктор запросов, так как все его методы доступны в моделях.

Коллекции

Методы all и get, возвращающие набор данных - возвращают объект Illuminate\Database\Eloquent\Collection. Этот класс предоставляет множество полезных методов для работы с результатами запросов. Также вы можете работать с коллекцией как с массивом, перебирая её в цикле foreach:

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

Chunking Results

Если вам надо обработать результат запроса, который содержит тысячи элементов, и не потратить всю память на сервере, используйте метод chunk. Он получает "кусочек" моделей и обрабатывает их в функции-замыкании:

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

Получение одного экземпляра модели / Аггрегация

Чтобы получить один экземпляр модели из запроса, вспользуйтесь методами find и first. Они возвращают экземпляр модели, а не коллекцию моделей.

// Получить экземпляр модели по ключу
$flight = App\Flight::find(1);

// Получить первый экземпляр модели по запросу
$flight = App\Flight::where('active', 1)->first();

В метод find также можно передавать массив значений ключей. В таком случае метод возвратит коллекцию.

$flights = App\Flight::find([1, 2, 3]);	

Not Found Exceptions

Иногда вам нужно бросить исключение, если экземпляр модели не найден. Это, как правило, нужно в роутах или контроллерах, когда вы получаете критичные для отображения страницы данные. В этом случае вам помогут методы findOrFail и firstOrFail. Они бросают исключение Illuminate\Database\Eloquent\ModelNotFoundException, если результат не получен:

$model = App\Flight::findOrFail(1);

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

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

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

Агрегирующие функции

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

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

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

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

Вставка

Чтобы вставить строку в таблицу, создайте модель, установите нужные аттрибуты и вызовите метод save:

<?php 

namespace App\Http\Controllers;

use App\Flight;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class FlightController extends Controller
{
	/**
	 * Create a new flight instance.
	 *
	 * @param  Request  $request
	 * @return Response
	 */
	public function store(Request $request)
	{
		// Валидируем запрос...

		$flight = new Flight;

		$flight->name = $request->name;

		$flight->save();
	}
}

В этом примере мы взяли значение name из аттрибутов HTTP-запроса и присвоили его аттрибуту name модели. Когда вызывается метод save, содержимое модели добавляется в соответствующую таблицу базы данных. Помимо значений модели, автоматически заполняются столбцы created_at и updated_at, если их поддержка не запрещена.

Обновление

Если модель существует в базе данных (есть строка с тем же primary key, что и в модели) метод save обновляет данные в БД, записывая их туда из модели. Таким образом, чтобы одновить модель, вы должны получить её из БД, установить необходимые аттрибуты и сохранить. Столбец updated_at обновится автоматически, если поддержка не запрещена в модели.

$flight = App\Flight::find(1);

$flight->name = 'New Flight Name';

$flight->save();

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

App\Flight::where('active', 1)
		  ->where('destination', 'San Diego')
		  ->update(['delayed' => 1]);

Метод update принимает на вход массив вида "название столбца" => "данные для записи".

Массовое присваивание атрибутов

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

Чем плохо массовое присваивание и почему от него нужно защищаться ? Дело в том, что при POST HTTP-запросе злоумышленник может прислать не только значения из формы, но и добавить несколько аттрибутов от себя. Например, is_admin, или другое поле, которое пользователям нельзя изменять напрямую. И если вы передаете данные запроса в метод create сразу, без фильтрации (в случае больших форм это может быть удобно), то без защиты от массового присваивания вы получаете потенциальную дыру в безопасности.

Вы можете оставить запрет массового присваивания для модели и разрешить его только для выбранных свойств. Для этого перечислите список этих свойств в свойстве модели $fillable:

<?php 

namespace App;

use Illuminate\Database\Eloquent\Model;

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

Здесь в методах массового присвоения мы разрешили использовать только свойство name.

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

Но вы можете поступить и по-другому. Вы можете разрешить массовое присваивание для всех свойств модели, запретив его только для избранных свойств. Для этого перечислите их в свойстве модели $guarded:

<?php 

namespace App;

use Illuminate\Database\Eloquent\Model;

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

Здесь в методах массового присвоения можно использовать любые свойства, кроме price и delayed.

Естественно, вы можете использовать или $fillable или $guarded - но не оба сразу.

Другие способы создания модели

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

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

// Retrieve the flight by the attributes, or create it if it doesn't exist...
$flight = App\Flight::firstOrCreate(['name' => 'Flight 10']);

// Retrieve the flight by the attributes, or instantiate a new instance...
$flight = App\Flight::firstOrNew(['name' => 'Flight 10']);

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

Для того, чтобы удалить модель, вызовите метод delete():

$flight = App\Flight::find(1);

$flight->delete();

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

В примере выше мы сначала получили модель, а затем удалили её. Но если вам известен primary key модели, необязательно её загружать из БД, можно удалить сразу при помощи метода destroy:

App\Flight::destroy(1);

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

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

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

Вы также можете удалить все модели, попадающие под определённые условия:

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

Псевдоудаление

Модели Eloquent имеют опцию псевдоудаления - при таком удалении данные остаются в базе данных, но не участвуют в дальнейших выборках. В поле deleted_at при этом записывается текущее время удаления. Для включения псевдоудалений добавьте в модель трейт Illuminate\Database\Eloquent\SoftDeletes:

<?php 

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Flight extends Model
{
	use SoftDeletes;

	/**
	 * The attributes that should be mutated to dates.
	 *
	 * @var array
	 */
	protected $dates = ['deleted_at'];
}

И, конечно, в таблице модели должен существовать столбец deleted_at. Добавить его в миграции можно так:

Schema::table('flights', function ($table) {
	$table->softDeletes();
});

Метод trashed возвращает true, если deleted_at у модели не равняется null, т.е. модель подвергалась псевдоудалению:

if ($flight->trashed()) {
	//
}

Работа с псевдоудалёнными моделями

Получение псевдоудалённых моделей

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

$flights = App\Flight::withTrashed()
				->where('account_id', 1)
				->get();

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

$flight->history()->withTrashed()->get();

Получение только псевдоудалённых моделей

Метод onlyTrashed вернет только псевдоудалённые модели:

$flights = App\Flight::onlyTrashed()
				->where('airline_id', 1)
				->get();

Восстановление псевдоудалённых моделей

Иногда вам нужно восстановить псевдоудалённую модель. Для этого используйте метод restore:

$flight->restore();

Его можно использовать на выборке нескольких моделей:

App\Flight::withTrashed()
		->where('airline_id', 1)
		->restore();

Также можно восстановить все псевдоудалённые модели в отношениях:

$flight->history()->restore();

Окончательное удаление

Иногда вам нужно окончательно удалить псевдоудалённую модель. Метод forceDelete физически удаляет запись в базе данных:

// удаление псевдоудалённого экземпляра
$flight->forceDelete();

// удаление всех псевдоудалённых моделей в отношении
$flight->history()->forceDelete();

Условия запросов (Query Scopes)

Глобальные условия

Глобальные условия запросов (global scopes) добавляют во все запросы для данной модели определённые условия. Например, механизм псевдоудаления моделей в Laravel построен на механизме глобальных условий - в можели с функцией псевдоудаления ставится условие отфильтровывать все псевдоудалённые значения.

Написание глобальных условий

Определите класс, имплементирующий интерфейс Illuminate\Database\Eloquent\Scope. Этот интерфейс требует, чтобы в классе был всего один метод - apply, в котором вы можете написать нужную фильтрацию при помощи методов построителя запросов Laravel:

<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class AgeScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        return $builder->where('age', '>', 200);
    }
}

В структуре фреймворка нет какого-то специального места для классов глобальных условий, держите их там, где вам удобно.

Применение глобальных условий

To assign a global scope to a model, you should override a given model's boot method and use the addGlobalScope method:

Для того, чтобы запросы к модели начали фильтроваться при помощи глобальных запросов, переопределите в модели метод boot и используйте в нем метод addGlobalScope:

<?php

namespace App;

use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(new AgeScope);
    }
}

Теперь при запросе User::all() будет выполняться вот такой sql-запрос:

select * from `users` where `age` > 200

Анонимные глобальные условия

Если вы не хотите создавать класс для применения глобальных условий в одной-единственной модели, вы можете определить глобальное условие прямо внутри метода addGlobalScope:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class User extends Model
{
    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('age', function(Builder $builder) {
            $builder->where('age', '>', 200);
        });
    }
}

The first argument of the addGlobalScope() serves as an identifier to remove the scope:

Первый аргумент в данном случае будет идентификатором этого глобального условия. Идентификатор нужен для того, чтобы быть способным отменить действие глобального условия:

User::withoutGlobalScope('age')->get();

Нейтрализация глобального условия

Чтобы выполнить запрос без заданного глобального условия, используйте метод withoutGlobalScope:

User::withoutGlobalScope(AgeScope::class)->get();

Можно отменить не только одно, но и несколько, или все глобальные условия - при помощи withoutGlobalScopes:

User::withoutGlobalScopes()->get();

User::withoutGlobalScopes([FirstScope::class, SecondScope::class])->get();

Локальные условия

Локальные условия запросов (local scope, скоупы) позволяют записывать повторяющиеся длинные конструкции where в запросах моделей в виде короткого метода. Они определяются в модели в виде метода, начинающегося на scope.

Например, создадим условие popular() и active():

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

    /**
     * Scope a query to only include active users.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeActive($query)
    {
        return $query->where('active', 1);
    }
}

Использование локальный условий Eloquent

Теперь мы можем использовать эти условия в запросе следующим образом:

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

Динамические условия

Локальные условия могут получать параметры. Для этого укажите их после первого аргумента $query:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Scope a query to only include users of a given type.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function scopeOfType($query, $type)
    {
        return $query->where('type', $type);
    }
}

Использование:

$users = App\User::ofType('admin')->get();

События моделей

Модели Eloquent в процессе работы инициируют некоторые события (envents), что позволяет вам добавить некоторый свой функционал к работе моделей Eloquent. Подписаться на события можно при помощи следующих методов: creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored.

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

Когда новая модель сохраняется в базу данных в первый раз, запускаются события creating и created. Если модель уже есть в базе данных и сохраняется в ней при помощи метода save, запускаются события updating и updated. И в обоих случаях запускаются события saving и saved.

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

Давайте попробуем внедриться в процесс создания модели и отменим его, если данные модели не пройдут внутреннюю валидацию (эту валидацию мы будем проводить с методе isValid модели). Для этого определим слушателя события creating в сервис-провайдере. Если слушатель возвращает false, то операция save() или update() будет отменена.

<?php 

namespace App\Providers;

use App\User;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
	public function boot()
	{
		User::creating(function ($user) {
			if ( ! $user->isValid()) {
				return false;
			}
		});
	}

	/**
	 * Register the service provider.
	 *
	 * @return void
	 */
	public function register()
	{
		//
	}
}