image


Всем привет. Хочу перед вами исповедаться и рассказать немного о том, что я чувствую, когда разрабатываю на Laravel. Нет, не подумайте, я обожаю этот фреймворк и безумно благодарен команде, которая его создала и поддерживает, они делают крайне крутое дело и, на мой взгляд, Laravel является лучшим продолжением не менее горячо любимого мною Symfony.


Я обожаю тупой код. Тупой в том смысле, что даже через 10 лет, если заказчик попросит тебя внести изменения в него, ты сможешь сделать это не вникая во всю логику, даже будучи после пятничного корпоратива, ничего в старом коде не сломав. И тупой в том смысле, что не нужно прикладывать никаких когнитивных усилий, чтобы его понять. Но есть в Laravel Eloquent ORM одно архитектурное решение, которое заставляет меня плакать. Интересно? Заходите под кат.


Умные люди уже давным давно все придумали за нас: OOP, Паттерны проектирования, SOLID, DDD и другие страшные словечки, которые так пугают по-началу, а потом применяются по наитию.


Этим мне нравится Laravel и Symfony. Они позволяют писать максимально тупой и защищенный код прямо из коробки. Да. У каждого из них есть свои недостатки… Но в Laravel есть один, который меня раздражает больше всего. Это использование Active Record Pattern (AR) для работы с моделями.


Для начала — немного об этом паттерне. Что это вообще такое? Для понимания, следует пойти к родителю этого опуса проектирования приложений — паттерну Repository. Этот паттерн представляет собой коллекцию. Коллекцию сущностей (Entity), которые может извлекать, изменять, сохранять, удалять, в общем, управлять ими в абстрактном месте хранения. В 90 из 100 процентах случаев, таким местом хранения являются различные базы данных. Но может быть и файловая система, и какой-то кэш, и даже внешнее API.


Такой подход полностью соответствует принципу единой ответственности и подходу DDD. Кроме того, благодаря этому подходу, реализуется слабая связанность — нам не важно, каким именно образом в приложении хранится что-либо, мы работаем с Entity, когда хотим работать непосредственно с объектным представлением данных и работаем с Repository, когда нам нужно взаимодействовать с хранилищем.


Но в Laravel решили пойти по пути использования AR, что бесспорно круто и невероятно удобно, когда тебе нужно сделать быстрый прототип, но становится невероятной головной болью, когда нужно взаимодействовать с несколькими источниками данных и оперировать с ними в рамках системы.


AR — паттерн, который сопоставляет Entity и Repository в одну Model. То есть объект становится представлением конкретной записи в базе данных. И… Что? К чему это приводит и почему это так раздражает?


Во первых, мы нарушаем тот самый принцип единой ответственности — логика работы с хранилищем в одном месте, а логика работы с сущностью в другой. Это важно, ведь в рамках моей системы я не хочу передавать по цепочке вызовов строчку из бд в объектном представлении. Я хочу передавать модель. Мне должно быть наплевать, как она получается, изменяется и сохраняется. Мне нужно иметь те методы, которые позволяют взаимодействовать только с моделью, а не со строками в БД.


