Разделение бизнес-логики
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.';
}
}
Заключение
Размещение бизнес-логики в соответствующих классах, таких как сервисы, репозитории, доменные сервисы, события, слушатели, политики и правила, помогает поддерживать чистоту и управляемость кода. Это также способствует лучшему разделению ответственности и облегчает тестирование и сопровождение приложения.