Обновить
30
Пётр Грибанов@ghost404

Symfony professional developer

19
Подписчики
Отправить сообщение

@Fesor Спасибо за конкретный пример. Я уже в общем понял что вы имели в виду, но за еще один пример спасибо. Я уже понял что обще признанные решения вы считаете недопустимыми.


Чем-то вы мне напоминаете нашего общего знакомого G-M-A-X. Вместо того чтоб использовать готовое решение делаем свое. Согласен, если мы разрабатываем ПО опираясь на парадигму Domain Driven Design, то готовые решения просто невозможно использовать. Я же предпочитаю опираться на Data Driven Design чтоб не усложнять себе жизнь и повышать уровень реиспользования кода. Да и не было у меня пока еще проектов с супер сложной бизнес логикой.


Сейчас решил углубится в тему DDD и был бы рад обмену опытом. Вы не думали написать статью на тему использования DDD в Symfony?


Размышления на тему


Как на счет сериализации сущностей для всяких API?


  • Условно, мы создаем сервис сериалайзер
  • Наследуемся от JsonSerializableNormalizer
  • В соответствии с форматом определяем формат нормализации объекта
  • Непосредственно нормализацию наверное выполняем все таки в сервисе, а не в сущности
  • На каждый формат для сущности я бы делал свой сервис чтоб не захламлять сериалайзер
  • А вот с денормализацией вопрос (эта задача хоть и не частая, но все равно задача)

Исходя из вашей логики денормализацией должна заниматься сущность.
Условно, пришел запрос от пользователя с id сущности и набором полей.


  • получаем сущность из бд
  • передаем сущность и данные в сериалайзер
  • сериалайзер передает данные в сущность
  • сущность заполняет свои поля на основе данных

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


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


Получаем минимум 4 дополнительных метода в сущности.
Так же не понятно как мы должны заполнять связи сущности при денормализации. Видимо туда же нужно передавать сериалайзер.


Мысль другая


Как идея. Разбить проект на 4 бандла и 4 окружения:


  • Frontend — веб часть проекта
  • Backend — админка
  • API — внешние сервисы
  • Core — для общего набора функций (не очень хорошая практика, но иногда иначе никак)

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


В таком случае фронтенд и api можно писать по DDD, а админку делать с тупым CRUD, сеттерами и на SonataAdminBundle. Это конечно если бизнес логика нам важна именно на внешнем интерфейсе, а не в админке, а так по идее и должно быть ибо админка все таки для администраторов.


Из минусов мы получаем дублирование некоторых функций (решается наследованием или трейтами)
Из плюсов нам не нужно писать админку с нуля под каждый проект отвечающую нашим бизнес задачам.

тут интереса ради заглянул под капот FOSUserBundle


при изменении сущности, по событию от Doctrine, выполняется обновление пароля
https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Doctrine/UserListener.php#L97


при обновлении берется plain password (он не хранится в бд), хешируется и сохраняется как основной пароль через setPassword
https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Model/UserManager.php#L195


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

Вклинюсь в обсуждение. На тему index.php, это не что ино как шаблон проектирования Front Controller.


@aktuba советую не увликатся холиваром. Макс не адекватен и осознавать этого не хочет. Не ты первый пытаешся направить его на путь истинный

Интересный у вас взгляд на реиспользование кода и оптимизацию процесса разработки.
Доктрина слишком умная для сонаты — давайте напишем свою доктирину, но попроще.
Соната слишком универсальная для дактрины — давайте напишем свою сонату, но отвечающую нашим требованиям.
Слушайте, а симфони для вас не слишком универчальная? Может стоит написать свой фраймворк? Хотя что-то мне вспоминается что вы так и сделали.


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

Выглядит неплохо, но у этого подхода есть недостатки:


  • Как я уже говорил, этот подход полностью не совместим с SonataAdminBundle.
    Это означает необходимости написания своей админки с нуля.
    А это уже повлечет за собой дополнительные расходы ресурсов компании на что будет готова пойти далеко не каждая, даже крупная, компания.
  • Этот подход не позволяет использовать оригинальные сущности в формах.
    Это означает создание новых сущностей, почти полных клонов оригинальной сущности, для использования их в формах и последующей конвертации в оригинальны сущности доктирины.
    В этот подход неплохо укладывается Command Bus, но в результате мы плодим пустые сущности, дублирование кода и оверхед.
  • Отказ от стандартных компонентов усложняет проект и повышает цену сопровождения кода.

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

