- Опубликовано: 7 мар 2025
- 94
Laravel Design Patterns, которые вам нужно знать
Представьте, что вы разрабатываете приложение на Laravel. Сначала всё идёт гладко — контроллеры получают данные из моделей, представления отображают результаты, и жизнь прекрасна. Но по мере роста проекта код начинает разрастаться, логика переплетается, и изменения становятся всё сложнее. Дизайн-паттерны приходят на помощь, чтобы организовать код, сделать его чище и облегчить поддержку. В этой статье мы поговорим о самых распространённых паттернах проектирования в Laravel, таких как Repository, Singleton, Factory, Observer, Facade и Strategy. Мы разберём, как они работают, в чём их польза и покажем пошаговые примеры кода для каждого паттерна. Стиль изложения будет простой и понятный, без лишнего формализма, чтобы материал был доступен и интересен. Поехали!
Паттерн Репозиторий (Repository)
Паттерн Репозиторий выступает прослойкой между бизнес-логикой приложения и источником данных. Проще говоря, репозиторий — это класс, через который ваш код получает доступ к данным, не зная деталей их хранения. Это своего рода абстракция уровня данных, позволяющая работать с объектами так, будто они в памяти, а не в базе данных (Паттерн «Репозиторий» в Laravel | Уроки Laravel). Бизнес-логика обращается к репозиторию, чтобы получить нужные данные, и ей всё равно, откуда эти данные — из MySQL, MongoDB или внешнего API. Такой подход облегчает поддержку: если изменится источник данных, вам достаточно изменить код репозитория, не трогая остальную часть приложения.
Зачем использовать Репозиторий? Во-первых, для разделения обязанностей: контроллеры и сервисы занимаются логикой, а репозиторий — общением с базой данных. Это делает код чище и тестируемее. Во-вторых, репозиторий способствует принципу инверсии зависимостей — вы пишете код против абстракций (интерфейсов), а не конкретных классов, что упрощает смену реализации хранилища данных (например, переход с Eloquent на другой ORM) (How to Use the Repository Pattern in a Laravel Application | Twilio) (How to Use the Repository Pattern in a Laravel Application | Twilio).
Реализация паттерна Репозиторий
Рассмотрим простой пример: допустим, у нас есть модель User
и нам нужен репозиторий для пользователей. Мы создадим интерфейс репозитория, реализацию под Eloquent и научим Laravel подменять класс модели на наш репозиторий в нужных местах.
Шаг 1. Создаём интерфейс репозитория. Интерфейс определяет методы, которые мы ожидаем от репозитория. Например, метод для получения всех пользователей, поиска по ID и создания нового пользователя:
<?php
namespace App\Repositories;
interface UserRepositoryInterface
{
public function all(): iterable;
public function find(int $id);
public function create(array $data);
// ... другие необходимые методы
}
Здесь мы описали контракт нашего хранилища пользователей, не вдаваясь в детали реализации.
Шаг 2. Реализуем интерфейс с помощью Eloquent. Создадим класс UserRepository
, который реализует UserRepositoryInterface
и внутри использует модель User
для выполнения операций. Например:
<?php
namespace App\Repositories;
use App\Models\User;
class UserRepository implements UserRepositoryInterface
{
public function all(): iterable
{
return User::all();
}
public function find(int $id)
{
return User::find($id);
}
public function create(array $data)
{
return User::create($data);
}
}
В этом классе мы вынесли всю работу с моделью User
– получаем всех пользователей, ищем по ID и создаём новую запись. Контроллеры будут пользоваться репозиторием, не обращаясь напрямую к модели.
Шаг 3. Привязываем интерфейс к реализации в контейнере. Чтобы Laravel понял, какой класс использовать для интерфейса, зарегистрируем связывание в сервис-провайдере. Обычно это можно сделать в app/Providers/AppServiceProvider.php
в методе register()
:
use App\Repositories\UserRepository;
use App\Repositories\UserRepositoryInterface;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// При обращении к интерфейсу вернуть экземпляр UserRepository
$this->app->bind(UserRepositoryInterface::class, UserRepository::class);
}
// ...
}
Метод bind
сообщает сервис-контейнеру Laravel: "если кто-то запросит UserRepositoryInterface
, предоставь ему UserRepository
". Также можно использовать $this->app->singleton
вместо bind
, если вы хотите, чтобы создавался единственный экземпляр репозитория (например, для кэша данных), но чаще для репозиториев достаточно bind, чтобы при каждом запросе давать новый экземпляр.
Шаг 4. Используем репозиторий в коде. Теперь мы можем в контроллере получить репозиторий через внедрение зависимостей (Dependency Injection). Например, в контроллере пользователей:
use App\Repositories\UserRepositoryInterface;
class UserController extends Controller
{
protected $users;
// Laravel сам передаст нужную реализацию репозитория в конструктор
public function __construct(UserRepositoryInterface $users)
{
$this->users = $users;
}
public function index()
{
// Получаем всех пользователей через репозиторий
$allUsers = $this->users->all();
return view('users.index', ['users' => $allUsers]);
}
public function show($id)
{
$user = $this->users->find($id);
return view('users.show', ['user' => $user]);
}
public function store(Request $request)
{
$newUser = $this->users->create($request->all());
// ... например, редирект или возврат ответа
}
}
Обратите внимание: мы совершенно не вызываем User::find
или User::create
напрямую в контроллере. Вместо этого контроллер "знает" только об интерфейсе UserRepositoryInterface
. Laravel сам передаст объект UserRepository
в конструктор контроллера, согласно привязке, которую мы описали в сервис-провайдере. В результате контроллер не зависит от Eloquent или конкретной реализации хранения данных — мы можем заменить UserRepository
на другую реализацию (например, хранящую пользователей в кэше или вообще получающую их по API) без изменения кода контроллера. Код стал более гибким и тестируемым: можно подставить фейковый репозиторий при тестировании, избежав реальных запросов к базе.
Паттерн Одиночка (Singleton)
Паттерн Singleton (Одиночка) — это способ сделать так, чтобы у класса была только одна единственная экземпляр (объект) во всем приложении. Обычно этот паттерн используют для классов, которые управляют каким-то общим ресурсом или хранят глобальные настройки — например, класс для работы с конфигурацией, логированием или подключением к внешнему API. В Laravel паттерн одиночка часто встречается при работе с сервис-контейнером, когда мы регистрируем singleton сервисы: это гарантирует, что Laravel будет пересоздавать объект при каждом запросе, а будет использовать один и тот же экземпляр.
Зачем нужен Singleton? Представьте, что у вас есть класс для подключения к внешнему сервису или к файлу. Если при каждом использовании создавать новый объект, можно тратить лишние ресурсы или получать несогласованные данные. Singleton же обеспечивает единое "истинное состояние": все части программы используют один экземпляр, данные в нём всегда актуальны и синхронизированы. К тому же singleton позволяет централизованно управлять доступом к ресурсу и предотвращает создание лишних объектов (например, чтобы не открыть одно и то же соединение несколько раз).
Реализация паттерна Singleton
В классическом виде реализация Singleton в PHP сводится к созданию класса с приватным конструктором и статическим методом доступа к экземпляру. Давайте сделаем простой пример: класс конфигурации AppConfig
, который загружает настройки и предоставляет их остальной части приложения как единый объект.
Шаг 1. Создаём класс Singleton. У класса будет приватный конструктор (чтобы нельзя было сделать new
снаружи) и статический метод getInstance()
, который либо создаёт новый экземпляр, либо возвращает уже созданный:
<?php
namespace App\Services;
class AppConfig
{
private static ?AppConfig $instance = null;
protected array $settings;
// Приватный конструктор: вызывается только внутри самого класса
private function __construct()
{
// Допустим, загружаем настройки из файла или env
$this->settings = [
'mode' => 'production',
'items_per_page' => 10,
// ... другие настройки
];
}
// Статический метод для получения экземпляра (Singleton access)
public static function getInstance(): AppConfig
{
if (self::$instance === null) {
self::$instance = new AppConfig();
}
return self::$instance;
}
// Пример метода для доступа к настройкам
public function get(string $key)
{
return $this->settings[$key] ?? null;
}
}
Здесь AppConfig::getInstance()
либо создаёт объект (при первом вызове), либо возвращает сохранённый в self::$instance
. В итоге где бы в коде мы ни вызвали AppConfig::getInstance()
, мы всегда получим один и тот же объект с одним и тем же набором настроек.
Шаг 2. Используем Singleton. Теперь можно в любом месте кода получить настройки:
$config = App\Services\AppConfig::getInstance();
$mode = $config->get('mode');
Переменная $mode
будет возвращать одно и то же значение, и если вдруг в процессе работы приложения в $config->settings
что-то изменится, все, кто получил $config
через getInstance()
, будут это видеть (потому что разделяют один объект).
В Laravel многие вещи можно зарегистрировать как Singleton в контейнере, не реализуя логику одиночки вручную. Например, если у нас есть свой сервис (класс), мы можем внутри AppServiceProvider
сделать так:
$this->app->singleton(App\Services\AppConfig::class, function ($app) {
return AppConfig::getInstance();
});
Однако часто даже не приходится явно писать собственные одиночки — Laravel сам предоставляет singleton-объекты (например, App
контейнер, объект запроса Request
и т.д. существуют в одном экземпляре на запрос). Но понимать сам паттерн полезно: вы будете знать, как сделать класс с единственным экземпляром и зачем это может понадобиться. Главное — использовать Singleton разумно, так как злоупотребление глобальными состояниями может затруднить тестирование и сопровождение (не зря Singleton иногда шутливо называют антипаттерном).
Паттерн Фабрика (Factory)
Паттерн Factory (Фабрика) — это порождающий шаблон проектирования, цель которого упростить создание объектов. Вместо того чтобы в коде разбросать вызовы конструкторов разных классов, вы выносите логику создания в особое место — фабрику. Фабрика решает, какой именно класс нуждается в создании, и возвращает готовый объект. Это особенно полезно, когда процесс создания сложный или зависит от условий.
В Laravel термин "Factory" также используется для фабрик моделей — удобного механизма для генерации тестовых данных (например, UserFactory
для создания пользователей). Но в контексте дизайн-паттернов мы говорим не только о тестовых данных, а вообще о принципе отделения логики создания объекта от его использования (Laravel Design Pattern: Simplifying Web Development | by techiydude | Stackademic). Это делает код более гибким: вы можете легко менять классы, которые создаются, не переписывая место их вызова. Кроме того, фабрика часто используется совместно с Dependency Injection — фабрика может решать, какой конкретно класс передать, исходя из конфигурации или окружения.
Реализация паттерна Factory
Давайте представим ситуацию: у нас есть приложение, которое может отправлять уведомления пользователям разными способами — по электронной почте или по SMS. У нас будут два класса уведомлений: EmailNotification
и SMSNotification
, оба реализуют общий интерфейс NotificationInterface
с методом send($message)
. Будем использовать фабрику, чтобы создавалось нужное уведомление в зависимости от типа, который мы укажем.
Шаг 1. Определяем интерфейс уведомления и несколько классов, его реализующих:
<?php
namespace App\Notifications;
interface NotificationInterface
{
public function send(string $message): void;
}
// Класс для email-уведомлений
class EmailNotification implements NotificationInterface
{
public function send(string $message): void
{
// Здесь бы произошла реальная отправка email
echo "Sending EMAIL with message: {$message}\n";
}
}
// Класс для SMS-уведомлений
class SMSNotification implements NotificationInterface
{
public function send(string $message): void
{
// Здесь логика отправки SMS
echo "Sending SMS with message: {$message}\n";
}
}
У нас есть два типа уведомлений, и мы могли бы создавать их так: new EmailNotification()
или new SMSNotification()
. Но тогда выбор класса (email или sms) был бы размазан по коду. Вместо этого напишем фабрику.
Шаг 2. Создаём фабрику уведомлений. Фабрика может быть реализована как отдельный класс с методами, либо как статический метод. Для простоты сделаем статический метод:
<?php
namespace App\Notifications;
class NotificationFactory
{
public static function create(string $type): NotificationInterface
{
switch ($type) {
case 'email':
return new EmailNotification();
case 'sms':
return new SMSNotification();
default:
throw new \Exception("Unsupported notification type: $type");
}
}
}
Метод create
принимает параметр $type
(например, 'email'
или 'sms'
) и на его основе возвращает экземпляр соответствующего класса, который реализует NotificationInterface
. Если тип не распознан, выбрасывается исключение (в боевом коде можно сделать обработку ошибок более изящной, но для примера сойдёт).
Шаг 3. Используем фабрику для отправки уведомлений. Теперь в коде, где нужно отправить уведомление, нам не нужно знать детали классов:
use App\Notifications\NotificationFactory;
// Где-то в контроллере или сервисе:
$type = 'email'; // допустим, выбираем тип уведомления (например, на основе настроек пользователя)
$notification = NotificationFactory::create($type);
$notification->send("Ваш заказ отправлен!"); // вызов метода send у нужного класса
Если позже мы решим добавить новый способ уведомлений, например пуш-уведомление (PushNotification
), мы просто создадим новый класс, реализуем NotificationInterface
и добавим новый case
в фабрику. Места, где фабрика используется, менять не придётся — они по-прежнему вызывают NotificationFactory::create($type)
.
Преимущество фабрики очевидно: централизация создания объектов. Удобно контролировать процесс создания, например, можно внутри фабрики применять паттерн Singleton (если хотим переиспользовать один объект) или возвращать разные классы в зависимости от окружения (например, в тестовом окружении возвращать заглушки). Код, вызывающий фабрику, при этом остаётся простым и не меняется.
Паттерн Наблюдатель (Observer)
Паттерн Observer (Наблюдатель) — это шаблон, позволяющий одним объектам подписываться на события в другом объекте и получать уведомления, когда эти события происходят. Говоря проще, есть издатель (subject), который рассылает сообщения о своих изменениях, и есть подписчики (observers), которые эти сообщения получают и реагируют. Этот паттерн помогает организовать реакцию на события без жёсткой связи между классами: издатель не знает о конкретных подписчиках, он просто оповещает "всех, кто подписан".
В Laravel паттерн наблюдатель используется, например, для отслеживания событий моделей (Model Events). Вы можете определить Observer-класс для модели, чтобы выполнять код при создании, обновлении, удалении записей и т.д. Это позволяет вынести побочные эффекты (например, отправку письма при регистрации пользователя, запись логов при обновлении модели) из контроллеров или моделей в отдельные классы.
Реализация паттерна Observer
Рассмотрим пример: у нас есть модель User
. Мы хотим автоматически отправлять приветственное письмо новому пользователю после регистрации. Вместо того чтобы вставлять код отправки письма прямо в контроллер или модель, мы создадим наблюдателя, который будет "слушать" событие создания пользователя.
Шаг 1. Создаём класс Observer. В Laravel достаточно создать класс, например UserObserver
, с методами-обработчиками событий модели. Для события "создание" Eloquent вызывает метод created
(после успешного сохранения нового объекта). Вот пример наблюдателя:
<?php
namespace App\Observers;
use App\Models\User;
use Mail; // условный фасад для отправки почты
class UserObserver
{
// Метод вызывается после создания нового пользователя
public function created(User $user)
{
// Отправляем приветственное письмо новому пользователю
Mail::to($user->email)->send(new \App\Mail\WelcomeMail($user));
}
// Можно добавить и другие события, например:
// public function updated(User $user) { ... }
// public function deleted(User $user) { ... }
}
У UserObserver
мы реализовали метод created()
, который Laravel вызовет каждый раз, когда новый User
будет сохранён в базу данных. Внутри метода мы выполняем нужное действие — здесь отправляем письмо (предположим, у нас есть Mailable WelcomeMail
для этого).
Шаг 2. Регистрируем Observer для модели. Чтобы Laravel знал, что наш UserObserver
должен следить за событиями модели User
, его нужно зарегистрировать. Обычно это делается в AppServiceProvider
(или можно создать отдельный EventServiceProvider
). Добавим в метод boot
:
use App\Models\User;
use App\Observers\UserObserver;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
User::observe(UserObserver::class);
}
}
Вызывая User::observe(...)
, мы привязываем наш Observer-класс к модели. Теперь при возникновении событий в модели User
(создание, обновление, удаление и пр.) Laravel будет вызывать соответствующие методы нашего UserObserver
.
Шаг 3. Проверяем в действии. Теперь, когда где-то в приложении создаётся пользователь, например:
$user = User::create([
'name' => 'Иван',
'email' => 'ivan@example.com',
'password' => bcrypt('secret'),
]);
После выполнения User::create(...)
Laravel автоматически вызовет UserObserver::created($user)
. Наш наблюдатель отправит письмо Ивану, а контроллеру или коду, создавшему пользователя, не нужно об этом заботиться. Таким образом, бизнес-логика (создание пользователя) отделена от побочного эффекта (отправка письма). Мы легко можем добавить новые действия на это же событие (например, записать информацию в лог) — достаточно расширить UserObserver
, не трогая код создания пользователя.
Laravel также предоставляет систему Events & Listeners, которая концептуально близка к паттерну Observer. Вы можете объявлять события (
event(new UserRegistered($user))
) и слушателей этих событий. Внутренне Eloquent-модели сами используют эту систему для реализации Observer (при вызовеUser::observe
Laravel регистрирует скрытые слушатели на события модели). Но удобство Observer-классов в том, что они группируют логику, связанную с конкретной моделью, в одном месте.
Паттерн Фасад (Facade)
Паттерн Facade (Фасад) — это структурный шаблон проектирования, который предоставляет простой (упрощённый) интерфейс к сложной системе или группе классов. Фасад как бы скрывает сложность за кулисами, предлагая клиентскому коду удобные методы для выполнения подкапотных процессов (php - What is Facades used in Laravel? - Stack Overflow). В контексте Laravel фасады знакомы всем — это те самые классы вроде Auth
, DB
, Cache
, Mail
и многие другие, которые мы вызываем статически, хотя на самом деле за ними скрываются объекты из сервис-контейнера.
Почему фасады удобны? Они позволяют писать выразительный код без необходимости вручную создавать объекты или тянуть их из контейнера при каждом использовании. Например, вместо $cache = app('cache'); $cache->get('key');
мы пишем Cache::get('key')
. Laravel под капотом находит объект нужного сервиса и вызывает метод. При этом фасады остаются более тестируемыми и гибкими, чем прямые статические вызовы, потому что в основе у них всё равно лежат обычные объекты (их можно подменить при тестировании). Фасады в Laravel — это не совсем классический шаблон Facade, но близки к нему: они дают единую точку доступа к функциональности и изолируют детали реализации.
(How Laravel Facades Work and How to Use Them Elsewhere — SitePoint) (How Laravel Facades Work and How to Use Them Elsewhere — SitePoint)Диаграмма ниже иллюстрирует суть паттерна Facade: клиент (Client) обращается к упрощённому интерфейсу (Facade class), а фасад внутри себя вызывает необходимые методы сложных подсистем (Complicated class 1, 2 и т.д.), скрывая их от пользователя. В Laravel роль "сложных классов" выполняют сервисы контейнера, а фасад — это оболочка, позволяющая к ним легко обратиться.
Теперь рассмотрим создание собственного фасада в Laravel в пару шагов.
Реализация паттерна Facade (на примере собственного фасада)
Предположим, у нас есть некий сервис (класс) для получения прогноза погоды, и мы хотим обращаться к нему через красивый фасад Weather
. Покажем упрощённо, как создать такой фасад.
Шаг 1. Создаём сервис-класс. Этот класс выполняет основную работу. Например, получит данные о погоде (для простоты вернём заглушку):
<?php
namespace App\Services;
class WeatherService
{
public function getForecast(string $city): string
{
// В реальности здесь, возможно, запрос к внешнему API погоды
// Пока вернём фиктивные данные:
return "Прогноз погоды для {$city}: солнечно, +20°C.";
}
}
Шаг 2. Регистрируем сервис в контейнере Laravel. Это нужно, чтобы Laravel знал, как создать наш сервис. В AppServiceProvider
добавим:
use App\Services\WeatherService;
class AppServiceProvider extends ServiceProvider
{
public function register()
{
// Регистрируем WeatherService как singleton в контейнере
$this->app->singleton('weather', function($app) {
return new WeatherService();
});
}
// ...
}
Здесь мы связываем строковый ключ 'weather'
с экземпляром WeatherService
. Теперь в любом месте приложения мы можем получить наш сервис через app('weather')
или через резолвинг зависимости.
Шаг 3. Создаём класс Facade. Laravel фасады — это классы, наследующие Illuminate\Support\Facades\Facade
. В нашей фасад-классе нужно определить метод getFacadeAccessor()
который возвращает ключ в контейнере, связанный с базовым объектом. Создадим Weather.php
в app/Facades
:
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class Weather extends Facade
{
protected static function getFacadeAccessor(): string
{
return 'weather'; // тот самый ключ, под которым сервис в контейнере
}
}
Этот минималистичный класс не содержит логики — он просто указывает Facade-наследнику, какой сервис вызывать из контейнера. Laravel, когда увидит вызов Weather::someMethod()
, заглянет в контейнер по ключу 'weather'
и вызовет этот метод на объекте WeatherService
.
Шаг 4. Подключаем фасад для удобного использования. Чтобы фасад класс Weather
стал доступен как Weather::method()
, мы можем добавить его в псевдонимы (aliases). В Laravel < 9 это делалось в config/app.php
в секции aliases
. В Laravel 9+ можно использовать автодискавер пакетов или просто использовать полное имя класса фасада. Для простоты предположим, что мы добавили алиас:
'aliases' => [
// ... другие фасады ...
'Weather' => App\Facades\Weather::class,
],
Теперь мы готовы использовать наш фасад.
Шаг 5. Используем фасад. Где-нибудь в контроллере можем написать:
$forecast = \Weather::getForecast('Таллин');
// $forecast теперь содержит "Прогноз погоды для Таллин: солнечно, +20°C."
При вызове Weather::getForecast(...)
Laravel найдет в контейнере зарегистрированный ранее weather
(наш WeatherService
) и вызовет метод getForecast
у его экземпляра, вернув результат.
Итак, мы получили статически вызываемый фасад для нашего сервиса. Мы спрятали за фасадом детали: контроллеру не нужно знать, как именно WeatherService
получает данные, или что он называется 'weather'
в контейнере. Он просто вызывает понятный метод фасада.
Преимущества фасадов: лаконичность кода и удобство. Вместо передачи объектов в каждый класс или использование хелперов app()
, мы получаем красивые однострочники. Главное — не злоупотреблять фасадами там, где прямая DI (Dependency Injection) лучше. Но для типичных случаев (кеш, очередь, логирование и т.п.) Laravel фасады значительно экономят время.
Паттерн Стратегия (Strategy)
Паттерн Strategy (Стратегия) — это поведенческий паттерн, который позволяет определить семейство алгоритмов (или вариантов поведения), и выбрать один из них во время выполнения программы. Объект, который использует стратегию, не знает деталей реализации алгоритма — он просто вызывает метод через общий интерфейс. Звучит абстрактно, но на практике стратегия помогает подставлять разную логику, не меняя окружающий код.
В Laravel (и вообще в PHP) стратегия часто используется, когда нужно по-разному обрабатывать данные в зависимости от настроек. Например, в самом Laravel есть разные драйверы кэширования (файловый, мемкеш, базу данных и т.д.) — они все реализуют единый интерфейс кэша, и в runtime выбирается нужная реализация (по сути, стратегия кэширования) (Laravel Design Pattern: Simplifying Web Development | by techiydude | Stackademic). Вы, как разработчик, можете тоже применять этот паттерн: скажем, переключаться между разными способами оплаты, разными алгоритмами расчёта или форматирования данных.
Реализация паттерна Strategy
Возьмём пример с оплатой заказов. Допустим, наше приложение поддерживает несколько способов оплаты: кредитной картой и PayPal. Мы хотим иметь возможность легко добавить новые способы в будущем, и не запутать код условностями вроде if ($method == 'card') { ... } else if ...
. Реализуем стратегию:
Шаг 1. Определяем интерфейс стратегии оплаты.
<?php
namespace App\Payment;
interface PaymentStrategy
{
public function pay(float $amount): bool;
}
Интерфейс PaymentStrategy
содержит метод pay
для выполнения оплаты на указанную сумму. Он возвращает bool
(например, успех или провал платежа).
Шаг 2. Реализуем несколько стратегий оплаты.
<?php
namespace App\Payment;
class CreditCardPayment implements PaymentStrategy
{
public function pay(float $amount): bool
{
// Логика оплаты кредитной картой
echo "Processing credit card payment of \${$amount}\n";
// ... (в реальности здесь взаимодействие с API платежного шлюза)
return true;
}
}
class PayPalPayment implements PaymentStrategy
{
public function pay(float $amount): bool
{
// Логика оплаты через PayPal
echo "Processing PayPal payment of \${$amount}\n";
// ... (взаимодействие с PayPal API)
return true;
}
}
У нас есть два класса, каждый реализует PaymentStrategy
по-своему.
Шаг 3. Создаём контекст, использующий стратегию. Можно, конечно, вызывать стратегии напрямую, но обычно выделяют класс-контекст, который хранит в себе ссылку на текущую стратегию и использует её. Назовём его PaymentProcessor
:
<?php
namespace App\Payment;
class PaymentProcessor
{
private PaymentStrategy $strategy;
public function __construct(PaymentStrategy $strategy)
{
// Внедряем выбранную стратегию через конструктор
$this->strategy = $strategy;
}
public function setStrategy(PaymentStrategy $strategy): void
{
// Дополнительно метод для смены стратегии во время исполнения, если нужно
$this->strategy = $strategy;
}
public function process(float $amount): bool
{
// Перенаправляем вызов к стратегии
return $this->strategy->pay($amount);
}
}
PaymentProcessor
не знает деталей, как именно производится оплата — он просто вызывает $this->strategy->pay()
. Стратегия может быть любая, лишь бы соответствовала интерфейсу PaymentStrategy
.
Шаг 4. Используем стратегию оплаты. Теперь клиентский код может выбрать нужную стратегию и выполнить оплату:
use App\Payment\PaymentProcessor;
use App\Payment\CreditCardPayment;
use App\Payment\PayPalPayment;
// Допустим, выбор способа оплаты сделан на основе настроек пользователя или параметра:
$method = 'paypal'; // или 'card'
if ($method === 'card') {
$strategy = new CreditCardPayment();
} else {
$strategy = new PayPalPayment();
}
$processor = new PaymentProcessor($strategy);
$processor->process(99.00); // вывод: "Processing PayPal payment of $99"
Здесь мы в зависимости от значения $method
выбираем стратегию (в реальности этот выбор мог бы происходить автоматически, например, фабрикой). Затем создаём PaymentProcessor
, передавая ему выбранную стратегию, и вызываем метод process
. Если позже понадобится добавить, например, оплату криптовалютой, мы просто реализуем PaymentStrategy
для нового способа и можем начать его передавать в PaymentProcessor
— остальной код не нужно переписывать.
Где применяется стратегия? Помимо приведённого примера, практически везде, где есть сменные алгоритмы: разные способы авторизации пользователей (пароль, соцсети, OTP), разные форматы экспорта данных (CSV, PDF, XML) — всё это можно реализовать через Strategy. Паттерн помогает поддерживать открытость к расширению: добавление нового поведения не затрагивает старый код, вы просто добавляете новую стратегию.
Заключение
Мы рассмотрели несколько ключевых шаблонов проектирования на примере Laravel: Repository, Singleton, Factory, Observer, Facade и Strategy. Эти паттерны не придуманы специально для Laravel — это общие принципы архитектуры, но Laravel предоставляет удобную почву для их применения. Используя их, вы сможете писать код "как профессионал": более чистый, гибкий и сопровождаемый.
Важно помнить, что паттерны — это инструменты, а не строгие правила. Не надо применять паттерн лишь ради галочки. Всегда оценивайте, принесёт ли он упрощение и пользу в вашем конкретном случае. Например, нет смысла создавать репозиторий для каждой модели в маленьком проекте, но в большом монолите это может значительно улучшить структуру. Singleton удобен для общих сервисов, но избегайте превращать всё в синглтоны. Фасады облегчают жизнь, но тоже имеют свои компромиссы.
Изучайте код Laravel и популярных пакетов — вы обнаружите там множество реализованных паттернов. Со временем вы начнёте "узнавать" их в диком природе и интуитивно понимать, какой шаблон применить для той или иной задачи. А эта осознанность отличает джуна от мидла, мидла от сеньора. Так что прокачивайтесь и пишите отличный код! 😎
Была статья полезной: