Строитель (Builder) паттерн

Это один из самых недооценённых, но мощных паттернов, особенно когда нужно конструировать сложные объекты пошагово. Сейчас всё разложу по полочкам, примеры — на Laravel + PHP 8.3.

Суть

Builder позволяет создавать сложные объекты поэтапно, а не через перегруженный конструктор или кучу set-методов. Он изолирует процесс построения объекта от его конечной репрезентации.


🔧 Когда применять

  • Объект состоит из множества параметров, не все из которых обязательны;

  • Нужно разные представления одного и того же объекта;

  • Нужно собирать объект поэтапно — особенно если сборка зависит от условий;

  • Нужен чистый код, без "спагетти" из ifnewset*() и т.п.


📦 Laravel-пример: Генератор отчёта

Ты делаешь модуль для генерации отчёта (PDF, HTML, JSON), и в зависимости от параметров ты:

  • подгружаешь данные;

  • добавляешь фильтры;

  • применяешь форматирование;

  • указываешь вывод.

 

Класс отчёта

class Report
{
    public string $title;
    public array $filters = [];
    public array $data = [];
    public string $format;

    public function output(): string
    {
        // простая симуляция вывода
        return "{$this->format} Report: {$this->title}";
    }
}

 

Интерфейс строителя

interface ReportBuilderInterface
{
    public function setTitle(string $title): static;
    public function setFilters(array $filters): static;
    public function setData(array $data): static;
    public function setFormat(string $format): static;
    public function getReport(): Report;
}

 

Конкретный строитель

class ReportBuilder implements ReportBuilderInterface
{
    protected Report $report;

    public function __construct()
    {
        $this->report = new Report();
    }

    public function setTitle(string $title): static
    {
        $this->report->title = $title;
        return $this;
    }

    public function setFilters(array $filters): static
    {
        $this->report->filters = $filters;
        return $this;
    }

    public function setData(array $data): static
    {
        $this->report->data = $data;
        return $this;
    }

    public function setFormat(string $format): static
    {
        $this->report->format = strtoupper($format);
        return $this;
    }

    public function getReport(): Report
    {
        return $this->report;
    }
}

 

Использование строителя

$builder = new ReportBuilder();

$report = $builder
    ->setTitle('Monthly Sales')
    ->setFilters(['region' => 'EU'])
    ->setData([/* данные из БД */])
    ->setFormat('pdf')
    ->getReport();

echo $report->output(); // PDF Report: Monthly Sales

 

С добавлением "директора" (по желанию)

Иногда добавляют Director — сущность, которая управляет сценарием сборки:

class ReportDirector
{
    public function buildSalesReport(ReportBuilderInterface $builder): Report
    {
        return $builder
            ->setTitle('Sales Report')
            ->setFilters(['year' => date('Y')])
            ->setData([/* ... */])
            ->setFormat('html')
            ->getReport();
    }
}

 

Laravel-стиль и практика

  • Очень удобно использовать в формировании email-уведомленийэкспорта CSV/XLSконфигурации API-запросовсоздания сложных DTO.

  • Можно внедрять Builder через контейнер Laravel, если он зависит от других сервисов.

  • Совместим с Fluent-интерфейсом ($builder->setX()->setY()) — удобно и читаемо.

 

Отличие от фабрики

Фабрика Строитель
Создает объект за один шаг Создает объект пошагово
Фокус на типе Фокус на процессе
Одна функция make() Много шагов setX()setY()
Пример: PaymentGatewayFactory Пример: ReportBuilder

 

Когда НЕ стоит использовать

❌ Когда объект простой (new User() и достаточно).
❌ Когда количество шагов фиксировано и без условий.

Опубликовано 11.06.2025
ООП Паттерны проектирования

Абстрактная фабрика (Abstract Factory) паттерн

📌 Суть

Абстрактная фабрика — это паттерн проектирования, который предоставляет интерфейс для создания семейства взаимосвязанных объектов без указания их конкретных классов.

Если обычная фабрика создает один объект — то абстрактная фабрика создает набор объектов, которые логически связаны и могут взаимодействовать друг с другом.

 

📦 Когда нужен этот паттерн?

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

 

🎯 Пример из жизни (Laravel Style)

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

Каждый канал может состоять из нескольких компонентов:

  • Генератор шаблона

  • Отправщик

  • Логгер

И ты хочешь создать всё это в рамках одного семейства объектов.

 

Интерфейсы

interface NotificationSenderInterface {
    public function send(string $to, string $message): bool;
}

interface TemplateRendererInterface {
    public function render(string $template, array $data): string;
}

 

Реализации (Email)

class EmailSender implements NotificationSenderInterface {
    public function send(string $to, string $message): bool {
        // логика отправки email
        return true;
    }
}

class EmailTemplateRenderer implements TemplateRendererInterface {
    public function render(string $template, array $data): string {
        // подстановка данных в шаблон
        return "Email: " . strtr($template, $data);
    }
}

 

Реализации (SMS)

class SmsSender implements NotificationSenderInterface {
    public function send(string $to, string $message): bool {
        // логика отправки sms
        return true;
    }
}

class SmsTemplateRenderer implements TemplateRendererInterface {
    public function render(string $template, array $data): string {
        return "SMS: " . strtr($template, $data);
    }
}

 

Абстрактная фабрика

interface NotificationFactoryInterface {
    public function makeSender(): NotificationSenderInterface;
    public function makeRenderer(): TemplateRendererInterface;
}

 

Конкретные фабрики

