Laravel Как уменьшить перенасыщенность контроллеров в Laravel ?

mstdmstd

Новичок
Всем привет!
К требованиям к приложению на Laravel 8 были такие пункты
- the logic of the database migrate to the repositories
- Controllers are oversaturated, it’s not the Laravel way
...
- the logic from the controllers has to be taken to the service
Я понимаю в чем проблема - когда скажем какие-то расчеты выполняются в контроле(скажем на сохранении данных)
и метод контрола становится очень большим. можно ли ссылки с хорошим примерами подобного подхода?

Спасибо!
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
что такое репозитории и как логика может мигриговать?
 

fixxxer

К.О.
Партнер клуба
логика может мигриговать
Бгг. Ну мы все понимаем, о чем они. :)

А вот это - если оставаться на Eloquent - крайне сомнительная затея.
В рамках Eloquent всей этой "логики базы" вообще не должно быть снаружи, он же ActiveRecord.

ActiveRecord-методы дергать снаружи только на aggregate roots, в котором eager load на все релейшены по дефолту. Для выборок скоупы. Сохранять только через push(); чтобы сработало с удалениями, на все релейшены софтделиты. Ну и как бы и всё. Зачем тут репозиторий-то, чтобы туда костылять ручные обходы релейшенов? Лучше этого просто не делать.

При всей моей личной неприязни к ActiveRecord, с таким подходом при качественном code review вполне можно получить достаточно чистый код.
 
Последнее редактирование:

grigori

( ͡° ͜ʖ ͡°)
Команда форума
так, смотрю описание паттерна репозиторий:
another layer of abstraction over the mapping layer where query construction code is concentrated ...
adding this layer helps minimize duplicate query logic
Мы все это делали - класс, в который выносятся часть вызовов, которыми составляются запросы.
Например, когда каталог нормализован в EAV-структуру, все время надо join-ить одни и те же таблицы.
Я это делал когда в mysql не было view.
Как вариант, общую часть множества запросов хочется вынести во view, но менять структуру большой базы не хочется.

Это актуально не для Active Record, а для нормальных read-моделей с выборками через Query Builder. Логично, разобрался.

P.S. главное - не сделать репозиторий для Eloquent AR в трейте :) это уже будет yii way
 
Последнее редактирование:

fixxxer

К.О.
Партнер клуба
что такое логика базы данных
Ты хочешь услышать грамотные формулировки от программистов на ларавеле?)) Понятно, что они имели в виду.
Я об том же, паттерн применим только в паре с датамаппером. "Repository mediates between the domain and data mapping layers". Где там в активрекорде between?
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
ну, логика базы данных как-бы тоже бывает - например, выбор ноды при шардировании
 

WMix

герр M:)ller
Партнер клуба
когда думаю о контроллере представляю такое
PHP:
//тут только уровень http. пишем про _GET, _POST, _SESSION, redirect, 200 0K и тд
public function myAction(Request $request){
    return $this->repository->get(
      $request->query->get('id')
    );
}
под repository представляю такую логику
PHP:
// тут мост между тем что уже читали и то что прочесть придется
public function get($id){
  if(!isset($this->rows[$id])){
    $this->rows[$id] = $this->mapper->get($id);
  }
  return $this->rows[$id];
}
// грубо говоря уникальная запись в базе => уникальный обьект entity на весь вызов
// self::assertSame( $repo->get(42), $repo->get(42));
под mapper такую
PHP:
//конвертор записей в базе в обьекты
public function get($id){
  return call_user_func(
    [$this->entityClass, 'fromArray'],
    $this->crud->read($this->table, $id)
  );
}
 

fixxxer

К.О.
Партнер клуба
для нормальных read-моделей с выборками через Query Builder
Не, с рид моделями это другое, query он и есть query.

Репозиторий - это когда у нас есть domain entities в виде plain objects без зависимостей, а репозиторий абстрагирует этакую "коллекцию", в которую сущность можно записать, удалить, или найти по какому-то ключу. Где оно там хранится на самом деле, в РСУБД, в монге, в редисе или в оперативке - это уже конкретная реализация интерфейса (MysqlUsersRepository implements UsersRepository), снаружи не видно.

Для activerecord абсолютно неактуально, поскольку там абстрагироваться на "внешнем" уровне нельзя. Там только если в самих моделях трейтами, бгг.