можно конечно и в контроллере все сделать


$user->setHashedPassowrd($this->hasher->hash($password);

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

Может я чего-то не понимаю, но вроде говорили про то, что сделать выбрасывания события на $user->setPassword(), обработчик которого будет хэшировать пароль и вызывать $user->setHashedPassowrd();

Это конечно вариант, но в этом случае метод setPassword() должен в зависимостях иметь Event Dispatcher. И это действительно получится ненужный оверхед. Я же имел в виду выбрасывание событие из контроллера.


$this->dispatcher->dispatch(
    StoreUserEvents::CHANGE_PASSWORD,
    new ChangeUserPassword($user, $password)
);

а в обработчике уже хешировать


public function onChangeUserPassword(ChangeUserPassword $event)
{
    $event->getUser()->setHashedPassowrd($this->hasher->hash($event->getPassword());
}
А захешированный пароль нужен самой сущности.

Да сущности захешированый пароль тоже не нужен. Ей нужен пароль. А хешировать этот самый пароль нужно кому-то другому. Например Symfony Security


Как минимум зачем вводить оверхид да ещё с циклическими зависимостями, если можно сделать простой вызов функции?

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


А ещё события могут логироваться, а там plaintext.

Запросы тоже могут логироваться, а там plaintext.

Пример можно? А то, кажется, про разные вещи говорим

Телепрограмма. Если нам нужно вытащить следующее/предыдущее событие то можно использовать sql запрос с выборкой по дате, а можно использовать напрямую связанное следующее/предыдущее событие. Второй вариант будет быстрее при выборке

гуглить доменные ивенты

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


Почему мы не можем рассматривать пароль в сущности как всегда хешированный пароль (Просто value object)? Например бросать событие об изменении пароля с сущностью и plaintext паролем, а обработчик хеширует пароль и устанавливает его в сущность. Чем это отличается от загрузки файлов и установки в сущность пути к файлу?

Аналогично, метод approveAs(User $editor)

а чем это отличается от setApprovedUser(User $editor)? семантикой?


это ответственность представления

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


Агрегаты не просто привязывает, а единственный кто их может создавать

А что если цепочка сущностей это целая таблица в которой 2кк записей? Агригатор тут не справится. Нужно как-то иначе связывать события.

Данные должны обрабатываться там, где для этого достаточно данных.

Вот об этом я и говорю. Функции changePassword в вашей первой реализации не достаточно данных для того чтобы изменить пароль. Она вынуждена использовать зависимости для хеширования пароля.


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


Приведу пример:
Есть 2 сущности с картинками:


  • обложка альбома
  • новость с картинкой на сайте группы.

Загружаемые картинки попадают во временную папку /upload/. После привязки к сущности они должны перемещаться каждая в свою папку:


  • обложка альбома — /image/album/{date}/cover/
  • новость с картинкой — /image/news/{date}/cover/

{date} это Y/m от даты создания сущности


Кто должен заниматься перемещением картинок? Напоминаю что только сущность знает путь загрузки картинки относительно корня проекта, но она не знает путь к корню проекта.


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


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


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

В таком случае сущность превращается в помойку.


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

Не многова ли зависимостей и ответственности у сущности? Может стоит делегировать часть задач?

Интересный взгляд на сеттеры. Спасибо. Хотя с вашим решением не соглашусь.


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

ни кто не мешает в коде проекта не хешировать пароль


$user->changePassword('123', function($password) {
    return $password;
});

или использовать разные алгоритмы хеширования


$user->changePassword('123', function($password) {
    return password_hash($password, PASSWORD_DEFAULT);
});
$user->changePassword('123', 'md5');

Конечно за такое надо отрывать руки, но речь не об этом.
Лучше использовать классический сеттер и при сохранении сущности хэшировать пароль


function setPassword(string $password) : User
{
    $this->password = $password;
    $this->password_changed = true;

    return $this;
}

function isPasswordChanged() : bool
{
    return $this->password_changed;
}

и обработчик события


if ($user->isPasswordChanged()) {
    // изменяем через сеттер или напрямую пишем в свойство
    $user->setPassword($this->hasher->hash($user->getPassword()));
}

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


Ну и отказ от классических сеттеров может вызвать определённые проблемы в использовании стандартных пакетов.
Например SonataAdminBundle использует PropertyAccessor для изменения полей сущности.
То есть отказ от классических сеттеров потребует написания костылей для SonataAdminBundle или полный отказ от этого бандла и самостоятельное написание аналога с блекджеком и ...


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

Маленький офтоп.
Правильно ли я понимаю что автор уже живет в Германии? Какая там сейчас ситуация с мигрантами, да и вообще в стране?


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

Окей. А теперь представим стандартную сетуацию. Изменился phpunit.xml.dist. У всех пользователей он обновится и тесты будут отрабатывать по новому, а у тех кто использует phpunit.xml, тесты будут работать по старому. А по скольку файл phpunit.xml.dist меняется редко, то это изменение может пройти очень незаметно если явно не сказать всем разработчикам. Я конечно утрирую, но такой кейс не стоит исключать.


Ну и для облегчения запуска из консоли со своими флагами, не проще ли написать bash скрипт? Да и вводить команду в консоли целиком не обязательно. Ctrl+R ни кто не отменял. Команду на code coverage я никогда не ввожу сам

Я честно вообще не понял какой смысл копировать phpunit.xml и запихивать в gitignore. Какой смысл делать свой, эксклюзивный конфиг для тестов которые должны у всех отрабатывать одинаково?

и еще. В PHPStorm с установленным плагином Symfony можно указать в начале шаблона комментарий


{# @controller BloggerBlogBundle:Page:contact #}

и тогда в шаблоне автокомплит будет видеть все параметры передаваемые контроллером и по щелчку Ctrl+Mouse Left Click мы перейдем в контроллер, а при аналогичном щелчке по ссылке на шаблон перейдем в сам шаблон


return $this->render('BloggerBlogBundle:Page:contact.html.twig', array(
    'form' => $form->createView()
));
Несколько замечаний:
  • Symfony best practices рекомендует использовать аннотации для описания роутов.
  • Валидаторы сущности лучше тоже делать через аннотации. По крайней мере это наглядней.
  • Хоть это и не описано в best practices, но если вы протестируете свой проект в SensioLabsInsight, то выясните что все формы должны находится в директории src/Blogger/BlogBundle/Form/Type/.
  • Параметры приложения принято описывать в файле parameters.yml, а не config.yml.
  • Конфиги бандла принято подглючать через DI extension, а не через app/config/config.yml.

Symfony2 автозагрузчик будет искать необходимые файлы в директории src


В Symfony нет автозагрузчика. Symfony использует для автозагрузки Composer.
Он использует расширение .txt.twig. Первой частью расширения, .txt определяется формат файла для генерации. Общие форматы включают, .txt, .html, .css, .js, XML и .json. В последней части расширения определяет, какой движок шаблона использовать, в данном случае Twig. Расширение .php использовало бы PHP для отображения шаблона.


Расширение .html.twig это только рекомендации к наименованию расширения. Расширение файлов ни как не влияет на работу шаблонизаторов. Можно указать любое расширение, хоть .foo.bar.

Вместо
$this->container->getParameter('blogger_blog.emails.contact_email')


можно использовать getParameter
$this->getParameter('blogger_blog.emails.contact_email')


Вместо
$this->get('session')->getFlashBag()->add('blogger-notice', 'Your contact enquiry was successfully sent. Thank you!');


можно использовать addFlash
$this->addFlash('blogger-notice', 'Your contact enquiry was successfully sent. Thank you!');


Несколько мелких замечаний по оформлению:
  • В сущностях рекомендуется описывать реальные типы, а не писать везде mixed.
  • В сущностях рекомендуется указывать значение по умолчанию. Например email всегда должен быть строкой и не когда не должен становится null-ом.
  • В setter-ах рекомендуется возвращать ссылку на текущий объект $this чтобы можно было использовать цепочки вызовов.
  • В классе формы рекомендуется использовать цепочку вызовов для конфигурирования формы.
  • В форме метод configureOptions не обязательный и если он пустой, то лучше его вообще не указывать.


PS: В целом статья хорошая. Спасибо за проделанную работу. Хотя лучше писать качественный код чтобы новички не перенимали плохое.
PSS: Не сочтите за рекламу. Несколько полезных сервисов для тестирования проекта:

по оформлению форм в шаблоне, правильней всего так:


{{ form_start(form, {action: path('BloggerBlogBundle_contact'), attr: {class: 'blogger'}}) }}
{{ form_widget(form) }}
<button type="submit">Submit</button>
{{ form_end(form) }}

Информация

В рейтинге
Не участвует
Откуда
Россия
Зарегистрирован
Активность