Pull to refresh

Comments 113

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

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


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


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

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

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


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

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

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


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

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

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

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

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

UFO just landed and posted this here
Тестировать модели можно, просто… Немного не просто.

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


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

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

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


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

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

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

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

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


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

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

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

Напишите 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]);
    }
}

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


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

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

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

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


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

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

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

Просто так, как вы написали, с 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 — создался правильно.

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

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


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

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

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

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


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

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


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

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

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

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

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

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


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


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 кучу малозначимых тестов поддерживать будет гораздо труднее.

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


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

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

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

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

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


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

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

Вы недооцениваете хороших тест фреймворков
<?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();
    }
}


А если вдруг случайно beginTransaction перенесут в конец метода — тест сломается?
Вы правы, была ошибка в логике теста, но тут уж не важно используем мы 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();
    }
}

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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


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


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

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


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

Имейте.


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

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

<?php
namespace SomeNamespace\models;

use Illuminate\Database\Eloquent\Model;

class someTable extends Model
{

}


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

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

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


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

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

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


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

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


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

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


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

Именно про 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');

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

Создать схему в БД через 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 можно тоже самое сделать, за то же время.

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


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

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


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

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

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


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


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


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

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




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

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

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


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


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

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

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

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

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

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

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>

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

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


Прототип не только о крудах, это может быть апи(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>

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

Я говорил не про использование 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 из коробки?

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

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


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

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


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

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


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

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


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

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

Время, которое уходит на проектирование domain models, несопоставимо с этой мелочевкой.


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

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

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

Если кому-то было интересно, вот пример.

Нет) У вас только для админок подходит, список заказов пользователя в личном кабинете вывести не получится. А у меня действия будут те же самые.


И текущий функционал различается.
У меня только список, у вас полный CRUD.
У меня произвольная разметка (сворачивающаяся форма), у вас жестко заданная.
Фильтров по нужным полям нет.
Поиск не работает, если ввести "User 1", ничего не находит.
Вывода даты в нужном формате нет.
__toString() с вызовом sprintf() зачем-то в модели появилось, это вообще часть представления.
N+1 не решено, если сделать 15 валют и назначить их 15 первым заказам, на каждую пойдет отдельный запрос в базу.

Challenge accepted =)
Получилось, немного посложнее, чем у вас, но надо сказать, что это уже не прототип, а вполне себе рабочий вариант.
При этом не заметил каких-то ограничений, которые накладывает Doctrine
Спасибо, интересно сравнить.
В Yii это тоже практически рабочий вариант. Прототип он в том смысле, что обычно для продакшена после этих действий нужны всякие мелкие доработки — валидация, интерфейс, дополнительные действия в логике.

$builder->andWhere('o.user = :user')->setParameter('user', $filter->user);
Как-то слишком многословно. Не думал, что у доктрины такой низкоуровневый query builder. Можно же ведь просто
$builder->andWhere(['o.user', '=', $filter->user]).

order.createdAt|date('Y-m-d H:i:s')
order.updatedAt|date('Y-m-d H:i:s')

<th>{{ knp_pagination_sortable(orders, 'Id', 'o.id') }}</th>
<th>{{ knp_pagination_sortable(orders, 'User', 'o.user') }}</th>

Тоже копипаста.

Получилось, немного посложнее
Ну я бы не назвал это «немного») Сколько здесь вручную написанных функций и классов, да еще и магические аннотации. Это ведь все код, который надо поддерживать. И сколько неявного кода надо учитывать, который выполняется «где-то там».

Для интереса проверил автозагрузку для отображения страницы заказов:
Yii: 170 файлов, 2041588 байт
Symfony: 466 файлов, 2689480 байт.

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

Если потом придет задача, к примеру:


  • раскрашивать строку таблицы цветом в зависимости от статуса заказа
  • выводить не таблицей, а списком
  • какие-то даты выводить в формате php:Y-m-d, какие-то в intl:yyyy-MMMM-dd
  • ...

Не проще в таком случае выкинуть виджет и сделать все обычным foreach?
Да, вижу, что у него много опций и, скорее всего, они покрывают все юзкейсы, но в итоге получается все такой же "километровый конфиг".


Ну я бы не назвал это «немного») Сколько здесь вручную написанных функций и классов, да еще и магические аннотации. Это ведь все код, который надо поддерживать. И сколько неявного кода надо учитывать, который выполняется «где-то там».