Можно сделать вид, что у меня тут дизайн-паттерны, а не эцсамое, написать что-нибудь типа
PHP:
class UserRepository {
    public function findById($id) {
        return User::findOrFail($id);
    }
    public function persist(User $user) {
        $user->push();
    }
    public function delete(User $user) {
        $user->delete();
    }
}
но это какое-то бессмысленное действие. Ну эти чуваки видимо так делают зачем-то))
 
Последнее редактирование:
  • Like
Реакции: AmdY

AmdY

Пью пиво
Команда форума
Я за 15 лет ни разу не встречал реального проекта, где бы использовался Aggregate root. Сам пробовал на фриланс проекте, но мелкий объём не позволял оценить плюсов-минусов.

Репозитории в laravel выглядят бесполезно, кроме связки с пакетом вроде этого https://github.com/andersao/l5-repository, который добавлял парочку полезных костылей - фильтры, критерии, презенторы.

В принципе даже для большого проекта с AR досточно простого правила - не использовать методы AR вне самой модельки. Будь все они protected, проекты с AR было бы легко поддерживать. Но когда в контроллерах налепят where, limit, with, has в сочетании со строковыми идентификаторами таблиц-полей - поддержка превращается в ад.
 

Adelf

Administrator
Команда форума
Давным давно уже написал пару статеек про репозиторий и Eloquent.
Первая о том, что не надо использовать шаблон этот с Eloquent: https://habr.com/ru/post/444688/
Вторая, как можно использовать огрызанный вариант его для рид моделей - https://habr.com/ru/post/445452/

В принципе всё то, что @fixxxer сказал, только с примерами
 

Adelf

Administrator
Команда форума
Но когда в контроллерах налепят where, limit, with, has в сочетании со строковыми идентификаторами таблиц-полей - поддержка превращается в ад.
эй! я на авто-дополнении этих полей в where и has деньги делаю! ) не наезжай! правильный это подход. правильный)
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
подождите, цели репозитория определены как хранилище объектов и уменьшение дублирования логики запросов к базе
Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection.
то есть,
Код:
public function get($id) {
        return User::find($id);
это не репозиторий, а просто адаптер - ни хранения объектов, ни инкапсуляции логики построения запросов,
а статью можно переименовать в "прекращайте называть адаптер репозиторием" )))
 
Последнее редактирование:

Adelf

Administrator
Команда форума
Любому, кто хоть немного задумается, понятно, что пытаться абстрагироваться от хранилища для актив-рекорд объектов - очень странное занятие.
Однако, построить такую абстракцию для доставания их из бд(read часть)- вполне можно. Там можно хотя бы кеширующие декораторы красиво встроить.
 

fixxxer

К.О.
Партнер клуба
это не репозиторий, а просто адаптер - ни хранения объектов, ни инкапсуляции логики построения запросов
Потому что, внезапно, в ActiveRecord функциональность репозитория уже внутри модели. Естественно, получится вырожденная фигня.

кеширующие декораторы
Напрямую через ActiveRecord кэширования нет, а через некий "репозиторий" есть - ух, это ж за этим замучаешься следить, чтобы напрямую не дергали. Самое веселое тут будет даже не с обходом кэша - а с инвалидацией. Или, что, инвалидация сама по себе по eloquent events, хотя кэширование как бы отдельно? Буэ. Короче, в этот момент - когда понадобится кэширование - понимаешь, что с ActiveRecord-ом ты вляпался. Ну наверное можно сделать какой-нибудь трейт типа use Cachable, и молиться, что не будет пересечений с другими трейтами и ларавеловской магией. :)
 

mstdmstd

Новичок
В ссылке fixxxer-а хорошего примера с сервисами я не нашел
Я погуглил и немного почитал

У меня CRUD категорий (+ связывание с данными mailChimp-а) под админкой, вроде :


PHP:
<?php


namespace App\Http\Controllers\Admin;


use App\library\CheckValueType;

use App\Models\Settings;

...

use ImageOptimizer;

use Carbon\Carbon;

use App\Http\Requests\CategoryRequest;


class CategoryController extends Controller

{

    private $mailChimpObject;


    public function __construct()

    {

        parent::__construct();

        $this->mailChimpObject= $this->getMailChimpObject();

    }


    public function index($page = 1)

