Настройка Eloquent ORM для Laravel веб-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Настройка Eloquent ORM для Laravel веб-приложения
Средняя
~1 рабочий день
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Настройка Eloquent ORM для Laravel веб-приложения

Eloquent — активная запись поверх PDO с поддержкой PostgreSQL, MySQL, SQLite и SQL Server. В Laravel 10/11 он уже встроен, но правильная конфигурация — не только .env. Рассматриваем весь путь: от конфига подключения до advanced-техник.

Конфигурация подключения

config/database.php — изменяем только критичные параметры, остальное из .env:

'pgsql' => [
    'driver'   => 'pgsql',
    'url'      => env('DATABASE_URL'),
    'host'     => env('DB_HOST', '127.0.0.1'),
    'port'     => env('DB_PORT', '5432'),
    'database' => env('DB_DATABASE', 'forge'),
    'username' => env('DB_USERNAME', 'forge'),
    'password' => env('DB_PASSWORD', ''),
    'charset'  => 'utf8',
    'prefix'   => '',
    'search_path' => 'public',
    'sslmode'  => env('DB_SSLMODE', 'prefer'),
    'options'  => [
        PDO::ATTR_PERSISTENT => env('DB_PERSISTENT', false),
        PDO::ATTR_TIMEOUT    => 10,
    ],
],

Для production за reverse proxy включайте ATTR_PERSISTENT = false — persistent connections конфликтуют с pgBouncer в transaction mode.

Модель

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder;

class Product extends Model
{
    use SoftDeletes;

    protected $table = 'products';

    protected $fillable = [
        'title', 'slug', 'price', 'status', 'category_id',
    ];

    protected $casts = [
        'price'      => 'decimal:2',
        'meta'       => 'array',          // JSON колонка
        'published_at' => 'datetime',
        'is_featured' => 'boolean',
    ];

    // Scope для фильтрации
    public function scopePublished(Builder $query): Builder
    {
        return $query->where('status', 'published');
    }

    public function scopeInCategory(Builder $query, int $categoryId): Builder
    {
        return $query->where('category_id', $categoryId);
    }

    // Computed attribute (Laravel 9+)
    protected function priceWithTax(): Attribute
    {
        return Attribute::make(
            get: fn () => round($this->price * 1.2, 2),
        );
    }

    // Отношения
    public function category(): \Illuminate\Database\Eloquent\Relations\BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
    {
        return $this->belongsToMany(Tag::class, 'product_tags')
                    ->withTimestamps();
    }

    public function images(): \Illuminate\Database\Eloquent\Relations\HasMany
    {
        return $this->hasMany(ProductImage::class)->orderBy('sort_order');
    }
}

Миграции

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
            $table->string('title', 500);
            $table->string('slug', 520)->unique();
            $table->foreignId('category_id')->constrained()->restrictOnDelete();
            $table->decimal('price', 12, 2);
            $table->string('status', 20)->default('draft');
            $table->jsonb('meta')->nullable();
            $table->boolean('is_featured')->default(false);
            $table->timestamp('published_at')->nullable();
            $table->timestamps();
            $table->softDeletes();

            $table->index(['status', 'created_at']);
            $table->index(['category_id', 'status']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('products');
    }
};

foreignId('category_id')->constrained() — создаёт FK с именем products_category_id_foreign, ссылающимся на categories.id. restrictOnDelete() предпочтительнее cascadeOnDelete() для продуктового каталога: не дадим случайно удалить категорию с товарами.

Жадная загрузка и устранение N+1

Для endpoint каталога с пагинацией:

$products = Product::query()
    ->published()
    ->with(['category', 'tags', 'images' => fn ($q) => $q->limit(1)])
    ->whereHas('category', fn ($q) => $q->where('is_active', true))
    ->orderBy('created_at', 'desc')
    ->paginate(24);

with() выполнит три дополнительных запроса вместо N+1. Ограничение limit(1) на изображения — частый трюк для preview.

Для глобального предотвращения N+1 в тестовой среде:

// AppServiceProvider::boot()
if (app()->isLocal()) {
    Model::preventLazyLoading();
}

Это выбросит LazyLoadingViolationException при первой же ленивой загрузке и заставит исправить запрос до production.

Транзакции

use Illuminate\Support\Facades\DB;

DB::transaction(function () use ($orderId, $items) {
    $order = Order::findOrFail($orderId);
    $order->update(['status' => 'processing']);

    foreach ($items as $item) {
        Product::where('id', $item['product_id'])
            ->decrement('stock', $item['quantity']);
    }

    $order->payments()->create([
        'amount'  => $order->total,
        'gateway' => 'stripe',
    ]);
});

При исключении — автоматический rollback. Третий аргумент DB::transaction($fn, 3) — количество повторных попыток при deadlock.

Repository pattern (опционально)

Eloquent — active record, и прямое использование в контроллерах — норма для небольших проектов. Для сложной бизнес-логики разделяем:

<?php

namespace App\Repositories;

use App\Models\Product;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;

class ProductRepository
{
    public function getPublishedPaginated(
        int $categoryId,
        int $perPage = 24,
    ): LengthAwarePaginator {
        return Product::query()
            ->published()
            ->inCategory($categoryId)
            ->with(['category', 'tags'])
            ->orderByDesc('created_at')
            ->paginate($perPage);
    }
}

Сроки

Настройка Eloquent для нового Laravel-проекта: 1 день (модели, миграции, seeders, базовые тесты). Аудит и рефакторинг существующего проекта с N+1 проблемами, неоптимальными индексами и отсутствующими FK-ограничениями: 1–2 дня.