class EmailNotificationFactory implements NotificationFactoryInterface {
    public function makeSender(): NotificationSenderInterface {
        return new EmailSender();
    }

    public function makeRenderer(): TemplateRendererInterface {
        return new EmailTemplateRenderer();
    }
}

class SmsNotificationFactory implements NotificationFactoryInterface {
    public function makeSender(): NotificationSenderInterface {
        return new SmsSender();
    }

    public function makeRenderer(): TemplateRendererInterface {
        return new SmsTemplateRenderer();
    }
}

 

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

function notify(NotificationFactoryInterface $factory, string $to, string $template, array $data)
{
    $sender = $factory->makeSender();
    $renderer = $factory->makeRenderer();

    $message = $renderer->render($template, $data);
    $sender->send($to, $message);
}

// Используем Email
$factory = new EmailNotificationFactory();
notify($factory, 'user@example.com', 'Привет, {{name}}', ['{{name}}' => 'Андрей']);

// Используем SMS
$factory = new SmsNotificationFactory();
notify($factory, '+71234567890', 'Привет, {{name}}', ['{{name}}' => 'Анна']);

 

Laravel-контекст

В Laravel ты можешь:

  • Создавать NotificationFactory как сервис, регистрировать в ServiceProvider.

  • Инъецировать нужную фабрику в зависимости от контекста (например, в зависимости от роли пользователя или источника запроса).

  • Применять Service Container, чтобы всё было прозрачно и гибко.

 

app()->bind(NotificationFactoryInterface::class, function () {
    return match (config('notification.driver')) {
        'email' => new EmailNotificationFactory(),
        'sms' => new SmsNotificationFactory(),
        default => throw new RuntimeException('Invalid notification driver'),
    };
});

 

Отличия от обычной фабрики

Критерий Обычная фабрика Абстрактная фабрика
Создаёт Один объект Набор логически связанных объектов
Интерфейс Один метод make() Несколько методов makeSender()makeRenderer() и т.д.
Когда использовать Простая логика создания Семейства компонентов, которые работают вместе
Пример в Laravel Model::factory() Каналы уведомлений, драйверы API, UI-компоненты
Опубликовано 11.06.2025
ООП Паттерны проектирования

Фабрика (Fabric) паттерн

Паттерн «Фабрика» (Factory)

📌 Что это такое?

Factory — это порождающий паттерн проектирования, цель которого — инкапсулировать логику создания объектов. То есть, вместо того чтобы напрямую писать new Class(), мы обращаемся к фабричному методу, который решает, какой объект и как создать.

Это особенно полезно, если:

  • ты не хочешь, чтобы код знал, какой именно класс создаётся;

  • тебе нужно создавать разные реализации одного интерфейса;

  • в процессе создания объекта нужна сложная логика.

 

Пример из реальной жизни

Допустим, ты делаешь сервис оплаты. У тебя есть несколько платёжных провайдеров: StripePayPalTinkoff.

 

Ты можешь создать интерфейс PaymentGatewayInterface и конкретные реализации:

interface PaymentGatewayInterface {
    public function pay(float $amount): bool;
}

class StripeGateway implements PaymentGatewayInterface {
    public function pay(float $amount): bool {
        // логика Stripe
        return true;
    }
}

class PayPalGateway implements PaymentGatewayInterface {
    public function pay(float $amount): bool {
        // логика PayPal
        return true;
    }
}

 

Пример: Реализация Factory в Laravel-стиле:

class PaymentGatewayFactory
{
    public static function make(string $driver): PaymentGatewayInterface
    {
        return match ($driver) {
            'stripe' => new StripeGateway(),
            'paypal' => new PayPalGateway(),
            default => throw new InvalidArgumentException("Unsupported gateway: $driver"),
        };
    }
}

 

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

$gateway = PaymentGatewayFactory::make('stripe');
$gateway->pay(1000);

 

Laravel-специфика

В Laravel ты часто можешь обернуть фабрику в сервис-провайдер, зарегистрировать её в контейнере, или использовать через Service Container:

app()->bind(PaymentGatewayInterface::class, function () {
    $driver = config('payment.driver'); // допустим, 'stripe'
    return PaymentGatewayFactory::make($driver);
});

// И потом в контроллере:

public function pay(PaymentGatewayInterface $gateway)
{
    $gateway->pay(1000);
}

 

Применение в Laravel:

  • Model Factories — factory(User::class)->create() — это тоже паттерн Фабрика!

  • Сервисные обёртки — создание сервисов в зависимости от типа пользователя, роли, страны и т.д.

  • Queue Job DispatchersNotification ChannelsLog Handlers — все они могут использовать фабричный подход.

 

Когда использовать

✅ Когда создание объекта зависит от условий (тип, конфигурация, пользовательская роль).
✅ Когда нужно разделить ответственность между логикой приложения и созданием объектов.
❌ Не использовать, если объект тривиален (new User() без условий и зависимостей).

Опубликовано 11.06.2025
ООП Паттерны проектирования

Паттерны проектирования - введение

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

В PHP (как и в других ООП-языках) паттерны делятся на три основные категории:

  • Порождающие (Creational) — Отвечают за удобное и безопасное создание новых объектов или даже целых семейств объектов.
  • Структурные (Structural) — Отвечают за построение удобных в поддержке иерархий классов.
  • Поведенческие (Behavioral) — Решают задачи эффективного и безопасного взаимодействия между объектами программы.

Паттерны проектирования помогают:

  • Избегать дублирования кода
  • Делать систему гибче
  • Упрощать поддержку

 

