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

Отредачено 23.01.2025,
а чекнуто 666 💁

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.';
    }
}

 

Заключение

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

ООП Laravel