Во вторых, мы не можем (как следствие того, что Persistent слой — слой хранения — связан со слоем сущности), простым образом изменить место хранения модели. Да, мы можем сделать это в конфигурации, изменив это сразу для всех, в рамках поддерживаемых баз данных. Или изменить только для конкретной модели (при всем при этом, мы не убираем никакие методы query builder'а, ведь нельзя избавиться от методов базового класса) и напороться на тонну вероятных ошибок в коде или, не дай Бог, если его кто-то другой поддерживать будет (а такое случается сплошь и рядом).


В третьих. Мне хочется тестировать свои сущности. Мне хочется, черт возьми, быть уверенным, что изменения, которые я вношу не сломают мою бизнес-логику. А, как показывает практика, в случае с AR ты этого сделать не можешь, потому что нарушен чертов принцип единой ответственности! Хотя тут я немного лукавлю. Тестировать модели можно, просто… Немного не просто.


Тем не менее, нельзя не сказать о плюсах этого паттерна. Хотя весь его плюс в том, что это "быстро, просто, не задумываясь". Сливая два близких по логике своего действия и постоянно использующихся вместе паттерна, мы получили удобное средство, которое немного сокращает объем кода (в сторону усложнения, мы же помним о "тупом коде"?). Это также, позволяет избавиться от лишних проблем на этапе формирования MVP, который обязательно (практика показывает, что такое случается редко, но все же) планируется переписать. Это позволяет сместить мысли с вопроса "как мы делаем" на вопрос "что мы делаем", то есть избавиться от лишних вопросов о технологиях и перейти к бизнес-логике.


К какому же выводу я пришел за несколько лет использования Laravel Eloquent ORM? Active Record зло во плоти? Да нет, это крутейший инструмент для некоторых ситуаций, особенно для этапа, когда пишется простенькое приложение или прототип такого приложения. Но это невозможная для работы вещь, когда твое приложение вырастает и тебе приходится работать с большим количеством источников данных, писать код со 100% покрытием тестами, начинаются большие проблемы.


Да, придумываются новые фишки (Trucker?), да идем на ухищрения. Но все же хочется немного больше свободы от фреймворка, тем более, что он так многим хорош!

Комментарии (75)


  1. Samouvazhektra
    02.01.2019 15:58

    Мыши плакали, кололись, но продолжали жрать кактус? Кто мешает прикрутить доктрину и использовать ее? Eloquent к Ларке гвоздями не приколочен. И зачем пытаться писать код со 100% покрытием, кроме как ради цифрочки? Используйте инструменты по назначению, и будет вам счастье.


    1. be_a_dancer Автор
      02.01.2019 16:09

      Никто не мешает. Об этом и пост.
      Проблема (ну или не проблема, а подводный камень) в том, что ларавель из коробки работает с Eloquent. И не заточен под доктрину, хотя никто не запрещает прикрутить ее.
      Фишка в том, что при этом придется забыть о генерации моделей (entity+repository) командой (естественно, с этой болезнью можно справиться). Придется подзабыть про нативную авторизацию из коробки (которая реально привязана к моделька юзера хотя и не намертво), да и про некоторые библиотеки.


      То есть запрещать то нам, разработчикам, никто не станет (собственно, многие так и делают, как ты сказал). Но лишние сложности привнесет.


      И еще одно. Этот пост имеет еще один подтекст. Когда приложение вырастает из стадии MVP, далеко не всегда (признаемся, почти никогда) бизнес идет на полное переписывание или хотя бы серьезный рефакторинг, который в этот момент требуется. Вот и получается. Нам, мышкам, приходится колоться, плакать, но все равно кушать кактус в виде AR.


      1. Samouvazhektra
        02.01.2019 16:30
        +1

        приходится колоться, плакать, но все равно кушать кактус в виде AR.

        Ну так учитесь доходчиво объяснять потребности в рефакторинге. Научитесь его готовить чтоб не сильно кололся. Не делать божественных моделей, выносить доступ в файндеры и сервисы, чтоб не размазывать по всему проекту. Генераторы всего что нужно пишутся за пару-тройку часов, как и скаффолд для авторизации… если вы конечно не по сайту в неделю клепаете (Тогда либо забить и продолжать клепать, либо сменить место работы на менее негроидное)
        Описываемая вами проблема — не eloquent и laravel, а паттерна ActiveRecord который жертвует канонами в угоду скорости. Тут, как говорится, либо шашечки, либо ехать.


        В конце концов это Open source — не нравится, форкните и сделайте лучше. А лучи ненависти распускать по такому поводу… не по-программистски. Хотя за вас уже постарались https://www.laraveldoctrine.org/


        1. be_a_dancer Автор
          02.01.2019 16:42

          Повторюсь, об этом и пост — надо всегда помнить об этом паттерне и писать код так, чтобы минимизировать проблемы при рефакторинге. Я упоминал, что ar неплох для быстрой разработки.
          А насчет того, кто виноват — никто. Это не проблема, а подводный камень, который следует иметь в виду. И сразу учитывать при проектировании. Это одна из тех жертв, на которые надо идти осознанно.


          1. Samouvazhektra
            02.01.2019 16:49
            +1

            Ну вот только статья называется про ненависть к eloquent и похожа на нытьё, которое мало кому чем поможет и мало что объяснит. Если рассчитана на джунов, то покажите как правильно.


            писать код так, чтобы минимизировать проблемы при рефакторинге

            Дык вот не понимают они и не умеют так. Они как в мануале написано, так и делают. А так весь эффект как от фразы "Надо быть хорошими мальчиками и девочками, и не делать плохо"


            1. be_a_dancer Автор
              02.01.2019 16:54

              Пожалуй ты прав. И с названием, и с тем, что надо чуть более явно выразить пути решения. Буду чуть более внимательным в дальнейшем. Наверное, стоит сделать материал о том, "как правильно" с высоты своей колокольни.
              Спасибо за критику)


              1. Samouvazhektra
                02.01.2019 16:55
                +2

                Я права :-)


      1. gerashenko
        03.01.2019 12:25

        А разве доктрина сама все не генерирует? И сущности и репозитории и базу? По крайней мере в Symfony это генерируется.


        1. index0h
          03.01.2019 12:52

          Все зависит от настроек, на лонг ране на много проще явно определять и энтити и репозитории.


      1. OnYourLips
        03.01.2019 15:03

        что ларавель из коробки работает с Eloquent
        И с доктриной тоже, всего лишь через один composer require.

        Лично для меня Eloquent — это пример того, каким не должен быть код. Это не просто неудобный, плохо спроектированный и непродуманный инструмент, но и потенциальная мина, которая обязательно сработает, когда код будет разрастаться. И будет срабатывать постоянно по мере роста проекта, увеличивая стоимость и уменьшая скорость этого роста.


  1. index0h
    02.01.2019 16:13

    Тестировать модели можно, просто… Немного не просто.

    Пфф, всего-то коннекшн замокать и докинуть пару десятков часов в ETA на задачу))


    Это также, позволяет избавиться от лишних проблем на этапе формирования MVP, который обязательно (практика показывает, что такое случается редко, но все же) планируется переписать

    Увы, это утверждение правдиво только для людей, слабо знакомых с Entity-Repository.


    1. be_a_dancer Автор
      02.01.2019 16:18

      Да. Всего-то. Пара десятков часов?) А потом менеджер приходит и спрашивает, почему такие неоправданно высокие расходы по времени?


      Да. Согласен. Это утверждение, да и пост в целом, рассчитаны на джунов и разработчиков не сильно хорошо разбирающихся в этом. Пожалуй, миддл или сеньор сам знает, как организовать код и какую ORM взять исходя из требований проекта.


      1. index0h
        02.01.2019 16:28

        А потом менеджер приходит и спрашивает, почему такие неоправданно высокие расходы по времени?

        Отличный повод еще и софт-скилы прокачать))


  1. michael_vostrikov
    02.01.2019 17:22

    Это важно, ведь в рамках моей системы я не хочу передавать по цепочке вызовов строчку из бд в объектном представлении. Я хочу передавать модель. Мне должно быть наплевать, как она получается, изменяется и сохраняется.

    При использовании AR вы можете точно так же передавать ее по цепочке вызовов и не задумываться, как она получается, изменяется и сохраняется.


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

    Расскажите подробнее, в чем проблема с тестированием бизнес-логики?


    1. index0h
      02.01.2019 18:19

      Расскажите подробнее, в чем проблема с тестированием бизнес-логики?

      Напишите unit тест


      public function store(Request $request)
      {
          // Проверка запроса...
      
          DB::beginTransaction();
      
          $myModel = new MyModel;
      
          $myModel->name = $request->name;
      
          try {
              $myModel->save();
      
              DB::commit();
          } catch (\Throwable $exception) {
              DB::rollBack();
      
              $this->logger->error($exception->getMessage(), ['exception' => $exception]);
          }
      }


      1. Samouvazhektra
        02.01.2019 19:27
        +1

        брррр, ну и причём тут элокент, если вы бизнес-логику в контроллере пишете


        https://medium.com/@DonnaInsolita/the-evolution-of-php-developer-4d3c2fdfa1ae


        1. index0h
          02.01.2019 20:09

          Вы ожидали, что я пол проекта сброшу в комментарии?)) Это простой пример использования. Не вопрос, меняем $request на string $name и называем это сервисом MyModelSaver, вот вам и БЛ. Unit тесты от этого не сильно поменяются.


          Хотя, я так подумал, а почему вдруг этот же метод не может быть в сервисе?)) Есть некий DTO My\Vendor\Dto\Billing\Request со свойством name. Есть сервис, управляющий созданием и сохранением MyModel. Не вижу проблем.


      1. michael_vostrikov
        02.01.2019 19:44

        А доктрина вам как тут поможет? Я к тому, что так же как вы пишете логику с доктриной, можно писать и с AR, с поправкой на то, где находится метод save().


        1. index0h
          02.01.2019 20:15

          А доктрина вам как тут поможет?

          Я могу замокать entityManager, в который буду делать persist нового инстанса MyModel.


          Я к тому, что так же как вы пишете логику с доктриной, можно писать и с AR, с поправкой на то, где находится метод save().

          Мы сейчас про тесты этой логики говорим))


          1. michael_vostrikov
            02.01.2019 20:22

            Я мало работал с доктриной. Покажите, как будет выглядеть ваш пример с ней, где там будет логика и как вы ее будете тестировать. Просто так, как вы написали, с AR обычно не делают, хотя бы потому что это куча копипасты.


            1. index0h
              02.01.2019 20:35

              Просто так, как вы написали, с AR обычно не делают, хотя бы потому что это куча копипасты.

              C AR не используют транзакции? Не используется логгирование? Модели AR не создаются в том же методе, где и сохраняются? Что не используется?)) Или вы просто доколупались до источника данных Request, так же как комментатор выше?


              Покажите, как будет выглядеть ваш пример с ней

              public function store(Request $request)
              {
                  // Проверка запроса...
              
                  $this->entityManager->beginTransaction();
              
                  $myModel = new MyModel;
              
                  $myModel->setName($request->name);
              
                  try {
                      $this->entityManager->persist($myModel);
                      $this->entityManager->flush();
              
                      $this->entityManager->commit();
                  } catch (\Throwable $exception) {
                      $this->entityManager->rollback();
              
                      $this->logger->error($exception->getMessage(), ['exception' => $exception]);
                  }
              }

              Что бы покрыть этот класс потребуется 2 теста, как минимум:


              1. Позитивный, в котором entityManager->rollback и logger->error не вызываются.
              2. Негативный, в в котором бросается исключение, например методом commit, транзакция при этом откатывается и логгируется исключение. которое мы бросили.

              Я создам мок от EntityManagerInterface, укажу какие методы будут вызываться, на этапе persist — проверю, что объект $myModel — создался правильно.


              1. michael_vostrikov
                02.01.2019 20:53

                C AR не используют транзакции? Не используется логгирование?

                Используются, но не копипастить же это все в каждый метод. Исключения можно в одном месте ловить например. И логировать их там же.


                Я создам мок от EntityManagerInterface, укажу какие методы будут вызываться

                В Laravel, насколько я знаю, тоже можно замокать beginTransaction() и все остальное. Я думал, под логикой вы подразумеваете, что у вас работа с моделью ($myModel->setName() и т.п.) находится отдельно от транзакций и всего остального, и вы ее хотите тестировать. А так не вижу отличий от примера с AR.


                1. index0h
                  03.01.2019 11:59

                  Используются, но не копипастить же это все в каждый метод.

                  DRY — это круто, но далеко не всегда дает профит. Например: у вас есть две сущности userType1 и userType2, они похожи, но принадлежат разным доменам. Для них код стоит копипастить.


                  Исключения можно в одном месте ловить например. И логировать их там же.

                  Вообще говоря это полностью зависит от задачи.


                  Я думал, под логикой вы подразумеваете, что у вас работа с моделью ($myModel->setName() и т.п.) находится отдельно от транзакций и всего остального, и вы ее хотите тестировать.

                  Т.е. по вашему сохранение данных в транзакции не может быть логикой?


                  1. michael_vostrikov
                    03.01.2019 13:29

                    В бизнес-логику транзакции обычно не входят.


                    1. index0h
                      03.01.2019 14:29

                      Обычно входят, пусть и не явно. Это происходит в момент, когда ваша система без транзакций может прийти в не консистентное состояние.


                      1. michael_vostrikov
                        03.01.2019 15:06

                        Транзакции в этом случае находятся отдельно и в юнит-тестах бизнес-логики не участвуют. Если надо, работа транзакций проверяется на интеграционных тестах с реальной БД. Бизнес-логика должна быть отвязана от хранилища, и ее юнит-тесты, соответственно, тоже. Про то, о чем вы говорите, я нигде не встречал информации.


              1. Samouvazhektra
                02.01.2019 21:15

                Тестировать нужно логику, а не сохранение в базу.


                Если у вас сложная логика заполнения моделей, делаете сервис/метод сервиса для заполнения


                class AwesomeMyModelService
                {
                      public function __construct (LoggerInterface $logger, MyModel $model){
                         $this->model = $model;
                         $this->logger = $logger;
                     }
                
                     public function instance(array $data = []) MyModel
                   {
                       return $this->model->newModelInstance($data);
                   }
                
                   public function complexFill(array $data, $otherData, $modelInstance):MyModel
                  {
                      //Todo - fill model
                      return $modelInstance;
                   }
                
                public function save(MyModel $modelInstance):MyModel
                {
                     $model->save();
                     return $model;
                }
                
                public function saveTransactional(MyModel $modelInstance):MyModel
                {
                     $modelInstance->getConnection()->beginTransaction();
                     try{
                            $modelInstance = $this->save($modelInstance);
                           $modelInstance->getConnection()->commit();
                            return $modelInstance;
                     }catch(\Throwable $e){
                         $modelInstance->getConnection()->rollback();
                         $this->logger->...
                         //etc..
                         throw $e;
                     }
                }
                
                }

                И всё будет мокаться. И может переиспользоваться как в админском контроллере, так и в апи контроллере, в job, в консольной команде


                И не гонитесь за 100% coverage кучу малозначимых тестов поддерживать будет гораздо труднее.


                1. index0h
                  03.01.2019 11:48

                  Далеко не всегда стоит разбивать методы, так как вы это делаете. Это вопрос безопасности и сложности интерфейса вашего сервиса. Вот что делает ваш сервис?
                  Он и реализует фабрику для MyModel, и сохраняет без транзакции, и гидрирует данные (по хорошему еще и отдельный метод для валидации нужен), и сохраняет с транзакцией с транзакцией.


                  Это все очень круто, если его сделать: final + запретить редактировать. Но, в реальности ваш сервис будет источником проблем. Я не спорю, для мелких проектов такое годится. Но не для чего-то среднего, или большого.


                  1. Samouvazhektra
                    03.01.2019 14:31

                    мой сервис ничего не делает, это просто от руки абстрактно накиданный пример — конкретная реализация завязана на потребности доменной логики


                    1. index0h
                      03.01.2019 14:39

                      Да не вопрос. Если вы не используете подходы, описанные в вашем примере — это отлично. Если же используете — ну что ж, печально.


                1. index0h
                  03.01.2019 12:48

                  Тестировать нужно логику, а не сохранение в базу.

                  Вы где-то увидели "INSERT" в примере, что я привел? Или может я написал, что нужен функциональный тест с дерганием живой БД?


                  И не гонитесь за 100% coverage кучу малозначимых тестов поддерживать будет гораздо труднее.

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


      1. rustacean137
        03.01.2019 08:19

        Вы недооцениваете хороших тест фреймворков
        <?php declare(strict_types=1);
        
        namespace App\Tests;
        
        use Psr\Log\LoggerInterface;
        use Mockery as m;
        use App\MyModel;
        use App\DB;
        use App\SomeService;
        use App\Request;
        use PHPUnit\Framework\TestCase;
        
        class SomeServiceTest extends TestCase
        {
            private $logger;
            private $someService;
        
            protected function setUp(): void
            {
                m::globalHelpers();
                $this->logger = mock(LoggerInterface::class);
                $this->someService = new SomeService($this->logger);
            }
        
            /**
             * @param bool $commitShouldFail
             * @dataProvider storeDataProvider
             */
            public function testStore(bool $commitShouldFail): void
            {
                $db = mock(\sprintf('alias:%s', DB::class));
                $db->shouldReceive('beginTransaction')->once();
                $request = new Request();
                $requestName = 'ada1b238-2703-4064-b2ed-7f6ef2f4a44b';
                $request->name = $requestName;
                $myModel = mock(\sprintf('overload:%s', MyModel::class));
                $myModel->shouldReceive('__set')
                        ->with('name', $requestName)
                        ->andSet('name', $requestName)
                        ->once();
        
                $myModel->shouldReceive('save')->once();
        
                if ($commitShouldFail) {
                    $exceptionMessage = 'b687f5d7-6347-4b35-8fa7-ec8bbe73fdd8';
                    $db->shouldReceive('commit')
                       ->once()
                       ->andThrow(\Exception::class, $exceptionMessage);
                    $db->shouldReceive('rollback')->once();
                    $this->logger
                        ->shouldReceive('error')
                        ->with($exceptionMessage, m::any())
                        ->once();
                } else {
                    $db->shouldReceive('commit')->once();
                }
        
                $this->someService->store($request);
            }
        
            public function storeDataProvider(): array
            {
                return [
                    [true],
                    [false],
                ];
            }
        
            protected function tearDown(): void
            {
                $this->addToAssertionCount(
                    m::getContainer()->mockery_getExpectationCount()
                );
        
                m::resetContainer();
            }
        }
        


        1. quantum
          03.01.2019 11:22

          А если вдруг случайно beginTransaction перенесут в конец метода — тест сломается?


          1. rustacean137
            03.01.2019 13:22

            Вы правы, была ошибка в логике теста, но тут уж не важно используем мы AR или DM.

            Обновленный тест
            <?php declare(strict_types=1);
            
            namespace App\Tests;
            
            use Psr\Log\LoggerInterface;
            use Mockery as m;
            use App\MyModel;
            use App\DB;
            use App\SomeService;
            use App\Request;
            use PHPUnit\Framework\TestCase;
            
            class SomeServiceTest extends TestCase
            {
                private $logger;
                private $someService;
            
                protected function setUp(): void
                {
                    m::globalHelpers();
                    $this->logger = mock(LoggerInterface::class);
                    $this->someService = new SomeService($this->logger);
                }
            
                /**
                 * @param bool $commitShouldFail
                 * @dataProvider storeDataProvider
                 */
                public function testStore(bool $commitShouldFail): void
                {
                    $db = mock(\sprintf('alias:%s', DB::class));
                    $db->shouldReceive('beginTransaction')->ordered(1)->once();
                    $request = new Request();
                    $requestName = 'ada1b238-2703-4064-b2ed-7f6ef2f4a44b';
                    $request->name = $requestName;
                    $myModel = mock(\sprintf('overload:%s', MyModel::class));
                    $myModel->shouldReceive('__set')
                            ->with('name', $requestName)
                            ->andSet('name', $requestName)
                            ->ordered(2)
                            ->once();
            
                    $myModel->shouldReceive('save')->ordered(3)->once();
            
                    if ($commitShouldFail) {
                        $exceptionMessage = 'b687f5d7-6347-4b35-8fa7-ec8bbe73fdd8';
                        $db->shouldReceive('commit')
                           ->ordered(4)
                           ->once()
                           ->andThrow(\Exception::class, $exceptionMessage);
                        $db->shouldReceive('rollback')->ordered(5)->once();
                        $this->logger
                            ->shouldReceive('error')
                            ->with($exceptionMessage, m::any())
                            ->ordered(6)
                            ->once();
                    } else {
                        $db->shouldReceive('commit')->ordered(4)->once();
                    }
            
                    $this->someService->store($request);
                }
            
                public function storeDataProvider(): array
                {
                    return [
                        [true],
                        [false],
                    ];
                }
            
                protected function tearDown(): void
                {
                    $this->addToAssertionCount(
                        m::getContainer()->mockery_getExpectationCount()
                    );
            
                    m::resetContainer();
                }
            }
            


            1. quantum
              04.01.2019 12:56

              Да, неважно AR или DM. Но я тут немного про другое.

              Если внимательно посмотреть на этот тест, то что мы увидим?
              — В методе должна быть строка beginTransaction, причем первая
              — Второй строкой должно идти setName
              — Третьей — save и тд

              Т.е. по сути этот тест +- эквивалентен сравнению кода метода с каким-то эталонным кодом. Лично у меня это вызывает батхерт:) и я не пишу такие тесты, ограничиваясь функциональными (с разворачиванием бд, оборачиванием теста в транзакцию и тд, как вы написали ниже)


        1. index0h
          03.01.2019 12:11

          Что будет, если до вашего тест кейса выполнится другой, в котором подтянется реальный класс App\DB? Или в ваш же тест кейс дописать такой тест метод.


          1. rustacean137
            03.01.2019 13:28

            Я так понимаю, вы о функциональных тестах(а где ещё может понадобиться реальный класс DB?).

            Нужно лишь сделать отдельную конфигурацию для юнит тестов, либо можете запустить тесты параллельно.


            1. index0h
              03.01.2019 14:14

              Не обязательно. Результат вашего теста может на прямую зависеть от других тестов. Это очень плохая практика. Поднятие всего окружения для каждого теста (когда их много) — очень ресурсоёмкая задача.


              1. rustacean137
                03.01.2019 17:09

                Результат вашего теста может на прямую зависеть от других тестов

                По своей природы, юнит тесты атомарные, и не зависят от ничего, для них оправдано держать отдельный конфигурационный файл.
                А то что тесты не проходят, когда кто то пытается использовать реальную БД в юнит тестах — прекрасно, так даже лучше.
                Вам довелось видеть среди юнит тестов, запрос к внешней АПИ, которому место в интеграционных тестах?


                Это очень плохая практика. Поднятие всего окружения для каждого теста (когда их много) — очень ресурсоёмкая задача.

                Поднять всего окружения не обязательно.
                Достаточно один раз поднять БД из фикстур, сделать стаб для DB, и обернуть тестов в rollback-only транзакцию.
                Это как раз становиться актуальным, когда у вас очень много тестов.


              1. Rukis
                04.01.2019 11:04

                Я обязательно напишу материал про то, как лично я считаю правильным организовывать и писать приложения на ларе.

                Отлично, это интересно! Интересно было бы увидеть ваш подход и с eloquent и с доктриной.

                Но по этой статье, мне, к сожалению, не стало понятнее в чем зло AR и в чем преимущества доктрины. Есть ощущение некоторой надуманности и натянутости аргументов, было бы круто увидеть реальный пример, когда, всё-таки AR вызовет фатальные проблемы и не за счет кривого применения?
                Предположим, что мы аккуратны с моделями, не позволяем логике расползаться по неожиданным местам и уверены, что не придется менять хранилище на то, которое не поддерживается eloquent.


  1. DaleMartinWatson
    02.01.2019 18:44
    +3

    Автор прав, по сути, но при этом статья ни о чем. Нет описания проблемы, нет примеров, нет решения или даже какой-то идеи, как последствия наличия проблемы минимизировать или устранить полностью, нет даже внятного заключения. Похоже на коммент к очередной обнове фрейворка, но прилично разбавленный водой: «Элок'уэнт плох, потому, что я так сказал, хочется свободы». Что, простите?


    1. be_a_dancer Автор
      02.01.2019 19:50
      +1

      Да. Я уже понял свою ошибку и критику воспринял. Посмотрите мои ответы под первым комментарием.
      Я обязательно напишу материал про то, как лично я считаю правильным организовывать и писать приложения на ларе.


  1. symbix
    03.01.2019 02:12

    Удивлен, что до сих пор в комментариях нет ссылки на DataMapper, основанный на коде Eloquent — Analogue ORM. Оставлю ее здесь.


    1. Samouvazhektra
      03.01.2019 02:41

      Он сейчас немного заброшенным выглядит.


  1. SamDark
    03.01.2019 03:22

    Во первых, мы нарушаем тот самый принцип единой ответственности — логика работы с хранилищем в одном месте, а логика работы с сущностью в другой.

    Нет. SRP AR сам по себе не нарушает потому что он не про то, что класс должен делать что-то одно. Он про то, что для изменений класса должна быть только одна причина.


    См., например, https://softwareengineering.stackexchange.com/questions/228672/doesn-t-active-record-violate-srp-and-ocp


    Это важно, ведь в рамках моей системы я не хочу передавать по цепочке вызовов строчку из бд в объектном представлении. Я хочу передавать модель. Мне должно быть наплевать, как она получается, изменяется и сохраняется.

    Ну и наплюйте. Eloquent за вас её сохраняет и получает.


    Мне нужно иметь те методы, которые позволяют взаимодействовать только с моделью, а не со строками в БД.

    Имейте.


    У AR есть минусы, но часть аргументации в статье формально некорректна.


  1. DemianFrai
    03.01.2019 08:19

    Elloquent — отличный вариант для того, чтобы писать быстро. Быстро свести вмести модельки и посмотреть, как они взаимодействуют. Быстро накидать домашний проектик — на это реально уходит меньше времени. Но, к сожалению, продвигается он именно как «серьезная ORM для крупных проектов». И вот в этом качестве он начинает раздражать неимоверно. Потому что он позволяет делать грязь. Вот такую:

    <?php
    namespace SomeNamespace\models;
    
    use Illuminate\Database\Eloquent\Model;
    
    class someTable extends Model
    {
    
    }
    


    Эмоции, которые испытываешь, натыкаясь на такие шедевры в проекте с овердохрена моделей сложно выразить цензурными словами.


    1. rustacean137
      03.01.2019 14:11

      Быстро свести вмести модельки и посмотреть, как они взаимодействуют.

      На doctrine это проще делается, вы можете проектировать в любой IDE для БД, или в коде.
      Приведите пожалуйста пример, какую структуру удобно было бы проектировать на Eloquent.


      1. DemianFrai
        03.01.2019 15:08

        Приведите пожалуйста пример, какую структуру удобно было бы проектировать на Eloquent.


        Любой прототип. Когда вся структура занимает 2-3 десятка файлов, охватывающих только необходимый функционал и вам нужно на грязную прописать взаимодействие таблиц — все прекрасно. Понадобилось новое поле? Закиньте его в миграцию и забудьте.

        Именно по этой же причине данная ORM ужасна в продакшене.

        На doctrine это проще делается, вы можете проектировать в любой IDE для БД, или в коде.


        И? В моем комментарии про doctrine нет ни слова. Он о том, где лучше использовать elloquent, где его не стоит использовать и где я его чаще вижу. Холивары в стиле «doctrine vs elloquent», «android vs iphone» и «js vs asm» меня не особо интересуют.


        1. rustacean137
          03.01.2019 17:23

          И? В моем комментарии про doctrine нет ни слова. Он о том, где лучше использовать elloquent, где его не стоит использовать и где я его чаще вижу.

          Именно про doctrine ни слова, но:


          Быстро накидать домашний проектик — на это реально уходит меньше времени.

          Т.е. уходить меньше времени чем аналогичных инструментов, так ведь?
          Мой коммент о том, что аналоги, вполне позволяют писать в т.ч. прототипы за то же время что и Eloquent.


          Если имели ввиду что то другое, тогда не вижу смысла продолжать, значить неправильно вас понял.


      1. michael_vostrikov
        03.01.2019 16:01
        +2

        Именно про Eloquent не знаю, на Yii с AR удобен такой кейс. Создать схему в БД через GUI клиент, проставить внешние ключи, генератором сгенерировать модели со всеми связями, вывести в грид с заменой user_id на user.name, с автоматической пагинацией и произвольной сортировкой, и возможностью сложных фильтров и обработкой N+1 (везде названия связей, а не таблиц)


        Order::find()->joinWith('tariff t')
            ->where(['=', 't.type_id', $this->tariff_type_id])
            ->with('user');

        Для прототипирования и средних проектов неплохо подходит, для больших да, могут быть проблемы.


        1. rustacean137
          03.01.2019 17:42

          Создать схему в БД через GUI клиент, проставить внешние ключи, генератором сгенерировать модели со всеми связями

          Doctrine умеет тоже самое.


          вывести в грид с заменой user_id на user.name, с автоматической пагинацией и произвольной сортировкой, и возможностью сложных фильтров и обработкой N+1 (везде названия связей, а не таблиц)

          Тоже самое, только в виде библиотеки, которых нужно подключить отдельно.


          В общем, для прототипов symofny/doctrine подходить не хуже фреймворков с AR, особенно с появлением symfony/flex.
          Вот тут можно глянуть на компоненты для RAD разработки http://rad.knplabs.com/




          P/s не спорю с тем что Laravel/Yii/etc подходить для "чего угодно", просто хотел прояснить, что в symfony/doctrine можно тоже самое сделать, за то же время.


          1. michael_vostrikov
            03.01.2019 18:14
            +1

            Да не совсем за то же время.


            How to use it?
            In a yaml routing file, it could look like this

            Пока только конфиг напишешь, больше времени уйдет.


            Если у вас есть время, можем проверить на каком-нибудь примере. С коммитами на каждый шаг, чтобы можно было проверить объем кода.


            1. rustacean137
              03.01.2019 20:18

              Пока только конфиг напишешь, больше времени уйдет.

              Конфиг можно генерировать из схемы, и потом модели из конфига, doctrine-cli это умеет из коробки.


              Да и не очень понятно, как из этого следует что разработка идёт медленнее, даже если их писать вручную, на это не уйдет много времени, чтобы это существенно влияло в целом.


              Мне кажется время больше зависит от того, сколько опыта у разработчика с инструментом(и опыт вообще), вон на Java пишут куча бойлерплейт кода, и быстро, IDE прекрасно поддерживает doctrine, в т.ч. схему его конфига.


              Если у вас есть время, можем проверить на каком-нибудь примере. С коммитами на каждый шаг, чтобы можно было проверить объем кода.

              К сожалению у меня нет ни времени, ни желания на это.
              К тому же, возможно, у нас с вами разная скорость разработки, не зависимо от инструментов.




              Понятно дело, сужу исходя своего опыта, на AR правда приходиться мало, примерно год.


              1. michael_vostrikov
                03.01.2019 21:24

                Да и не очень понятно, как из этого следует что разработка идёт медленнее, даже если их писать вручную, на это не уйдет много времени, чтобы это существенно влияло в целом.

                Так а на что у вас остальное время уходит тогда? Построение БД занимает одинаковое время и там и там. Генерация моделей со связями ну пусть тоже одной командой. Дальше генерируем CRUD-контроллер с представлениями и все, это уже рабочая заглушка. Убираем ненужные экшены из контроллера, добавляем обработку фильтров и правила валидации в класс формы, настраиваем рендеринг грида. В итоге получаем полностью кастомизируемое решение, интерфейс и роутинг можно менять как угодно.


                В результате получаются файлы:
                — контроллер с методом actionIndex()
                — представление index.php с конфигом грида
                — форма с полями и 2 методами для правил и фильтров (без пагинации и сортировки вручную, но с возможностью настроить)
                — модель, где надо разве что поправить связи


                То есть объем изменяемого вручную кода сопоставим с размером конфига Knp Rad Resource Resolver. Который к тому же не PHP код, а отдельная магия. В Symfony количество файлов и методов будет побольше.


                1. Samouvazhektra
                  03.01.2019 23:46

                  Ну какбы очень много проектов выходят за рамки CRUD-концепции и гридов


                  1. michael_vostrikov
                    04.01.2019 10:01

                    Я не сказал, что это только CRUD и гриды. В примере у меня только вывод сущностей с фильтрами, это много где используется, даже в API. Грид можно на список заменить, для него тоже компонент есть.


                  1. SamDark
                    04.01.2019 13:16

                    Очень мало по сравнению с теми, что не выходят.


                    1. Samouvazhektra
                      04.01.2019 15:06
                      +1

                      ну так штука в том, для тех кто не выходят — AR должно хватать за глаза и профита от доктрины никакого,


                      1. SamDark
                        04.01.2019 17:26

                        Именно так. А для тех, что выходят, может хватить, например, Query Builder-а и репозиториев. Ну, может, в довесок какого-нибудь гидратора.


                1. rustacean137
                  03.01.2019 23:58

                  symfony тоже позволяет нечто подобное:


                  curl -sS https://get.symfony.com/cli/installer | bash
                  symfony new --full my_project; cd my_project
                  bin/console make:user
                  bin/console make:registration-form
                  bin/console make:auth
                  bin/console make:entity
                  bin/console make:crud
                  bin/console doctrine:database:create
                  bin/console make:migration
                  bin/console doctrine:migrations:migrate --no-interaction
                  # ...

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


                  Так а на что у вас остальное время уходит тогда? Построение БД занимает одинаковое время и там и там. Генерация моделей со связями ну пусть тоже одной командой.

                  Прототип не только о крудах, это может быть апи(REST/GraphQL) на apiplatform, и фронтенд SPA/мобильное приложение, вот на них и уходить остальное время.


                  То есть объем изменяемого вручную кода сопоставим с размером конфига Knp Rad Resource Resolver.

                  Выше показал, что для круда конфиг не придется писать, это если без грида, с гридом — да, придется библиотеки подключить, настраивать, что может занять больше времени, чем в Yii.


                  Knp Rad Resource Resolver. Который к тому же не PHP код, а отдельная магия. В Symfony количество файлов и методов будет побольше.

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


                  + этот конфиг стандартный для symfony/dependency-injection, т.е. его можно писать хот на php, а информацию о конфиге легко получить стандартными средствами: bin/console config:dump-reference <bundleName>


                  1. michael_vostrikov
                    04.01.2019 10:14

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

                    Так не нужен голый круд или админка. Я же написал — результат полностью кастомизируемый. Почему-то всегда начинают приводить в пример компоненты для админок, где разметка прибита гвоздями и надо написать километровый конфиг чтобы оно заработало.


                    Прототип не только о крудах, это может быть апи(REST/GraphQL) на apiplatform, и фронтенд SPA/мобильное приложение, вот на них и уходить остальное время.

                    А REST API это типа не CRUD) Конкретно вот здесь описано то, что я описал в примере — создание модели, валидации, фильтров, и некоторых операций с ней, только без Web UI. Поддержка REST API в Yii встроенная (в новых версиях вроде вынесли отдельно), занимает несколько файлов, и думаю простота использования AR здесь сыграла не последнюю роль.


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

                    Я говорил не про использование YAML как такового, а про то, что чтобы задать нужную логику, надо писать специальные магические константы, которые где-то там в движке обрабатываются и типа "оно само" работает. А если в одном разделе надо разметку "чуть-чуть поменять", то начинаются танцы с бубном.


                    symfony тоже позволяет нечто подобное

                    Попробовал. Вписывать ответы вручную гораздо дольше, чем проектировать схему БД через GUI-клиент. Вот из таких мелочей и получается быстрее.


                    index.html.twig
                    <th>Id</th>
                    <th>Email</th>

                    Ни сортировки, ни страниц, надо руками вписывать. Ах да, библиотеки. Но тогда ваш пример это не "нечто подобное".


                    1. rustacean137
                      04.01.2019 18:42

                      Я говорил не про использование YAML как такового, а про то, что чтобы задать нужную логику, надо писать специальные магические константы, которые где-то там в движке обрабатываются и типа "оно само" работает. А если в одном разделе надо разметку "чуть-чуть поменять", то начинаются танцы с бубном.

                      Это надуманные проблемы, и Yii/AR не спасет вас от этого.
                      У вас config.php из кастомного DSL.
                      Вы же настраиваете, как минимум роуты, и они по вашей логике тоже магия.
                      Eсли бы у вас было немного опыта на symfony, вы бы поняли как это работает, легко нашли бы место где это обрабатывается.


                      Так не нужен голый круд или админка. Я же написал — результат полностью кастомизируемый. Почему-то всегда начинают приводить в пример компоненты для админок, где разметка прибита гвоздями и надо написать километровый конфиг чтобы оно заработало.

                      Мне кажется вы неправильно поняли, вы читали "если нужен грид или админка", и сделали вывод "для грида нужно использовать админку".


                      А REST API это типа не CRUD)

                      Очевидно АПИ это не только круд, да и есть ещё много чего, где то понадобиться WS, поиск, интеграция с внешними АПИ, или что то ещё в зависимости от предметной области проекта.


                      Поддержка REST API в Yii встроенная (в новых версиях вроде вынесли отдельно), занимает несколько файлов, и думаю простота использования AR здесь сыграла не последнюю роль.

                      Разве что AR легче было реализовать, т.к. DM требует больше абстракции в реализации, но при использовании никакой разницы в скорости нет.


                      Попробовал. Вписывать ответы вручную гораздо дольше, чем проектировать схему БД через GUI-клиент. Вот из таких мелочей и получается быстрее.

                      Это немного субъективно, на самом деле необязательно генерировать из консоли, просто вы так любите генерировать, так я и показал что так тоже можно.
                      Проще самому создавать модели, и там же всё настраивать через аннотации, поддержка IDE прекрасная, а скорость приходить с опытом.
                      Поймите, если я буду создавать AR модели, то для меня это больше когнитивная нагрузка, просто потому что я не работаю с этой штукой каждый день.
                      К тому же, никто вам не запрещает проектировать в каком нибудь GUI (datagrip/mysql workbench), и генерировать всё остальное одной командой.


                      Ни сортировки, ни страниц, надо руками вписывать. Ах да, библиотеки. Но тогда ваш пример это не "нечто подобное".

                      Ваши претензии к тому что datagrid не идёт в комплекте с фреймворком?




                      В итоге, получается удобство AR для прототипов, это наличие datagrid в Yii из коробки?


                      1. michael_vostrikov
                        04.01.2019 20:55

                        Это надуманные проблемы, и Yii/AR не спасет вас от этого.

                        Здесь я говорю конкретно про отображение страницы. Когда я в грид передаю конфиг с магическими константами, меня это не интересует, потому что я контролирую окружающую разметку и могу поместить его в любое место на странице. И еще пару произвольных форм добавить. Данные для них я тоже передаю сам. В вашем же примере вид страницы контролируется конфигом, и чтобы его поменять, надо копировать template, править там разметку, подстраиваться под то как туда движок параметры передает и т.д. и т.п.


                        Мне кажется вы неправильно поняли, вы читали "если нужен грид или админка"

                        Так причем тут админка-то?) Я говорю о прототипе любой страницы, где отображается информация из БД — список сущностей либо свойства сущности. Как бы да, я имел в виду в первую очередь web UI, но это необязательно. У меня в примере к разметке относится только файл index.php. Собственно, REST API работает через тот же ActiveDataProvider, который в грид передается.


                        Проще самому создавать модели, и там же всё настраивать через аннотации

                        Так о том и речь, что это все надо руками писать.


                        Ваши претензии к тому что datagrid не идёт в комплекте с фреймворком?

                        Мои претензии к тому, что ваш пример не полный. Если он будет давать тот же функционал, ручного кода там будет гораздо больше. Тут неважно, есть ли что-то из коробки или нет.


                        В итоге, получается удобство AR для прототипов, это наличие datagrid в Yii из коробки?

                        Нет. Удобство в ее использовании в стороннем коде — в логике или компонентах для UI. Получается меньше кода и логических сущностей (функций, классов). Но менее гибко, да.


          1. SamDark
            03.01.2019 19:20
            +1

            За то же время не выйдет. За прошлый год плотно поработал с доктриной. Мощная штука, в большинстве случаев с ней приятно (хоть и не без WTF), но даже нормально разобравшись не получается той же скорости, что выходит для прототипирования с Yii AR.


            1. rustacean137
              03.01.2019 20:31

              Интересно, какие особенности doctrine мешали вам разрабатывать с той же скорости?


              Не могло влиять, перевес опыта в одну сторону?


              1. SamDark
                03.01.2019 21:51

                Не думаю, что это опыт, хотя не полностью исключаю. Я наблюдал это не только на себе. Сложность конфигурации (более строгий маппинг надо настраивать, фич больше, при добавлении или удалении столбцов нужны дополнительные телодвижения). Менее интуитивный query builder. Не совсем тривиальная работа с batch-ами и особенно с "упавшим" entity manager.


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


  1. GreedyIvan
    03.01.2019 11:10

    Не нужно мешать DDD и Database-centric подход.


    В DDD модель ничего не знает ни о каких БД. Это для неё внешний источник, откуда брать инфу и куда её грузить. Через репозиторий. Если удобно, то где-то в репозитории используется AR, чтобы упростить взаимнодействие с базой.


    Если же используется database-centric, то бизнес модель — это таблица в базе данных. И там принципы DDD не применимы от слова совсем.


    1. quantum
      03.01.2019 11:24

      Прям-таки совсем?


      1. GreedyIvan
        03.01.2019 11:32

        В DDD доменый уровень является самым нижним и ни от чего не зависит. В database-centric доменый уровень рамазан по таблицам и коду с бизнес-логикой их изменения. Круглое в квадратное не вставляется.


        1. SamDark
          03.01.2019 19:22
          +3

          image


          1. GreedyIvan
            04.01.2019 11:27

            Квадратура круга, раз уж мы о взаимозаменяемости говорим.


    1. Anton_Zh
      05.01.2019 08:19

      Вы говорите про использование ООП в качестве инструмента реализации модели домена. Но также можно модель домена реализовать с помощью PL/SQL и таблиц. Просто исторически так сложилось, что модели реализуют именно с помощью ООП. Модель в первую очередь делается в головах/на доске/бумаге и с помощью UML или других языков моделирования (ну или в специализированных программах), реализация должна быть близкой к модели, чтобы легче было понять где и что. Может иногда даже получится использовать код в качестве документации модели, код настолько понятен и прост (хотя я не видел такого, лучше уж фото рисунков на доске на телефон). Так вот тут и кроется основная цель DDD — получение реализации, облегчающей понимание модели и внесение изменений. Ни о какой изоляции от БД речи изначально не шло, это возникло как следствие желания упрощения реализации модели.


  1. webdevium
    04.01.2019 21:27

    Егор Бугаенко в своих выступлениях очень часто говорил, что Doctrine — вообще не OOP. Я с ним полностью согласен.


    1. Anton_Zh
      05.01.2019 06:04

      Соглашусь с Вами и добавлю от себя. В Doctrine в основу положено сознательное нарушение инкапсуляции. А это один из основополагающих принципов ООП. Вместо изоляции обращений к БД в классах AR, подключение к БД вынесено наружу. Сделано это ради того, чтобы код было легче читать и тестировать, переносить код на другие БД.
      Как я уже сказал в комментариях, с базой AR нормально тестится, ничего страшного там нет. Лично мне наличие обращений к БД читать код не мешает. Перенос на другую БД? Бывает крайне редко и все равно потребуется большой рефаторинг. В общем с этим дело вкуса.
      Но есть еще одно большое НО. Имея инкапсулированное подключение мы можем управлять транзакциями и блокировками изнутри AR, соответственно внутри сущностей Doctrine мы этого делать не можем. Например:

      class Product extends ActiveRecord
      {
      
           public function increaseVolume()
           {
                $this->db->transactional(function() {
                    //это для блокировки, для того, чтобы другие процессы не перетерли счетчик, пока мы его не подифицируем
                    $this->db->query('SELECT id FROM table WHERE id=:id FOR UPDATE', [':id' => $this->id])->scalar();
                    $this->refresh();
                    $this->volume = volume + self::VOLUME_STEP;
                    $this->save();
                });
           }
      }
      
      

      Код показан чисто как пример, конечно для инкремента есть более эффективное решение. Как сделать подобное в Doctrine, и чтобы код частично не протек в сервис/контроллер? А так тут все инкапсулировано и снаружи будет только один вызов метода.


  1. Anton_Zh
    05.01.2019 05:40

    AR нормально тестится вместе с БД, ну пусть чуть медленее. В чем тут проблема?