Не так много:


  • OrderController — и там и там он был частично сгенерирован
  • Сущности — и там и там, только у вас migration-first, у меня наоборот, создаются модели, а миграции генерируются
  • Модель для поиска — и там и там (да, у вас она сгенерировалась, но по факту код был полностью изменен и добавлены те же публичные поля, что и у меня)
  • Форма для поиска — и там и там (только у меня через класс, у вас через шаблон)
  • Логика поиска — у меня в репозитории, у вас в модели (по количеству кода — ± одинаково)

Забавно, что в итоге "кода, который нужно поддерживать"
(git ls-tree -r master --name-only | wc -l)
в Symfony — 63 файлов
в Yii — 173


да еще и магические аннотации

Тоже так думал вначале, но сейчас понимаю, что это очень удобно, тем более что поддержка аннотаций в PhpStorm есть


Пример

За счет того, аннотации находятся рядом с кодом, который они конфигурируют,
получается меньше переключений контекста: не надо переходить в другой файл, чтобы описать то или иное правило.
За счет высокой связанности в Yii 2 такого нет, посмотрим что будет в третьей версии, когда код разобьют на компоненты.

За счет высокой связанности в Yii 2 такого нет

В ActiveRecord схема кодом определяется, поэтому ничего прописывать не надо.


Если потом придет задача, к примеру:

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


у меня наоборот, создаются модели

Вручную. Даже с консольным мастером все равно много всего прописывать надо. И верстка вручную. Я не о том, что в Yii генератор есть, а что c ActiveRecord его проще написать и использовать.


Забавно, что в итоге "кода, который нужно поддерживать"

Ну не) В Yii и тесты и vagrant и заготовка для админки. Я почему и предложил по коммитам смотреть.


Логически-то вы верно написали, плюс-минус одинаково. Но например код в контроллере я вообще не менял, только удалил ненужный. В верстке работал с понятиями задачи "какие поля отобразить и в каком виде", а не с foreach и knp_pagination. Форму с правилами валидации можно и в API использовать, а DateTimeType на верстку заточен.


Я не против Symfony, для большого проекта я бы выбрал ее, просто с ней для некоторых действий нужно больше телодвижений делать.

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

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


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

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


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

Скорость изначальной разработки с AR, конечно, выше.


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


Использование AR само по себе создает технический долг — как и любой паттерн из разряда "срежем углы". Вопрос только в том, понадобится ли его в конкретном проекте выплачивать. :-)

Всё так. В очень многих проектах выплачивать его в этой области не требуется. Именно поэтому AR всё ещё востребован.

ну, отличие elloquent как раз в том, что Вам не обязательно прописывать заранее поля в модели. Приведенная в примере модель — работает. Это очень удобно если Вы сами представляете достаточно туманно, какая структура у Вас должна получиться в итоге.
Добавили десяток полей в таблице — все продолжает работать.В yii для этого бы понадобилось заново генерить модель. И это если вы в нее сверх генератора ничего не добавляли. Так что здесь elloquent удобен. При этом с IDE он работать нормально не умеет, под капотом сплошная магия ради магии, поэтому для того, чтобы это все добро адекватно воспринималось анализатором — Вам придется на PHPdoc текста написать ВТРОЕ больше, чем занимает сама модель. Так что в серьезном проекте я бы обрадовался любой другой ORM, в которой модель хотя бы возвращается через self, а не через билдер со случайным набором методов.
В yii для этого бы понадобилось заново генерить модель

Зачем? Там тоже все продолжает работать.

ide-helper и пара плагинов для шторма сейчас нивелируют практически все проблемы магии ларки (кстати если выставить настройку в конфиге ide-helper он и для моделей все php-доки аттрибутов пропишет). Ну и в Yii2 сейчас для шторма отличные плагины Yii2-support и Yii2-inspections, все php-доки аттрибутов подсказывает и генерит непосредственно из соответствующей таблицы. Накатываешь миграцию, alt + enter — fix missing properties. И всё как надо.
Надеюсь в перспективе их быстро подточат для Yii3… а то без них будет печальненько

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


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


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

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

Основная цель DDD — сделать так, чтобы разработчики (и сам код) говорили на одном языке с бизнесом. Объекты прекрасно подходят для моделирования предметной сущности, а вот с таблицами в РСУБД возникает проблема перевода, причины которой те же, что у Object-relational impedance mismatch.