    {

        $backendItemsPerPage = Settings::getValue('backend_items_per_page', CheckValueType::cvtInteger, 20);


        $filter_name = $this->requestData['filter_name'] ?? '';

        $totalCategoriesCount = Category::count();

        $startRowsFrom = (($page-1)*$backendItemsPerPage);

        $categories = Category

            ::getByName($filter_name)

            ->withCount('adCategories')

            ->orderBy('categories.id', 'asc')

            ->paginate($backendItemsPerPage, null, null, $page)

            ->map(function ($categoryItem) {

                $categoryItemImgProps = Category::readCategoryImageProps($categoryItem->id, $categoryItem->image, true);

                if (!empty($categoryItemImgProps)) {

                    $categoryItem['categoryImgProps'] = $categoryItemImgProps;

                }

                return $categoryItem;

            });


        $prevUrlLinks= getPaginationPrevUrlLinks($startRowsFrom, $backendItemsPerPage, $page);

        $nextUrlLinks= getPaginationNextUrlLinks($totalCategoriesCount, $categories->count(),  $backendItemsPerPage, $page);

        $viewParamsArray = $this->getAppParameters(true, ['csrf_token'], []);

        $viewParamsArray['page'] = $page;

        $viewParamsArray['prevUrlLinks'] = $prevUrlLinks;

        $viewParamsArray['nextUrlLinks'] = $nextUrlLinks;

        $viewParamsArray['categories'] = $categories;

        $viewParamsArray['totalCategoriesCount'] = $totalCategoriesCount;

        return view('admin.categories.index', $viewParamsArray);

    }



    public function store(CategoryRequest $request)

    {

        $categoryUploadFile = $request->file('categoryImage');

        if ( !empty($categoryUploadFile)) {

            $categoryImageBasename    = checkValidImgName($categoryUploadFile->getClientOriginalName(), 50, true);

            $categoryFileSourcePath = $categoryUploadFile->getPathName();

            $this->requestData['image']    = $categoryImageBasename;

        }


        DB::beginTransaction();

        try {

            $category            = Category::create($this->requestData);

            DB::commit();


            if ( ! empty($categoryFileSourcePath)) {

                $categoryDestImageDir =  storage_path() . '/app/public/' . Category::getCategoryDir($category->id);

                $categoryDestImagePath = /* $categoryDestImageDir  */'/public/' .  Category::getCategoryDir($category->id) . urldecode($categoryImageBasename);

                createDir($tmpAdImagesDirs, 0755);

                Storage::disk('local')->put($categoryDestImagePath, File::get($categoryFileSourcePath));

                ImageOptimizer::optimize(storage_path() . '/app/' . $categoryDestImagePath, null);

            }


        } catch (\Exception $e) {

            DB::rollback();

            $this->catchError($e);

        }


        $this->flashMessage('success', 'Category created successfully');

        return redirect('admin/categories/' . $category->id . '/edit');

    }


}

Я создал сервис командой
Bash:
php artisan make:provider AdminCategoryCrudServiceProvider
пытаясь запихнуть в него 1й метод :


PHP:
<?php


namespace App\Providers;


use Illuminate\Support\ServiceProvider;

use App\Models\Category;

use App\Models\Settings;

use App\library\CheckValueType;


class AdminCategoryCrudServiceProvider extends ServiceProvider

{


    public function register()

    {

    }


    public function boot()

    {

    }


    public function getFilteredListing($viewParamsArray, $requestData= [], $page= 1) {

        $backendItemsPerPage = Settings::getValue('backend_items_per_page', CheckValueType::cvtInteger, 20);


        $filter_name = $requestData['filter_name'] ?? '';


        $totalCategoriesCount = Category::count();

        $startRowsFrom = (($page-1)*$backendItemsPerPage); // ;

        $categories = Category

            ::getByName($filter_name)

            ->withCount('adCategories')

            ->orderBy('categories.id', 'asc')

            ->paginate($backendItemsPerPage, null, null, $page)

            ->map(function ($categoryItem) {

                $categoryItemImgProps = Category::readCategoryImageProps($categoryItem->id, $categoryItem->image, true);

                if (!empty($categoryItemImgProps)) {

                    $categoryItem['categoryImgProps'] = $categoryItemImgProps;

                }

                return $categoryItem;

            });


        $prevUrlLinks= getPaginationPrevUrlLinks($startRowsFrom, $backendItemsPerPage, $page);

        $nextUrlLinks= getPaginationNextUrlLinks($totalCategoriesCount, $categories->count(),  $backendItemsPerPage, $page);

        $viewParamsArray['page'] = $page;

        $viewParamsArray['prevUrlLinks'] = $prevUrlLinks;

        $viewParamsArray['nextUrlLinks'] = $nextUrlLinks;

        $viewParamsArray['categories'] = $categories;

        $viewParamsArray['totalCategoriesCount'] = $totalCategoriesCount;

        return $viewParamsArray;

    }

}