Порождающие:

  • Фабричный метод (Factory Method)
  • Абстрактная фабрика (Abstract Factory)
  • Строитель (Builder)
  • Прототип (Prototype)
  • Одиночка (Singleton)

 

 

Структурные:

  • Адаптер(Adapter)
  • Мост(Bridge)
  • Компоновщик(Composite)
  • Декоратор(Decorator)
  • Фасад(Facade)
  • Легковес(Flyweight)
  • Заместитель(Proxy)

 

Поведенческие:

  • Цепочка обязанностей(Chain of Responsibility)
  • Команда(Command)
  • Итератор(Iterator)
  • Посредник(Mediator)
  • Снимок(Memento)
  • Наблюдатель(Observer)
  • Состояние(State)
  • Стратегия(Strategy)
  • Шаблонный метод(Template Method)
  • Посетитель(Visitor)

Я буду описывать каждый в следующих статьях

Опубликовано 11.06.2025
ООП Паттерны проектирования

Сортировка таблицы - простой вариант Part 1

У меня была таблица

<table class="table table-hover">
    <thead>
    <tr class="fw-bold fs-6 text-gray-800">
        <th>ID</th>
        <th>Задача</th>
        <th>Ответственный</th>
        <th>Место</th>
        <th>Статус</th>

    </tr>
    </thead>
    @forelse($tasks as $t)
        <tr>
            <td><a href="{{ route('tasks.edit', $t->id) }}">{{ $t->id }}</a></td>
            <td>{{ $t->task }}</td>
            <td>{{ $t->employer->last_name }} {{ $t->employer->first_name }}</td>
            <td>{{ $t->organization->name }}/{{ $t->building->name }}/{{ $t->room->name }}</td>
            <td><span class="badge badge-light-dark">{{ $t->status?->name }}</span></td>
        </tr>
    @empty
        <tr>
            <td><h3>Список пуст</h3></td>
        </tr>
    @endforelse
</table>

По сути - обычная таблица, но для нее нужна сортировка и фильтры, я занялся сортировкой.

И выбрал простой вариант, есть и посложнее.

Для этого устанавливаем пакет:

composer require kyslik/column-sortable

 

В таблице вносим изменение в thead, сразу скажу что storm так и не определил @sortablelink()

@sortablelink('id', 'ID')@sortablelink('task', 'Задача')@sortablelink('employer.last_name', 'Ответственный')Место@sortablelink('status.name', 'Статус')

 

В модельке

class Task extends Model
{
    use Sortable;
    
    public $sortable = [
        'id',
        'task'
    ];
    
    // Добавляем сортируемые отношения (думаю, логика понятна почему и как)
    public $sortableAs = [
        'employer_last_name' => 'employer.last_name',
        'status_name' => 'status.name',
    ];
    
    // Обязательно нужны отношения
    public function employer()
    {
        return $this->belongsTo(Employer::class);
    }
    
    public function status()
    {
        return $this->belongsTo(Status::class);
    }
}

 

И остается только добавить изменение в контроллер 

public function list(): View
    {
        return view(
            'dashboard.admin.' . $this->localName . '.list',
            [
                'title'          => 'Список ' . $this->listName,
                $this->localName => $this->model->with(['employer', 'status','organization','room','building'])->sortable()->paginate(15)
            ]
        );
    }

Ключевое здесь это - ->sortable(), загрузка отношений, можно через load и ->orderBy() должна быть до -> sortable(), иначе гг

На этом всё

Опубликовано 05.05.2025
Laravel Оптимизация Из реального мира

Кеширование|Логирование|Ретраи на примере SMS

🔹 1. Кеширование

Зачем кешировать отправку SMS?

  1. Чтобы не дёргать внешний API слишком часто, если запросы повторяются (например, при повторном нажатии "Отправить код").
  2. Чтобы ускорить работу – достать данные из кеша быстрее, чем отправить HTTP-запрос.
  3. Чтобы избежать лишних трат – API-провайдеры SMS часто берут деньги за каждый запрос.

Как кешировать отправку SMS?

Допустим, мы не хотим отправлять одно и то же SMS чаще, чем раз в 5 минут.

✅ Создадим CacheSmsLibrary, которая оборачивает основную библиотеку и кеширует отправленные SMS:

use Illuminate\Support\Facades\Cache;

class CacheSmsLibrary implements SmsTemplatesLibraryInterface
{
    public function __construct(private SmsTemplatesLibraryInterface $library) {}

    public function sendTemplateById(int $id, string $phone, string $message): SmsResponseDTO
    {
        $cacheKey = "sms_sent_{$phone}_{$id}";
        if (Cache::has($cacheKey)) {
            return new SmsResponseDTO(success: false, message: "SMS недавно отправлено, подождите.", statusCode: 429);
        } // Отправляем SMS через основную библиотеку
        $response = $this->library->sendTemplateById($id, $phone, $message);
        if ($response->success) { // Кешируем на 5 минут
            Cache::put($cacheKey, true, now()->addMinutes(5));
        }
        return $response;
    }
}

📌 Теперь если одно и то же SMS пытаются отправить повторно в течение 5 минут – оно не будет отправлено повторно.

Применение:

app()->bind(SmsTemplatesLibraryInterface::class, function () { return new CacheSmsLibrary(new SmsProviderA()); });

🔹 2. Логирование