Егор Бугаенко в своих выступлениях очень часто говорил, что Doctrine — вообще не OOP. Я с ним полностью согласен.
Соглашусь с Вами и добавлю от себя. В 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, и чтобы код частично не протек в сервис/контроллер? А так тут все инкапсулировано и снаружи будет только один вызов метода.

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

Лично мне наличие обращений к БД читать код не мешает. Перенос на другую БД? Бывает крайне редко и все равно потребуется большой рефаторинг.

С точки зрения AR, модель и есть таблица, поэтому логично, что работа с этой таблицей, инкапсулируется в нем же.


С точки зрения doctrine, модель это всего лишь доменный объект, который ничего не знает о хранилище.
Т.е. мы опираемся на объектах, находим их через репозиторий, делаем что то и сохраняем, на этом этапе не важно где храниться наши доменные объекты.
Это не значить, что мы должны абстрагироваться от конкретного БД, внутри репозиторий мы будем использовать что угодно, нет никаких ограничений, можем даже использовать AR.


Как я уже сказал в комментариях, с базой AR нормально тестится, ничего страшного там нет.

Согласен.


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

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


<?php

class Product
{
    public function increaseVolume(ProductRepo $repo): void
    {
        $product = $repo->find($this->id, LockMode::PESSIMISTIC_WRITE);
        $product->volume += self::VOLUME_STEP;
        $repo->save($product);
        $repo->refresh($this);
    }
}

Заголовок спойлера
<?php

/**
 * @method Product|null find($id, $lockMode = null, $lockVersion = null)
 * @method Product|null findOneBy(array $criteria, array $orderBy = null)
 * @method Product[]    findAll()
 * @method Product[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
interface ProductRepo extends BaseRepo, ObjectRepository
{
}

class DefaultProductRepo extends ServiceEntityRepository implements ProductRepo
{
    use BaseRepoTrait;

    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, Product::class);
    }
}

interface BaseRepo
{
    public function save(object $product): void;
    public function refresh(object $product): void;
}

/**
 * @method EntityManager getEntityManager
 */
trait BaseRepoTrait
{
    public function save(object $entity): void
    {
        $manager = $this->getEntityManager();
        $manager->persist($entity);
        $manager->flush($entity);
    }

    public function refresh(object $entity): void
    {
        $this->getEntityManager()->refresh($entity);
    }
}
Сделать то можно, зная про инверсию зависимостей, вот пожалуйста, чисто для примера, код не протек в сервисы, снаружи будет только один вызов, как вы и хотели.


Нет, будет не так как я хотел. Будет еще код по получению объекта репозитория для передачи при вызове `increaseVolume`.
Это решение мне тоже не по душе, оно отражает то что я уже сказал. В Doctrine вынесли подключение к БД наружу, убрали его инкапсуляцию внутри объекта. Теперь чтобы делать подобные вещи, приходится это подключение всегда снаружи передавать, пусть и обернутое в репозиторий.
А так тут все инкапсулировано и снаружи будет только один вызов метода.
Нет, будет не так как я хотел. Будет еще код по получению объекта репозитория для передачи при вызове increaseVolume.

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


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

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


Это легко реализуется, т.к. можно подключиться на событие onLoad, и вызвать конструктор с нужными параметрами, благодаря компайл тайм штукам в symfony, это будет прозрачно для пользование, и производительность не пострадает (рефлекции только в компайл тайм).


Как видите, это не ограничение со стороны doctrine, просто такая штука никому не нужна.
Причина в том, что сущности всё же ближе к структурам, чем к сервисам.
Например, вы может захотите создать сущность, который инитиализируется через конструктор с параметрами, и будете уверены, что хотя бы типы всегда корректны:


<?php

class User
{
    private $address;
    private $profile;

    public function __construct(Address $address, Profile $profile)
    {
        $this->address = $address;
        $this->profile = $profile;
    }
}

В Doctrine вынесли подключение к БД наружу, убрали его инкапсуляцию внутри объекта

Странное у вас определение инкапсуляции.




AR не нарушает инкапсуляцию?
Например, есть юзер, хочу поменять пароль используя метод changePassword, но не хочу чтобы пароль был доступным для чтения, как мне это реализовать в AR?
(В doctrine, я могу не создавать гет/сет методы)

Как видите, это не ограничение со стороны doctrine, просто такая штука никому не нужна.
Причина в том, что сущности всё же ближе к структурам, чем к сервисам.

Так о том и речь. Само разделение на структуры и алгоритмы (на сущности/сервисы) ничего не напоминает?
Странное у вас определение инкапсуляции.