И тогда вызов в контроле выглядит как :

PHP:
  public function index($page = 1)

{

    $viewParamsArray = $this->getAppParameters(true, ['csrf_token'], []);

    $this->requestData['filter_name']= 'comp';

    $viewParamsArray= (new AdminCategoryCrudServiceProvider(app()) )->getFilteredListing($viewParamsArray, $this->requestData, $page);

    return view('admin.categories.index', $viewParamsArray);

}
Получается что мой AdminCategoryCrudServiceProvider это хранилище методов С таким же успехом я мог создавть отдельно класс или helper с функциями
Я пытаюсь увидеть тут возможный профит - и никак. Крудовые операции в админе довольно специфичны - куда их еще прикрутить ?
Тем более как сервисы?



Нашел статью https://code.tutsplus.com/ru/tutorials/how-to-register-use-laravel-service-providers--cms-28966
вроде интересно, но с классом DemoOne - опять непонятно - что это за связывание и какой из этого может быть профит ?

Кто понимает - разжуйте, плиз.
 

fixxxer

К.О.
Партнер клуба
В ссылке fixxxer-а хорошего примера с сервисами я не нашел
А знаешь, почему их там нет? Потому что их не существует. Подобные «сервисы» не решают проблему сведения всего к процедурному программированию. Они лишь в определенной степени устраняют возможное дублирование кода в контроллерах, по сути просто выделяя процедуру. Для простых приложений с примитивной бизнес-логикой, которая сводится к банальному crud, этого, может, и достаточно. Но когда она усложняется, и появляется все больше взаимосвязей, прокрустово ложе crud начинает трещать по швам и становится все сложнее поддерживать логику, оперирующую напрямую данными, а не бизнес-глаголами. Инкапсуляция не просто так придумана, без неё логика размывается по всему приложению вне зависимости от того, как ты назовёшь контейнеры процедур. Про aggregate root я ведь не просто так упомянул.

Но для несложных приложений можно и с процедуркой, у неё при всех минусах есть неоспоримое преимущество - она на порядок меньше загружает головной мозг, проектировать особо не требуется. Берёшь код как он был в контроллере, оставляешь в нем только то, что связано напрямую с обработкой http-запроса и формированием ответа, остальное уносишь в метод сервиса.

Чтобы не накосячить прямо сходу, забудь про ассоциативные массивы в php, вот прямо запрети себе ими пользоваться, представь себе, что пишешь на строго типизированном языке.
 
Последнее редактирование:

AmdY

Пью пиво
Команда форума
Всегда поражает умение писать на ровном месте такой сложный код. Даже нормальную пагинацию зачем-то перелапатили, map так же потому что релейшины не освоили?

Почитайте про SOLID, там первая буква про единственной ответственности, вы же просто из одной кучи весь мусор перенесли в другую и даже не отсортировали. Обращаетесь базе, вынесите это отдельно. Формируете пагинатор, создайте отдельный слой, который можно переиспользовать без привязки к модели.
 

mstdmstd

Новичок
Всегда поражает умение писать на ровном месте такой сложный код. Даже нормальную пагинацию зачем-то перелапатили, map так же потому что релейшины не освоили?

Почитайте про SOLID, там первая буква про единственной ответственности, вы же просто из одной кучи весь мусор перенесли в другую и даже не отсортировали. Обращаетесь базе, вынесите это отдельно. Формируете пагинатор, создайте отдельный слой, который можно переиспользовать без привязки к модели.
Насчет
Формируете пагинатор, создайте отдельный слой, который можно переиспользовать без привязки к модели
Нашел пример https://stackoverflow.com/questions/61809982/custom-extend-laravel-pagination-method
Или что-то другое?
 
Сверху