Зачем логировать отправку SMS?

  1. Для отладки – видеть, какие сообщения были отправлены.
  2. Для мониторинга – можно анализировать частоту отправок.
  3. Для обработки ошибок – фиксировать неудачные попытки.

Как логировать?

✅ Создадим LoggingSmsLibrary, которая оборачивает основную библиотеку и логирует отправку:

use Illuminate\Support\Facades\Log;

class LoggingSmsLibrary implements SmsTemplatesLibraryInterface
{
    public function __construct(private SmsTemplatesLibraryInterface $library) {}

    public function sendTemplateById(int $id, string $phone, string $message): SmsResponseDTO
    {
        Log::info("Отправка SMS [$id] на номер: $phone, текст: $message");
        $response = $this->library->sendTemplateById($id, $phone, $message);
        if ($response->success) {
            Log::info("SMS отправлено успешно: {$response->message}");
        } else {
            Log::error("Ошибка отправки SMS: {$response->message}");
        }
        return $response;
    }
}

Теперь все отправки SMS логируются, а если что-то пошло не так – мы это увидим в storage/logs/laravel.log.

Применение:

app()->bind(SmsTemplatesLibraryInterface::class, function () { return new LoggingSmsLibrary(new SmsProviderA()); });

👉 Теперь все вызовы SMS отправки будут логироваться.


🔹 3. Ретраи (повторная отправка при неудаче)

Зачем делать ретраи?

  1. Внешний API может временно не работать (например, проблемы с сервером провайдера).
  2. Интернет-соединение может прерываться – если SMS API недоступен, стоит попробовать ещё раз.
  3. Некоторые ошибки можно обработать повторной попыткой (например, если сервер ответил 500 – можно повторить).

Как делать ретраи?

✅ Создадим RetrySmsLibrary, которая повторяет отправку SMS до 3 раз, если произошла ошибка:

use Exception;

class RetrySmsLibrary implements SmsTemplatesLibraryInterface
{
    public function __construct(private SmsTemplatesLibraryInterface $library) {}

    public function sendTemplateById(int $id, string $phone, string $message): SmsResponseDTO
    {
        $attempts = 3;
        $delay    = 2; // секунды между попытками
        for ($i = 0; $i < $attempts; $i++) {
            try {
                $response = $this->library->sendTemplateById($id, $phone, $message);
                if ($response->success) {
                    return $response;
                }
                Log::warning("Ошибка отправки SMS, попытка $i: {$response->message}");
            } catch (Exception $e) {
                Log::error("Исключение при отправке SMS, попытка $i: " . $e->getMessage());
            } // Ждём перед повторной попыткой
            sleep($delay);
        }
        return new SmsResponseDTO(success   : false,
                                  message   : "Не удалось отправить SMS после $attempts попыток",
                                  statusCode: 500
        );
    }
}

📌 Теперь если первый запрос не удался, библиотека попробует ещё 2 раза с задержкой 2 секунды.

Применение:

app()->bind(SmsTemplatesLibraryInterface::class, function () { return new RetrySmsLibrary(new SmsProviderA()); });

💡 Итог: Комбинируем всё вместе!

Теперь можно объединить все улучшения в цепочку классов:

app()->bind(SmsTemplatesLibraryInterface::class, function () { return new CacheSmsLibrary( new LoggingSmsLibrary( new RetrySmsLibrary( new SmsProviderA() ) ) ); });

📌 Теперь у нас:
✅ Кеширование (не отправляем SMS повторно в течение 5 минут).
✅ Логирование (все попытки и ошибки фиксируются).
✅ Ретраи (если SMS не отправилось, пробуем ещё 2 раза).

Вся магия происходит без изменений в коде контроллера и сервиса! 🚀


🎯 Вывод

  1. Кеширование – помогает избежать повторных запросов, уменьшает нагрузку на API.
  2. Логирование – фиксирует отправки и ошибки, помогает при отладке.
  3. Ретраи – позволяют повторять запрос при временных ошибках.

Когда стоит использовать эти техники?

Метод Когда использовать?
Кеширование Если есть вероятность повторных SMS за короткий период (например, OTP-коды)
Логирование Всегда – полезно для мониторинга и отладки
Ретраи Если API SMS-провайдера не всегда стабильно работает

🔥 Лучший вариант – использовать все 3 техники, обернув основную библиотеку в цепочку классов! 🚀

Опубликовано 21.02.2025
ООП Laravel Оптимизация

SQL - Tips n Tricks

Поиск дубля

SELECT column_name, COUNT(*)
FROM table_name
GROUP BY column_name
HAVING COUNT(*) \> 1;

Несколько дублей

SELECT column1, column2, COUNT(*)
FROM table_name
GROUP BY column1, column2
HAVING COUNT(*) > 1;

Если же вам нужно найти записи, где любое из значений в наборе колонок дублируется, вы можете использовать более сложный запрос с подзапросами или объединением (UNION) нескольких запросов GROUP BY, каждый из которых будет проверять дубликаты в отдельной колонке. Вот пример такого запроса:

SELECT column1 FROM table_name
GROUP BY column1
HAVING COUNT(column1) > 1

UNION

SELECT column2 FROM table_name
GROUP BY column2
HAVING COUNT(column2) > 1;

Имба запрос если нужно группировать и выводить данные из одной и той же колонки (не по которой группировали кон)