Я не приводил никаких определений. Что странного?

Например, есть юзер, хочу поменять пароль используя метод changePassword, но не хочу чтобы пароль был доступным для чтения, как мне это реализовать в AR?


А где в описании паттерна говорится, что поля AR всегда должны быть публичны? Здесь реализуемо и без public.
Так о том и речь. Само разделение на структуры и алгоритмы (на сущности/сервисы) ничего не напоминает?

В сущности не только структура, но и логика будет.


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


  • В рамках одного типа у вас будет разные зависимости:
    <?php
    /** @var UserFactory $factory */
    $user1 = $factoroy->create(1, $someService1, $someService2);
    $user2 = $factoroy->create(2, $someService1, $someService2);
  • При восстановлении, нужно будет так же внедрить зависимостей.

Это верно не только для сущности, но и для других подобных объектов, AR тут ничем вам не помогут, кроме как глобального интерфейса, которого в 10 строк можете и на doctrine реализовать, но никому такой глобальный интерфейс на самом деле не нужно, причину надеюсь не надо объяснить.


Я не приводил никаких определений. Что странного?

Странно, что считаете нарушением инкапсуляции, то что его не нарушает, совсем:


  1. Внедрение зависимостей через double dispatch, не нарушает инкапсуляцию.
  2. Внедрение деталей хранилища нарушает принцип SRP.

Напомню, AR неплохо так нарушает как SRP, так и инкапсуляцию:


  1. Про SRP и так понятно.
  2. Публичные свойства, и методы базового класса, у которого вывернуты все кишки нарушу, это не нарушение инкапсуляции по вашему?

А где в описании паттерна говорится, что поля AR всегда должны быть публичны? Здесь реализуемо и без public.

Речь была про реализацию в Yii, или мы обсуждаем Row(Table)Gateway?

Речь была про реализацию в Yii, или мы обсуждаем Row(Table)Gateway?


Речь про паттерн AR в общем, а не про какую-то конкретную реализацию.

Внедрение зависимостей через double dispatch, не нарушает инкапсуляцию.


1. Что понимаете под double dispatch? То что понимаю я, вообще тут не при чем.

2. Чтобы не было недопониманий, скажу что я понимаю под инкапсуляцией. Инкапсуляция это в первую очередь сокрытие какой-то функциональности и данных внутри объекта. Сокрытие информации, для того, чтобы вместо того, чтобы каждый раз возвращаться к чему-то конкретному, оперировать более общими/абстрактными понятиями/объектами. Модификаторы доступа public, private, protected это просто синтаксический сахар. Есть множество языков программирования, где можно работать используя объектную парадигму и в которых этого нет. Хотя бы тот же js. Да и в PHP если захотеть можно тоже что угодно получить через Reflection API. Тут все дело не в инструментах, а как с этим работает программист. Так вот, вместо того, чтобы сокрыть знание о сервисе, подключении к БД в объекте, вы предлагаете каждый раз передавать его используя внедрение метода, то есть вы скрываете меньше информации в своем объекте, чем могли бы, а это снижает уровень сокрытия информации, т.е. инкапсуляция становится более слабой.

В сущности не только структура, но и логика будет.


Нет, ну вы же сами написали, что сущности ближе к структурам. Вот это и имеется ввиду, когда говорят о том, что Doctrine это не ООП. А что логику поместить можно это понятно, только вот зависимости для ее работы придется всегда снаружи передавать, либо костылить с onLoad.
Речь про паттерн AR в общем, а не про какую-то конкретную реализацию.

Мы говорили про доктрину и AR, в контексте Yii, вы ранее не сообщали что речь об AR в общем.


Что понимаете под double dispatch? То что понимаю я, вообще тут не при чем.

Упращенно: Получение(внедрение) зависимостей через параметер в рантайме на основе типов.


Инкапсуляция это в первую очередь сокрытие какой-то функциональности и данных внутри объекта.

Именно, и как минимум реализация AR в Yii его нарушает.


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

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


Нет, ну вы же сами написали, что сущности ближе к структурам. Вот это и имеется ввиду, когда говорят о том, что Doctrine это не ООП. А что логику поместить можно это понятно, только вот зависимости для ее работы придется всегда снаружи передавать, либо костылить с onLoad.

Что в DM что в AR(не в контексте Yii) вам всё равно придется разруливать зависимостей вручную.


