Строитель (Builder) паттерн
Это один из самых недооценённых, но мощных паттернов, особенно когда нужно конструировать сложные объекты пошагово. Сейчас всё разложу по полочкам, примеры — на Laravel + PHP 8.3.
Суть
Builder позволяет создавать сложные объекты поэтапно, а не через перегруженный конструктор или кучу set-методов. Он изолирует процесс построения объекта от его конечной репрезентации.
🔧 Когда применять
-
Объект состоит из множества параметров, не все из которых обязательны;
-
Нужно разные представления одного и того же объекта;
-
Нужно собирать объект поэтапно — особенно если сборка зависит от условий;
-
Нужен чистый код, без "спагетти" из
if,new,set*()и т.п.
📦 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() и достаточно).
❌ Когда количество шагов фиксировано и без условий.
Абстрактная фабрика (Abstract Factory) паттерн
📌 Суть
Абстрактная фабрика — это паттерн проектирования, который предоставляет интерфейс для создания семейства взаимосвязанных объектов без указания их конкретных классов.
Если обычная фабрика создает один объект — то абстрактная фабрика создает набор объектов, которые логически связаны и могут взаимодействовать друг с другом.
📦 Когда нужен этот паттерн?
Когда у тебя есть несколько реализаций целой системы или под-системы, и тебе нужно легко переключаться между ними.
🎯 Пример из жизни (Laravel Style)
Представь, ты делаешь систему уведомлений, которая может работать в разных каналах: email, sms, slack.
Каждый канал может состоять из нескольких компонентов:
-
Генератор шаблона
-
Отправщик
-
Логгер
И ты хочешь создать всё это в рамках одного семейства объектов.
Интерфейсы
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-компоненты |
Фабрика (Fabric) паттерн
Паттерн «Фабрика» (Factory)
📌 Что это такое?
Factory — это порождающий паттерн проектирования, цель которого — инкапсулировать логику создания объектов. То есть, вместо того чтобы напрямую писать new Class(), мы обращаемся к фабричному методу, который решает, какой объект и как создать.
Это особенно полезно, если:
-
ты не хочешь, чтобы код знал, какой именно класс создаётся;
-
тебе нужно создавать разные реализации одного интерфейса;
-
в процессе создания объекта нужна сложная логика.
Пример из реальной жизни
Допустим, ты делаешь сервис оплаты. У тебя есть несколько платёжных провайдеров: Stripe, PayPal, Tinkoff.
Ты можешь создать интерфейс 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 Dispatchers, Notification Channels, Log Handlers — все они могут использовать фабричный подход.
Когда использовать
✅ Когда создание объекта зависит от условий (тип, конфигурация, пользовательская роль).
✅ Когда нужно разделить ответственность между логикой приложения и созданием объектов.
❌ Не использовать, если объект тривиален (new User() без условий и зависимостей).
Паттерны проектирования - введение
Паттерны проектирования — это типовые решения часто встречающихся проблем в разработке программного обеспечения. Они помогают писать чистый, поддерживаемый и масштабируемый код.
В 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)
Я буду описывать каждый в следующих статьях
Сортировка таблицы - простой вариант 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(), иначе гг
На этом всё
Кеширование|Логирование|Ретраи на примере SMS
🔹 1. Кеширование
Зачем кешировать отправку SMS?
- Чтобы не дёргать внешний API слишком часто, если запросы повторяются (например, при повторном нажатии "Отправить код").
- Чтобы ускорить работу – достать данные из кеша быстрее, чем отправить HTTP-запрос.
- Чтобы избежать лишних трат – API-провайдеры SMS часто берут деньги за каждый запрос.
Как кешировать отправку SMS?
Допустим, мы не хотим отправлять одно и то же SMS чаще, чем раз в 5 минут.
✅ Создадим CacheSmsLibrary, которая оборачивает основную библиотеку и кеширует отправленные SMS:
📌 Теперь если одно и то же SMS пытаются отправить повторно в течение 5 минут – оно не будет отправлено повторно.
Применение:
🔹 2. Логирование
Зачем логировать отправку SMS?
- Для отладки – видеть, какие сообщения были отправлены.
- Для мониторинга – можно анализировать частоту отправок.
- Для обработки ошибок – фиксировать неудачные попытки.
Как логировать?
✅ Создадим LoggingSmsLibrary, которая оборачивает основную библиотеку и логирует отправку:
Теперь все отправки SMS логируются, а если что-то пошло не так – мы это увидим в storage/logs/laravel.log.
Применение:
👉 Теперь все вызовы SMS отправки будут логироваться.
🔹 3. Ретраи (повторная отправка при неудаче)
Зачем делать ретраи?
- Внешний API может временно не работать (например, проблемы с сервером провайдера).
- Интернет-соединение может прерываться – если SMS API недоступен, стоит попробовать ещё раз.
- Некоторые ошибки можно обработать повторной попыткой (например, если сервер ответил
500– можно повторить).
Как делать ретраи?
✅ Создадим RetrySmsLibrary, которая повторяет отправку SMS до 3 раз, если произошла ошибка:
📌 Теперь если первый запрос не удался, библиотека попробует ещё 2 раза с задержкой 2 секунды.
Применение:
💡 Итог: Комбинируем всё вместе!
Теперь можно объединить все улучшения в цепочку классов:
📌 Теперь у нас:
✅ Кеширование (не отправляем SMS повторно в течение 5 минут).
✅ Логирование (все попытки и ошибки фиксируются).
✅ Ретраи (если SMS не отправилось, пробуем ещё 2 раза).
Вся магия происходит без изменений в коде контроллера и сервиса! 🚀
🎯 Вывод
- Кеширование – помогает избежать повторных запросов, уменьшает нагрузку на API.
- Логирование – фиксирует отправки и ошибки, помогает при отладке.
- Ретраи – позволяют повторять запрос при временных ошибках.
Когда стоит использовать эти техники?
| Метод | Когда использовать? |
|---|---|
| Кеширование | Если есть вероятность повторных SMS за короткий период (например, OTP-коды) |
| Логирование | Всегда – полезно для мониторинга и отладки |
| Ретраи | Если API SMS-провайдера не всегда стабильно работает |
🔥 Лучший вариант – использовать все 3 техники, обернув основную библиотеку в цепочку классов! 🚀
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 записи до одной с понятным выводом о применении джобы
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 - как раз концептуальный пример того, что можно сделать в базовом исключении
Шпаргалка - Laravel Collections
Eloquent\Collection в Laravel - это расширенная версия базового класса Illuminate\Support\Collection, который предоставляет множество полезных методов для работы с массивами данных. Вот некоторые из методов, которые доступны в Eloquent\Collection:
-
all - Возвращает все элементы коллекции.
-
avg (или average) - Возвращает среднее значение заданного ключа.
-
chunk - Разбивает коллекцию на несколько меньших коллекций заданного размера.
-
collapse - Объединяет массив массивов в один массив.
-
contains - Определяет, содержит ли коллекция заданный элемент.
-
count - Возвращает количество элементов в коллекции.
-
diff - Сравнивает коллекцию с другим массивом или коллекцией и возвращает значения, которые не присутствуют в переданном массиве.
-
each - Применяет заданную функцию обратного вызова ко всем элементам коллекции.
-
filter - Фильтрует коллекцию, используя заданную функцию обратного вызова.
-
first - Возвращает первый элемент коллекции, который соответствует заданному условию.
-
groupBy - Группирует элементы коллекции по заданному ключу.
-
keyBy - Ключирует коллекцию по заданному ключу.
-
map - Применяет функцию обратного вызова к каждому элементу коллекции и возвращает новую коллекцию с элементами, которые были получены в результате вызова функции.
-
max - Возвращает максимальное значение заданного ключа.
-
min - Возвращает минимальное значение заданного ключа.
-
pluck - Возвращает коллекцию значений для заданного ключа.
-
push - Добавляет элемент в конец коллекции.
-
reject - Удаляет элементы из коллекции, соответствующие заданному условию.
-
sortBy и sortByDesc - Сортирует коллекцию по заданным значениям и ключам.
-
sum - Возвращает сумму всех элементов коллекции.
-
unique - Возвращает коллекцию уникальных элементов.
-
where - Фильтрует коллекцию по заданному ключу и значению.
-
whereIn и whereNotIn - Фильтрует коллекцию по заданным значениям включения или исключения.
-
zip - Объединяет значения массива с элементами коллекции.
Это далеко не полный список методов, доступных в Eloquent\Collection. Каждый из этих методов может быть очень полезен в различных сценариях работы с данными в вашем приложении Laravel. Вы также можете использовать метод collect для создания новой коллекции из обычного массива PHP, что позволяет вам использовать все эти методы с любыми массивами данных.
Шпаргалка - REST
REST (Representational State Transfer) представляет собой архитектурный стиль взаимодействия компонентов в сети. В контексте веб-сервисов REST использует стандартные HTTP-методы для управления данными через API. Вот основные HTTP-методы, используемые в RESTful сервисах, и их назначение:
-
GET: Используется для извлечения данных с сервера. GET-запросы должны быть идемпотентными, то есть не должны изменять состояние сервера. Их можно выполнять многократно без риска изменения данных или их поведения.
-
POST: Применяется для создания нового ресурса на сервере. POST-запросы не идемпотентны, что означает, что повторение одного и того же POST-запроса обычно приводит к созданию новых ресурсов.
-
PUT: Используется для обновления существующего ресурса или создания нового, если он отсутствует, на основе предоставленного URI. PUT-запросы должны быть идемпотентными, так что повторный запрос с теми же данными должен давать тот же результат.
-
DELETE: Применяется для удаления существующего ресурса по указанному URI. DELETE-запросы также должны быть идемпотентными, и повторное выполнение запроса должно подтверждать удаление ресурса, но не приводить к дополнительным изменениям.
-
PATCH: Используется для частичного обновления ресурса. В отличие от PUT, который обычно требует отправки полного набора данных ресурса, PATCH применяется для отправки только изменений. PATCH-запросы не обязательно идемпотентны.
-
HEAD: Аналогичен GET, но сервер возвращает только заголовки ответа без тела сообщения. Это полезно, например, для извлечения метаданных о ресурсе без загрузки самого ресурса.
-
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(){}Разделение бизнес-логики
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);
}
}
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.';
}
}
Заключение
Размещение бизнес-логики в соответствующих классах, таких как сервисы, репозитории, доменные сервисы, события, слушатели, политики и правила, помогает поддерживать чистоту и управляемость кода. Это также способствует лучшему разделению ответственности и облегчает тестирование и сопровождение приложения.
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 и могут значительно улучшить производительность вашего приложения, если их использовать правильно.