SELECT 
    batch_id,
    GROUP_CONCAT(
        CONCAT(
            CASE
                WHEN job_name = 'App\\Jobs\\GenerateAndSignXmlJob' THEN 'БКИ'
                WHEN job_name = 'App\\Jobs\\ClientCheckJob' THEN 'Инфосфера'
                WHEN job_name = 'App\\Jobs\\ReceivingKBKIJob' THEN 'КБКИ'
                WHEN job_name = 'App\\Jobs\\MakeDecisionJob' THEN 'СПР'

                ELSE job_name
            END,
            ' (', result, ')'
        ) SEPARATOR ', '
    ) AS job_names_with_status,
    created_at
FROM 
    job_results
WHERE 
    created_at > '2024-10-31 00:00:00'
GROUP BY 
    batch_id;

Таблица выглядит так

 

После выполнения запроса

Грубо говоря, мы сокращаем 4 записи до одной с понятным выводом о применении джобы

Опубликовано 23.01.2025
Оптимизация

Exceptions в Laravel

В Laravel создание собственного класса исключения (Exception) позволяет вам обрабатывать ошибки в приложении более гибко и организованно. Вот шаги, чтобы создать и использовать собственный класс исключения:

Шаг 1: Создание класса исключения

Создайте новый файл для вашего класса исключения в каталоге app/Exceptions. Например, создадим исключение CustomException.php:

<?php

namespace App\Exceptions;

use Exception;

class CustomException extends Exception
{
    // Можно добавить дополнительные свойства или методы
    public function report()
    {
        // Логика для регистрации исключения
        \Log::debug($this->getMessage());
    }

    public function render($request)
    {
        // Логика для ответа клиенту
        return response()->json([
            'error' => 'Custom error occurred.',
            'message' => $this->getMessage()
        ], 400);
    }
}

Шаг 2: Использование исключения в приложении

Теперь вы можете выбрасывать это исключение в любом месте вашего приложения, используя ключевое слово throw:

public function someMethod()
{
    if ($someCondition) {
        throw new \App\Exceptions\CustomException('Something went wrong!');
    }

    // Остальная логика метода
}

Шаг 3: Обработка исключения

Обработка исключений в Laravel обычно выполняется в классе Handler, который находится в app/Exceptions/Handler.php. Вы можете добавить логику обработки вашего исключения в метод render или report этого класса:

public function render($request, Exception $exception)
{
    if ($exception instanceof \App\Exceptions\CustomException) {
        return $exception->render($request);
    }

    return parent::render($request, $exception);
}

Шаг 4: Тестирование исключения

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

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

 

PS

Спустя время использование exceptions переросло в нечто более удобное

Структура

 

Пример FileServiceBaseException

namespace App\Exceptions\FileService;

use App\Exceptions\BaseCustomException;
use Throwable;

class FileServiceBaseException extends BaseCustomException
{
    public function __construct(string $message = 'Critical error', int $code = 500, ?Throwable $previous = null, string $clientMessage = 'Ошибка')
    {
        parent::__construct($message, $code, $previous, $clientMessage);
    }
}

 

И сам BaseCustomException

namespace App\Exceptions;

use App\DTO\BoolDTO;
use App\DTO\DataTransferObject;
use App\DTO\Response;
use App\Helpers\GlobalHelper;
use Throwable;

class BaseCustomException extends \Exception
{
    private string $clientMessage;

    public function __construct(string $message = 'Critical error', int $code = 500, ?Throwable $previous = null, string $clientMessage = 'Ошибка')
    {
        parent::__construct($message, $code, $previous);
        $this->clientMessage = $clientMessage;
        $this->logException();
    }

    public function logException(): void
    {
        \Log::error(
            GlobalHelper::getLogType(),
            [
                'data' => [
                    'log_type'       => GlobalHelper::getLogType(),
                    'description'    => $this->getMessage() . ' in ' . $this->getFile() . ' line ' . $this->getLine(),
                    'client_message' => $this->getClientMessage(),
                ],
            ]
        );
    }

    public function answer(DataTransferObject $dto = new BoolDTO(false)): Response
    {
        return new Response($this->code, $this->getClientMessage(), $dto);
    }

    public function notifyAdmin()
    {
//        mail('', 'Error', $this->getMessage());//todo
    }

    public function getClientMessage(): string
    {
        return $this->clientMessage;
    }

}

 

Есть пара моментов - clientMessage и метод answer - это часть структуры проекта, вся работа контроллеров вынесена в Library, контроллер только вызывает библу и передает данные, но ответ от библы всегда одного типа - Response, он принимает в себя ДТО, код ответа и сообщение которое будет передано клиенту (апи/вьюха - не важно)

notifyAdmin - как раз концептуальный пример того, что можно сделать в базовом исключении

 

Опубликовано 23.01.2025
ООП Laravel

Шпаргалка - Laravel Collections