Возмем простой RowDataGateway, если вы захотите внедрить зависимости к объекту реализующий RowDataGatewafInterface, то вам придеться при каждой загрузки такого объекта внедрить в него зависимостей.
Даже если будете унаследовать его от базового класса, всё равно придеться внедрить зависимости при загрузке, т.к. это будет новым объектом.
Обходный путь, сделать базовый класс со статическым состоянием, но помимо этого состояние, есть ещё проблема с другими зависимостями(как их внедрить?).


Нет, ну вы же сами написали, что сущности ближе к структурам. Вот это и имеется ввиду, когда говорят о том, что Doctrine это не ООП.

Нет, это говорить о том, что:


  • Бизнес объекты не сложные, анемичные модели не приносят проблем.
  • Либо не было договоренности об архитектуры, так часто бывает, причем это не мешает разрабатывать большие проекты, особенно когда всё на микросервисах.

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




А что вы предлагаете взамен?
Инкапсулировать всю работу с БД за интерфесом, и не отражать доменные объекты(или таблицу) в коде?

Инкапсулировать всю работу с БД за интерфесом, и не отражать доменные объекты(или таблицу) в коде?

Да.

Упращенно: Получение(внедрение) зависимостей через параметер в рантайме на основе типов.


А при чем тут тогда двойная диспетчеризация? Кто и что диспетчеризует? То что вы описали это просто передача зависимости в виде аргумента метода.

Программирование это компромиссы, не стоит идти в крайности и пытаться сокрыть вообще всё.


А я и не пытаюсь скрыть все. Я пытаюсь скрыть то, что нужно для упрощения использования объекта и улучшения уровня абстракции.

Гораздо лучше проектировать таких объектов менее связанными, стоит учитывать их жизненный цикл, который вообще то не зависит от DM/AR.


А причем тут жизненный цикл? Зависимости могут быть переданы в конструктор по ссылке или получены из статического локатора сервисов, они от жизненного цикла не зависят.

Именно, и как минимум реализация AR в Yii его нарушает.


И чем же? Тем что там все атрибуты public? Вы до конца мой комментарий дочитали? Я насчет синтаксического сахара уже сказал. В js вообще вон без этого живут.

Возмем простой RowDataGateway, если вы захотите внедрить зависимости к объекту реализующий RowDataGatewafInterface, то вам придеться при каждой загрузки такого объекта внедрить в него зависимостей.


А что в этом плохого?

Бизнес объекты не сложные, анемичные модели не приносят проблем.


Ну так вот она и суть современных практик по использованию доктрины и DataMapper. Сделать простые структуры данных с рудиментарной логикой и обложить процедурами в виде сервисов. Об этом и говорится в первом комментарии.

В общем это два противоположных похода к организации персистентности — инкапсуляция подключения к БД vs маппинг. Мне по душе больше первое, оно мне кажется ближе к ООП. Может вы думаете по-иному, предпочитаете маппинг. И в сообществе я думаю тоже будут противоположные точки зрения на этот вопрос. Я останусь при своем. Я думаю наша дискуссия затянулась, так что если хотите, можете привести свои контраргументы на этот пост и я предлагаю покончить с этим. Праздники заканчиваются).
А при чем тут тогда двойная диспетчеризация? Кто и что диспетчеризует? То что вы описали это просто передача зависимости в виде аргумента метода.

С точки зрения получающего класса никакой разницы, кто и что там диспетчеризует.


И чем же? Тем что там все атрибуты public? Вы до конца мой комментарий дочитали? Я насчет синтаксического сахара уже сказал. В js вообще вон без этого живут

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


А что в этом плохого?

Ничего плохого, просто вы сами не хотели сделать это на DM, через onLoad (или гидрацию).


А я и не пытаюсь скрыть все. Я пытаюсь скрыть то, что нужно для упрощения использования объекта и улучшения уровня абстракции.

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


Ну так вот она и суть современных практик по использованию доктрины и DataMapper. Сделать простые структуры данных с рудиментарной логикой и обложить процедурами в виде сервисов. Об этом и говорится в первом комментарии.

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


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

Если AR не приводить к маппингу, то я бы посмотрел на реализацию (но зачастую, в AR также существует маппинг).


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


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

Полностью согласен.

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

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


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

Во первых, мы нарушаем тот самый принцип единой ответственности

Единственной ответственности (лишь одной для каждого, не общей для всех). Поправьте, пожалуйста.







Господи,

в

какой

бред

превратился

хабр




