Как использовать репозитории и сервисы в Laravel?

Хотелось бы узнать как правильно использовать репозитории и сервисы в Laravel и когда их вообще нужно использовать, поскольку в Laravel есть Eloquent модели.
К примеру есть сайт на тему продажи авто.
Подскажите, насколько правильно или нет будет использовать такой вариант: создаётся интерфейс, который используется как для выборки авто, так и для выборки из базы данных всего остального, к примеру категорий, меню:

namespace App\Repositories\Contracts;

interface SiteRepositoryInterface
{
    public function all();

    public function paginate(array $params);

    public function find(int $id);

    public function findByAlias(string $alias);

}

к примеру выборка авто
namespace App\Repositories;

class CarRepository implements SiteRepositoryInterface
{
    public function all()
    {
        return Car::whereStatus(1)->get();
    }

    public function paginate(array $params)
    {
        $perPage = $params['per_page'] ?? Setting::value('cars_per_page');
        ...
        return $builder->paginate($perPage);
    }

    public function find(int $id)
    {
        return Car::find($id);
    }

    public function findByAlias(string $alias)
    {
        return Car::whereStatus(1)->where('alias', $alias)->first();
    }
}

выборка категорий
namespace App\Repositories;

class CategoryRepository implements SiteRepositoryInterface
{
    public function all()
    {
        return Category::whereStatus(1)->get();
    }

    public function paginate(array $params)
    {
        return [];
    }

    public function find(int $id)
    {
        return Category::find($id);
    }

    public function findByAlias(string $alias)
    {
        return Category::whereStatus(1)->where('alias', $alias)->first();
    }
}

namespace App\Services\Contracts;

interface GeneralAppLayerInterface
{
    public function getData(array $params = null);
}
interface IndexAppLayerInterface extends GeneralAppLayerInterface {}

namespace App\Services;

class GeneralService implements GeneralAppLayerInterface
{
    public $path;
    private $route;
    private $settings;
    private $menu;
    private $modelRepo;

    public function __construct(
        SiteRepositoryInterface $settingRepo,
        SiteRepositoryInterface $menuRepo
    )
    {
        $this->route = Route::currentRouteName();
        $this->settings = $settingRepo;
        $this->menu = $menuRepo;
        $this->path = config('app.public_images');
    }

    public function getData(array $params = null):array
    {
        $settings = $this->settings->find(1)->toArray();
        $menu = $this->menu->all();
        $categories = Category::with('images', 'cars.mark', 'marks.models', 'marks.cars')
            ->whereStatus(1)->where('parent_id', null)->get();

        return [
            'route' => $this->route,
            'title' => $page->name ?? '',
            'publicPath' => $this->path,
            'menu' => $menu,
            'categories' => $categories,
        ];
    }
}

namespace App\Services;

class CarService implements IndexAppLayerInterface
{
    public $path;
    private $repo;

    public function __construct(SiteRepositoryInterface $carrepo)
    {
        $this->repo = $carrepo;
    }

    public function getData(array $params = [])
    {
        $cars = $this->repo->paginate($params);
        $marks = $cars->loadMissing('mark')->pluck('mark')->unique()->sort();
        $category = Category::with('marks')->where('alias', $params['category_parent'])->first();
        ...

        return [
            'cars' => $cars,
            'marks' => $marks,
            'category' => $category,
            ...
            'years' => $years,
            'min_price' => $priceMin,
            'max_price' => $priceMax,
        ];
    }
}

namespace App\Providers;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(GeneralAppLayerInterface::class, GeneralService::class);

        $this->app->singleton(GeneralService::class, function($app) {
            return new GeneralService(new SettingRepository(), new MenuRepository());
        });
        
        $this->app->when(CarController::class)
            ->needs(GeneralAppLayerInterface::class)->give(function($app) {
            return $app->make(GeneralService::class);
        });

        $this->app->when(CarController::class)
            ->needs(IndexAppLayerInterface::class)->give(function($app) {
            return $app->make(CarService::class);
        });

        $this->app->when(CarService::class)
            ->needs(SiteRepositoryInterface::class)->give(function($app) {
            return $app->make(CarRepository::class);
        });
    }
}

namespace App\Http\Controllers;

class CarController extends Controller
{
    public function __construct(GeneralAppLayerInterface $general, IndexAppLayerInterface $carService)
    {
        $this->general = $general;
        $this->service = $carService;
    }

    public function index()
    {
        return view('site.index')->with($this->general->getData())->with($this->service->getData());
    }
}

Как это сделать правильно? Лишние ли здесь репозитории и сервисы?


Ответы (1 шт):

Автор решения: unreal_serg

Репозитории

Работают только с данными из базы. Однако, я встречал, по меньшей мере, 3 мнения на счет как именно их использовать:

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

  2. Отделение работы с БД от бизнес логики. Но тут, все равно, репозитории используются только для получения данных. Например, у вас есть модель Car, вы создали для нее репозиторий. У вас есть страница с автомобилями, а также есть страница с покупкой запчастей, где есть select, в котором нужно выбрать модель автомобиля. Так вот у вас в репозитории будет, по меньшей мере, 2 метода - getCars() и getCarsForSelectField(). В первом вы сделаете большую выборку с кучей полей, отношений и т.д., во втором, возможно пару полей и все. Такой подход повысит читаемость и организованность кода.

  3. Отделение работы с БД от бизнес логики, но с использованием всех CRUD операций, включая create, update и т.д.

На мой взгляд, репозитории были придуманы для более глобальных целей (вариант 1), но никто вас не ограничивает, если вы сможете с использованием репозиториев придумать более элегантный подход к организации приложений и не выстрелите себе в ногу, то почему бы и нет!

Сервисы

Нужны для бизнес логики. Все задачи решаем в сервисах. Но не забываем про Single Responsibility - разбиваем сервис на более мелкие сервисы под конкретную обособленную задачу, чтобы не было божественных классов.

В зависимости от подхода по репозиториям, в сервисах вы будете использовать:

  1. $data = $this->carsRepository->all(); (Вариант 1)(Как у вас)
  2. $data = $this->carsRepository->getCars(); $data = $this->carsRepository->getCarsForAnotherPage(); (Вариант 2)
  3. $data = $this->carsRepository->getCars(); $result= $this->carsRepository->create($createData); (Вариант 3)
→ Ссылка