Eloquent\Collection в Laravel - это расширенная версия базового класса Illuminate\Support\Collection, который предоставляет множество полезных методов для работы с массивами данных. Вот некоторые из методов, которые доступны в Eloquent\Collection:

  1. all - Возвращает все элементы коллекции.

  2. avg (или average) - Возвращает среднее значение заданного ключа.

  3. chunk - Разбивает коллекцию на несколько меньших коллекций заданного размера.

  4. collapse - Объединяет массив массивов в один массив.

  5. contains - Определяет, содержит ли коллекция заданный элемент.

  6. count - Возвращает количество элементов в коллекции.

  7. diff - Сравнивает коллекцию с другим массивом или коллекцией и возвращает значения, которые не присутствуют в переданном массиве.

  8. each - Применяет заданную функцию обратного вызова ко всем элементам коллекции.

  9. filter - Фильтрует коллекцию, используя заданную функцию обратного вызова.

  10. first - Возвращает первый элемент коллекции, который соответствует заданному условию.

  11. groupBy - Группирует элементы коллекции по заданному ключу.

  12. keyBy - Ключирует коллекцию по заданному ключу.

  13. map - Применяет функцию обратного вызова к каждому элементу коллекции и возвращает новую коллекцию с элементами, которые были получены в результате вызова функции.

  14. max - Возвращает максимальное значение заданного ключа.

  15. min - Возвращает минимальное значение заданного ключа.

  16. pluck - Возвращает коллекцию значений для заданного ключа.

  17. push - Добавляет элемент в конец коллекции.

  18. reject - Удаляет элементы из коллекции, соответствующие заданному условию.

  19. sortBy и sortByDesc - Сортирует коллекцию по заданным значениям и ключам.

  20. sum - Возвращает сумму всех элементов коллекции.

  21. unique - Возвращает коллекцию уникальных элементов.

  22. where - Фильтрует коллекцию по заданному ключу и значению.

  23. whereIn и whereNotIn - Фильтрует коллекцию по заданным значениям включения или исключения.

  24. zip - Объединяет значения массива с элементами коллекции.

Это далеко не полный список методов, доступных в Eloquent\Collection. Каждый из этих методов может быть очень полезен в различных сценариях работы с данными в вашем приложении Laravel. Вы также можете использовать метод collect для создания новой коллекции из обычного массива PHP, что позволяет вам использовать все эти методы с любыми массивами данных.

Опубликовано 23.01.2025
Laravel

Шпаргалка - REST

REST (Representational State Transfer) представляет собой архитектурный стиль взаимодействия компонентов в сети. В контексте веб-сервисов REST использует стандартные HTTP-методы для управления данными через API. Вот основные HTTP-методы, используемые в RESTful сервисах, и их назначение:

  1. GET: Используется для извлечения данных с сервера. GET-запросы должны быть идемпотентными, то есть не должны изменять состояние сервера. Их можно выполнять многократно без риска изменения данных или их поведения.

  2. POST: Применяется для создания нового ресурса на сервере. POST-запросы не идемпотентны, что означает, что повторение одного и того же POST-запроса обычно приводит к созданию новых ресурсов.

  3. PUT: Используется для обновления существующего ресурса или создания нового, если он отсутствует, на основе предоставленного URI. PUT-запросы должны быть идемпотентными, так что повторный запрос с теми же данными должен давать тот же результат.

  4. DELETE: Применяется для удаления существующего ресурса по указанному URI. DELETE-запросы также должны быть идемпотентными, и повторное выполнение запроса должно подтверждать удаление ресурса, но не приводить к дополнительным изменениям.

  5. PATCH: Используется для частичного обновления ресурса. В отличие от PUT, который обычно требует отправки полного набора данных ресурса, PATCH применяется для отправки только изменений. PATCH-запросы не обязательно идемпотентны.

  6. HEAD: Аналогичен GET, но сервер возвращает только заголовки ответа без тела сообщения. Это полезно, например, для извлечения метаданных о ресурсе без загрузки самого ресурса.

  7. OPTIONS: Используется для описания параметров связи с ресурсом. С его помощью клиент может узнать, какие HTTP-методы поддерживаются для ресурса. Это может быть использовано для проверки возможностей веб-сервера или для проверки наличия функций, таких как CORS (Cross-Origin Resource Sharing).

Эти методы соответствуют операциям CRUD (Create, Read, Update, Delete), которые являются основой для управления данными. RESTful API, правильно использующий эти методы, может предоставить интуитивно понятный и мощный интерфейс для взаимодействия с ресурсами на сервере.

В Laravel, когда вы генерируете контроллер ресурса с помощью консольной команды artisan, создается контроллер с несколькими методами, каждый из которых соответствует определённой операции CRUD:

  • index для отображения списка ресурсов

  • create для возвращения формы создания нового ресурса

  • store для сохранения нового ресурса в базе данных

  • show для отображения определенного ресурса

  • edit для возвращения формы редактирования существующего ресурса

  • update для обновления ресурса в базе данных

  • destroy для удаления ресурса

Метод store обычно принимает данные из HTTP-запроса POST, обрабатывает их (например, проводит валидацию), и если данные корректны, создает новый ресурс в базе данных. Вот пример того, как может выглядеть метод store в контроллере ресурса:

 
public function store(Request $request)
{
    // Валидация входящих данных
    $validatedData = $request->validate([
        'name' => 'required|max:255',
        // другие поля для валидации
    ]);

    // Создание нового ресурса
    $resource = new Resource($validatedData);
    $resource->save();

    // Перенаправление на страницу с ресурсами или вывод сообщения об успехе
    return redirect()->route('resources.index')->with('success', 'Resource created successfully!');
}

Этот метод store соответствует действию "Create" в CRUD и обычно вызывается в ответ на POST запрос на соответствующий URI.

Просто список:

public  function get(){}
public  function post(){}
public  function put(){}
public  function delete(){}
public  function patch(){}
public  function head(){}
public  function options(){}
Опубликовано 23.01.2025
ООП

Разделение бизнес-логики

1. Разделение ответственности

 

Контроллеры должны быть ответственны только за обработку запросов и возврат ответов. Они не должны содержать бизнес-логику.

Модели должны быть ответственны за взаимодействие с базой данных. Они могут содержать методы для работы с данными, но бизнес-логика должна быть вынесена в отдельные классы.

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

 