Попался это пост в топе Гугла по запросу "mock eloquent entity".


Решил тут изолировать модель от приложения в стиле DDD. На уровне приложения всё хорошо: сущности почти настоящие (в конструктор принимают модель, проксируют на неё "скалярные" геттеры, оборачивают в другие сущности связи и собирают VO, дергают её сеттеры в своих мутаторах), репозитории пускай и привязанные жестко к моделям и с доступом через бинд кложуры к приватному свойству сущности, в котором модель. Почти как если бы на Доктрине писал.


Но, блин, покрыть юнит-тестами я это не могу, как только доходит до работы со связями! С обычными атрибутами более-менее решаемо. Чтобы как-то замокать его, убедиться, что я правильно "проксирую", например, attach, семиэтажный мок надо сделать. Или переносить тест в интеграционные или функциональные, чтобы с чистой совестью поднимать базу для тестов.

Entity в ORM — это DTO для репозитория. Зачем вам DTO юнит-тестами покрывать? Там же нет и не должно быть логики.

Даже в мануалах по тестам мокают репу и возвращают готовую энтити.

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

Entity у меня полноценный объект, принимающий в конструктор модель eloquent в репозитории. DTO нет в системе. Мне не нужно тестировать эту модель, мне нужно тестировать, что репозиторий и entity ожидаемо с ней взаимодействуют, дёргают нужные методы eloquent с нужными параметрами. И мне не нужно тестировать взаимодействие eloquent с базой, я доверяю это тестам eloquent. Мне нужны обычные юнит тесты.

принимающий в конструктор модель eloquent в репозитории

Так это же хорошо. Вы спокойно подставляете фикстуру и тестируете логику вашего объекта. Он же не зависит от содержимого внедряймой сущности… Это же логика над ней.

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

Так в том-то и дело, что фикстура семиэтажная получается. eloquent очень любит цепочки вызовов. И не даёт малой кровью проверить результирующий sql.

Решил пока попробовать по другому пути пойти: не прятать под капотом eloquent model, а маппить снаружи модельки на свои сущности через reflection или типа того. Типа Object Eloquent Mapper, а не Object Relational Mapper.

Репозиторий возвращает модельку орм. Моделька мепится сервисом в бизнес-сущность. Над бизнес-сущностью проводятся необходимые операции. Сервис мепит бизнес-сущность в модельку орм, которая сохраняется репозиторием.

Бизнес-сущность ничего не знает о том, как вы храните стейт. Есть служебный слой, который преобразует стейт-объекты в бизнес-сущности. ОРМ выполняет ту функцию, для которой была придумана: позволяет работать с таблицами базы данных как с нативными объектами языка.

Репозиторий возвращает бизнес-сущность. Только репозиторий знает о существовании eloquent вообще (ну и di-контейнер) и как маппить eloquent на бизнес-сущность и обратно в частности. Снаружи это самописная DataMapper ORM, под капотом которой eloquent ActiveRecord ORM

Репозиторий возвращает бизнес-сущность

Вот вы и испытываете последствия этого архитектурного решения. Бизнес-сущности — это не что-то внутренее, что является ядром вашей программы, и то, чем она по сути является (если убрать сайд эффекты), а нечто, что вы получаете извне. Плюс со всем обвесом в виде логики получения и сохранения этой сущности обратно. Это database-centric подход, где таблицы базы данных — это и есть бизнес-сущности. Бизнес-логика сосредоточена на манипуляции и связях между таблицами. Постепенно мигруирует из кода приложения в саму базу данных в виде триггеров и процедур. И по мере усложнения модели данных, ОРМ начинает мешать, так как умеет, по сути, только мепить таблицу в объект, и немного связи (с минимальными оптимизациями). А взаимоотншения между таблицам в таком походе обычно сложнее, чем можем орм.

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

В моём мире бизнес-сущности, вернее их модели, не приходят извне обычно. Извне (от пользователя или программных агентов) приходят команды на их создание, мутацию или удаление. Ну и запросы на просмотр/поиск, но это отдельный разговор. Ну и связи (ограничения foreign key) )между таблицами всё чаще рвутся во время переезда частей функциональности в микросервисы.


Ну вот наивная попытка скрестить DDD и Eloquent провалилась из-за сложности юнит тестирования. Чуть ли не весь Eloquent надо мокать. Перешёл ко второму подходу, где слой бизнес-модели — ядро приложения не только семантически, но и технически.

Sign up to leave a comment.

Articles