2. Пример структуры

Вот пример, в котором у нас есть управление пользователями и их профилями.

Модель User

 

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}

 

Контроллер UserController

namespace App\Http\Controllers;

use App\Http\Requests\UserRequest;
use App\Services\UserService;
use Illuminate\Http\Request;

class UserController extends Controller
{
    protected $userService;

    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    public function store(UserRequest $request)
    {
        $user = $this->userService->createUser($request->validated());

        return response()->json($user, 201);
    }

    public function update(UserRequest $request, $id)
    {
        $user = $this->userService->updateUser($id, $request->validated());

        return response()->json($user);
    }

    public function delete($id)
    {
        $this->userService->deleteUser($id);

        return response()->json(null, 204);
    }
}
 
 
#Сервис UserService

namespace App\Services;

use App\Models\User;
use Illuminate\Support\Facades\Hash;

class UserService
{
    public function createUser(array $data): User
    {
        $data['password'] = Hash::make($data['password']);
        return User::create($data);
    }

    public function updateUser(int $id, array $data): User
    {
        $user = User::findOrFail($id);
        if (isset($data['password'])) {
            $data['password'] = Hash::make($data['password']);
        }
        $user->update($data);
        return $user;
    }

    public function deleteUser(int $id): void
    {
        $user = User::findOrFail($id);
        $user->delete();
    }
}

 

Запросы UserRequest

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UserRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:8|confirmed',
        ];
    }
}

 

3. Объяснение

  • Контроллеры: В контроллере UserController мы видим, что он отвечает за обработку HTTP-запросов и передачу данных в сервисы. Контроллеры не содержат бизнес-логику.
  • Модели: Модель User отвечает за взаимодействие с базой данных. Она содержит атрибуты и методы, специфичные для работы с таблицей пользователей.
  • Сервисы: Класс UserService содержит бизнес-логику для создания, обновления и удаления пользователей. Здесь мы можем инкапсулировать все правила и операции, которые касаются управления пользователями.
  • Запросы: Класс UserRequest используется для валидации входящих данных. Это помогает контроллерам оставаться чистыми и сосредоточенными на обработке запросов.
 

4. Преимущества

  • Чистый код: Разделение ответственности делает код более чистым и понятным.
  • Тестируемость: Легче тестировать бизнес-логику, так как она вынесена в отдельные классы.
  • Повторное использование: Сервисы можно использовать в разных частях приложения, что способствует повторному использованию кода.
  • Поддерживаемость: Легче поддерживать и расширять код, так как каждая часть системы имеет свою четко определенную роль.

В Laravel и других современных фреймворках принято разделять бизнес-логику и логику взаимодействия с базой данных. Однако, в некоторых случаях, определенные аспекты бизнес-логики могут быть удобно размещены в моделях. Ниже приведены несколько примеров, как это можно сделать правильно.

 

1. Accessors и Mutators

Accessors и Mutators позволяют вам изменять значение атрибутов модели при их получении или установке.

Пример

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    // Mutator для шифрования пароля перед сохранением
    public function setPasswordAttribute($value)
    {
        $this->attributes['password'] = bcrypt($value);
    }

    // Accessor для форматирования имени пользователя
    public function getNameAttribute($value)
    {
        return ucfirst($value);
    }
}

Это концептуальный пример, лично я создаю trait и через use добавляю в модель, именую так: для модели User аксессор будет - UserAccessorTrait, такой вариант мне кажется более чистым

 

2. Локальные Scopes

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

Пример

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    // Локальный scope для активных пользователей
    public function scopeActive(Builder $query)
    {
        return $query->where('active', 1);
    }
}

 

Использование:
$activeUsers = User::active()->get();

 

3. Методы модели

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

Пример

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    // Метод для проверки, является ли пользователь администратором
    public function isAdmin()
    {
        return $this->role === 'admin';
    }
}

 

4. События модели

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

Пример

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use HasFactory, Notifiable;

    protected static function booted()
    {
        static::creating(function ($user) {
            // Бизнес-логика перед созданием пользователя
            // Например, установка значения по умолчанию
            if (empty($user->role)) {
                $user->role = 'user';
            }
        });

        static::updating(function ($user) {
            // Бизнес-логика перед обновлением пользователя
        });
    }
}

 

Когда бизнес-логика в моделях оправдана?

  • Простые операции: Если бизнес-логика проста и касается только одного атрибута модели, использование Accessors и Mutators может быть оправданным.
  • Повторяющиеся условия: Локальные Scopes подходят для повторяющихся условий запроса.
  • Логика, специфичная для модели: Методы модели и события модели могут быть использованы для логики, которая тесно связана с конкретной моделью.
 

Когда избегать бизнес-логики в моделях?

  • Сложная логика: Если логика сложная и затрагивает несколько моделей или внешние системы, лучше вынести её в сервисы или отдельные классы.
  • Повторное использование: Если логика может быть повторно использована в разных частях приложения, её лучше вынести в отдельные классы.

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

 

1. Сервисные классы (Services)

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

Пример

namespace App\Services;

use App\Models\User;
use Illuminate\Support\Facades\Hash;

class UserService
{
    public function createUser(array $data): User
    {
        $data['password'] = Hash::make($data['password']);
        return User::create($data);
    }

    public function updateUser(int $id, array $data): User
    {
        $user = User::findOrFail($id);
        if (isset($data['password'])) {
            $data['password'] = Hash::make($data['password']);
        }
        $user->update($data);
        return $user;
    }

    public function deleteUser(int $id): void
    {
        $user = User::findOrFail($id);
        $user->delete();
    }
}

 

2. Репозитории (Repositories)

Репозитории используются для инкапсуляции логики доступа к данным. Это позволяет отделить бизнес-логику от логики взаимодействия с базой данных.

Пример

namespace App\Repositories;

use App\Exceptions\Tasks\TaskNotCreatedException;
use App\Models\Task;

class TasksRepository
{
    private Task $model;

    public function __construct()
    {
        $this->model = new Task();
    }

    /**
     * @param $user_id
     * @param $amount
     * @param $application_date
     * @param $way_code
     * @param $uuid
     * @param null $terms
     * @param null $loanProductId
     * @return Task
     * @throws TaskNotCreatedException
     */
    public function createTask($user_id, $amount, $application_date, $way_code, $uuid, $terms = null, $loanProductId = null): Task
    {
        try {
            return $this->model->create(
                [
                    'user_id'          => $user_id,
                    'amount'           => $amount,
                    'way_code'         => $way_code,
                    'uuid'             => $uuid,
                    'application_date' => $application_date,
                    'terms'            => $terms,
                    'loan_product_id'  => $loanProductId,
                ]
            );
        } catch (\Exception $exception) {
            throw new TaskNotCreatedException($exception->getMessage());
        }
    }

    public static function createTaskStatic($user_id, $amount, $application_date, $way_code, $uuid, $terms = null, $loanProductId = null): Task
    {
        return Task::create(
            [
                'user_id'          => $user_id,
                'amount'           => $amount,
                'way_code'         => $way_code,
                'uuid'             => $uuid,
                'application_date' => $application_date,
                'terms'            => $terms,
                'loan_product_id'  => $loanProductId,
            ]
        );
    }
}
 

3. Доменные сервисы (Domain Services)

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

Пример

namespace App\Domain\Services;

use App\Models\Order;
use App\Models\Product;

class OrderService
{
    public function createOrder(array $orderData, array $products): Order
    {
        $order = Order::create($orderData);

        foreach ($products as $productData) {
            $product = Product::find($productData['id']);
            $order->products()->attach($product, ['quantity' => $productData['quantity']]);
        }

        return $order;
    }
}
 

4. События и слушатели (Events and Listeners)

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

Пример

Событие

namespace App\Events;

use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class UserRegistered
{
    use Dispatchable, SerializesModels;

    public $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }
}

 

Слушатель

namespace App\Listeners;

use App\Events\UserRegistered;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;

class SendWelcomeEmail implements ShouldQueue
{
    use InteractsWithQueue;

    public function handle(UserRegistered $event)
    {
        // Логика отправки приветственного письма
    }
}

 

5. Политики (Policies)

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

Пример

namespace App\Policies;

use App\Models\User;
use App\Models\Post;

class PostPolicy
{
    public function update(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }

    public function delete(User $user, Post $post)
    {
        return $user->id === $post->user_id;
    }
}

 

6. Правила (Rules)

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

Пример

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class ValidAge implements Rule
{
    public function passes($attribute, $value)
    {
        return $value >= 18;
    }

    public function message()
    {
        return 'The :attribute must be at least 18 years old.';
    }
}

 

Заключение

Размещение бизнес-логики в соответствующих классах, таких как сервисы, репозитории, доменные сервисы, события, слушатели, политики и правила, помогает поддерживать чистоту и управляемость кода. Это также способствует лучшему разделению ответственности и облегчает тестирование и сопровождение приложения.

Опубликовано 23.01.2025
ООП Laravel

Load или With?

В Laravel, with и load используются для загрузки связанных моделей, но в разных контекстах. Они являются частью механизма "Eager Loading", который помогает решить проблему N+1 запросов, загружая связанные модели заранее, вместо того чтобы делать это отдельным запросом для каждой модели.

 

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

 

with используется при первоначальном построении запроса Eloquent и позволяет указать, какие связи должны быть загружены "жадно". Он эффективен, когда вы знаете, что вам понадобятся связанные модели заранее, до того, как вы получите основные модели.

Пример использования with:

$books = Book::with('author')->get();

 

Плюсы:
  • Уменьшает количество запросов к базе данных, загружая все нужные связи одним запросом.
  • Улучшает производительность, особенно при работе с большими наборами данных.
  • Позволяет избежать проблемы N+1 запросов с самого начала.

Минусы:

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

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

 

load используется для загрузки связей на уже существующем экземпляре модели. Это полезно, когда вы уже имеете модель и решаете, что вам нужны ее связи после того, как она была получена.

Пример использования load:

$book = Book::find(1);
$book->load('author');

Плюсы:

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

Минусы:

  • Если использовать load бездумно, можно случайно столкнуться с проблемой N+1 запросов, так как каждый вызов load будет выполнять отдельный запрос к базе данных.
  • Не так эффективно, как with, если вы знаете, что вам нужны связи заранее.
 

Когда использовать with или load?

 

  • Используйте with, когда вы знаете, что вам нужны связанные данные вместе с основными данными, и вы хотите минимизировать количество запросов к базе данных.
  • Используйте load, когда вам нужно динамически добавить связи к уже извлеченным моделям, особенно если необходимость в связях зависит от каких-то условий или логики, которая выполняется после первоначального запроса.

Оба эти метода являются мощными инструментами Eloquent и могут значительно улучшить производительность вашего приложения, если их использовать правильно.

Опубликовано 21.01.2025
Laravel Оптимизация