Создание блога на Symfony 2.8 lts [ Часть 3 ]

  • Tutorial





Обзор



Doctrine 2 представляет собой хороший пример механизма объектно-реляционного отображения (ORM) для PHP 5.3+, позволяющий работать с базой данных максимально прозрачно, где в качестве промежуточного слоя используются обычные объекты PHP. В качестве основы используется весьма мощный слой абстракции от базы данных (DBAL). Основная задача ORM — связать две концепции: объекты PHP и записи в реляционной базе данных. Одна из ключевых особенностей Doctrine — возможность написания запросов на собственном объектно-ориентированном языке, чем-то напоминающим SQL, называемым Doctrine Query Language (DQL). Помимо небольших отличий от SQL, он позволяет значительно усилить степень абстракции между объектами и строками базы данных, что позволяет создавать мощные и гибкие запросы, при этом сохраняя целостность.

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

Проект на Github

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

Doctrine 2: Модель


Чтобы наш блог функционировал нам нужно подумать о том, как мы будем хранить данные. Doctrine 2 предоставляет библиотеку ОRМ, предназначенную именно для этой цели. Она позволяет использовать различные СУБД включая MySQL, PostgreSQL и SQLite. Мы будем использовать MySQL, но ее можно легко заменить другой СУБД.
Заметка

Если вы не знакомы с ORM мы раскроем базовые принципы. Определение от Wikipedia гласит:
ORM (англ. Object-Relational Mapping, рус. объектно-реляционное отображение) — технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных».

ОRМ облегчает перевод данных из реляционной базы данных, такие как MySQL в PHP объекты, которыми мы можем манипулировать. Это позволяет нам инкапсулировать нужную функциональность, которая нам необходима в таблице, через класс. Представьте таблицу пользователя, она вероятно имеет поля: username, password, first_name, last_name, email и job. С помощью ОRМ это становится классом с свойствами username, password, first_name и т.д., что позволяет нам вызывать такие методы, как getUsername() и setPassword(). ORM пошло гораздо дальше, оно также позволяет получить связанные таблицы, в то время как мы извлекаем объект пользователя или позже. Теперь предположим, что наш пользователь имеет несколько друзей, связанных с ним. Это будет таблица друзей, с первичным ключом для таблицы пользователя. Теперь с помощью ORM мы можем вызвать метод $user->getFriends() чтобы вернуть объекты из таблицы друзей. ORM также занимается сохранением данных, так что мы можем создавать объекты в PHP, вызывать метод save() и ORM займется сохранением данных в базу. Так как мы используем Doctrine 2 ORM библиотеку, вы гораздо лучше узнаете о её возможностях по ходу прохождения руководства.


Примечание

В то время как, это руководство будет использовать библиотеку Doctrine 2 ORM, вы можете выбрать библиотеку Doctrine 2 ODM. Существует несколько вариаций этой библиотеки, включая реализации для MongoDB и CouchDB. Смотрите страницу Doctrine Projects для получения дополнительной информации.
Так же вы можете посмотреть эту статью, которая объясняет, как установить ODM с Symfony 2.


База данных



Создание Базы данных

Если вы проходите руководство с первой части, у вас уже должны быть прописаны параметры базы данных. Если вы пропустили первую часть, то обновите значения database_ * в файле параметров, расположенного в app/config/parameters.yml

Создадим базу данных с помощью команды Doctrine 2. Эта команда лишь создаёт базу данных и не создаёт никаких таблиц в базе. Если база данных с таким же именем уже существует будет выведено сообщение об ошибке, а существующая база данных будет оставлена без изменений.
$ php app/console doctrine:database:create


Сущность Blog



Мы начнём с создания класса сущности Blog. Мы уже говорили о сущностях в предыдущей части, когда создавали сущность Enquiry. Поскольку сущность предназначена для хранения данных, имеет смысл использовать одну сущность для представления записи в блоге. Когда мы определяем сущность мы не говорим этим чтобы данные автоматически были сопоставлены с базой. Мы видели это в нашей сущности Enquiry (Запрос) где данные, хранящиеся в сущности были лишь только отправлены по электронной почте веб-мастеру.
Создайте новый файл src/Blogger/BlogBundle/Entity/Blog.php и вставьте следующее:
<?php
// src/Blogger/BlogBundle/Entity/Blog.php
				
namespace Blogger\BlogBundle\Entity;

class Blog
{
    protected $title;

    protected $author;

    protected $blog;

    protected $image;

    protected $tags;

    protected $comments;

    protected $created;

    protected $updated;
}



Как вы видите это простой PHP класс. Он не расширяет родительский и не имеет методов доступа. Каждое свойство заявлено как protected таким образом, мы не в состоянии получить доступ к ним при работе с объектом этого класса. Мы могли бы прописать геттеры и сеттеры для этих свойств, но в Doctrine 2 есть команда, которая выполняет эту задачу за нас.
Перед тем как запустить эту команду, мы должны сообщить Doctrine 2 каким образом сущность Blog должна быть отображена в базе данных. Информация указана в качестве метаданных, используя Doctrine 2 сопоставления. Метаданные могут быть определены в разных форматах включая: YAML, PHP, XML и Аннотации. В этом руководстве мы будем использовать аннотации. Важно отметить, что не все свойства в объекте должны быть сохранены, так что мы не будем предоставлять метаданные для них. Это дает нам возможность выбрать только те элементы, которые нам требуются, чтобы Doctrine 2 сопоставило их с базой данных. Замените содержимое класса сущности Blog, расположенной src/Blogger/BlogBundle/Entity/Blog.php
следующим:
<?php
// src/Blogger/BlogBundle/Entity/Blog.php

namespace Blogger\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="blog")
 */
class Blog
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string")
     */
    protected $title;

    /**
     * @ORM\Column(type="string", length=100)
     */
    protected $author;

    /**
     * @ORM\Column(type="text")
     */
    protected $blog;

    /**
     * @ORM\Column(type="string", length=20)
     */
    protected $image;

    /**
     * @ORM\Column(type="text")
     */
    protected $tags;

    protected $comments;

    /**
     * @ORM\Column(type="datetime")
     */
    protected $created;

    /**
     * @ORM\Column(type="datetime")
     */
    protected $updated;
}



Во-первых, мы импортируем и определяем пространство имен Doctrine 2 ОRМ Mappings. Это позволяет нам использовать аннотации для описания метаданных сущности. Метаданные содержат информацию о том, как свойства должны быть отображены в базе данных.

Заметка

Мы использовали только небольшую часть предоставляемых Doctrine 2 типов сопоставления. Полный список типов сопоставлений можно найти на веб-сайте Doctrine 2. Другие типы сопоставлений будут рассмотрены позже в этом руководстве.


Посмотрев внимательнее, вы возможно, заметили, что для свойства $comments не описаны метаданные. Это произошло потому, что мы не нуждаемся в сохранении, оно будет просто предоставлять набор комментариев, относящихся к сообщению в блоге. Подумайте об этом абстрагируясь от базы данных. Следующий код покажет более наглядно.
// Create a blog object.
$blog = new Blog();
$blog->setTitle("symblog - A Symfony2 Tutorial");
$blog->setAuthor("dsyph3r");
$blog->setBlog("symblog is a fully featured blogging website ...");

// Create a comment and add it to our blog
$comment = new Comment();
$comment->setComment("Symfony2 rocks!");
$blog->addComment($comment);

Приведенный выше фрагмент кода демонстрирует нормальное поведение, которого вы ожидаете от блога и комментариев класса. Внутренний метод $blog->addComment() может быть реализован следующим образом.
class Blog
{
    protected $comments = array();

    public function addComment(Comment $comment)
    {
        $this->comments[] = $comment;
    }
}

Метод addComment просто добавляет новый объект комментария свойства блога $comments. Получение комментариев тоже реализуется довольно просто.
class Blog
{
    protected $comments = array();

    public function getComments()
    {
        return $this->comments;
    }
}

Как вы можете видеть свойство $comments это просто список объектов Comment. Doctrine 2 не меняет, то как это работает. Doctrine 2 будет иметь возможность автоматически заполнять $comments свойство объектами, связанных с объектом blog.
Теперь, когда мы поговорили как сопоставить свойства сущности в Doctrine 2, мы можем сгенерировать методы доступа, используя следующую команду в консоли.
$ php app/console doctrine:generate:entities Blogger

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

Заметка

В то время как мы используем аннотации в нашей сущности, можно преобразовать информацию сопоставления в другие поддерживаемые форматы сопоставления используя команду doctrine:mapping:convert. Например, следующая команда, преобразует сопоставления сущности выше в yml формат.

$ php app/console doctrine:mapping:convert --namespace="Blogger\BlogBundle\Entity\Blog" yaml src/Blogger/BlogBundle/Resources/config/doctrine


И создаст файл в src/Blogger/BlogBundle/Resources/config/doctrine/Blogger.BlogBundle.Entity.Blog.orm.yml который будет содержать сопоставления сущности blog в yml формате.


Теперь мы готовы создать представление сущности Blog в базе данных. Есть 2 способа с помощью которых мы можем достичь этого. Мы можем использовать Doctrine 2 schema команды для обновления базы данных или более мощные, миграции Doctrine 2. Сейчас мы будем использовать schema команды. Миграции Doctrine буду рассмотрены в следующей части руководства.

Создание таблицы blog



Для создания таблицы blog мы можем воспользоваться следующей командой.

$ php app/console doctrine:schema:create


Это вызовет действия для генерации схемы базы данных для сущности blog. Вы можете также передать --dump-sql опцию, чтобы сделать дамп SQL. Если вы посмотрите на базу данных, то вы увидите, что таблица blog была создана с полями, к которым мы сделали сопоставление.

Совет

Мы использовали ряд консольных команд Symfony2. Вы можете получить помощь по любой команде введя опцию --help. Например, чтобы увидеть помощь по команде doctrine:schema:create введите:

$ php app/console doctrine:schema:create --help


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


Модель и отображение. Вывод записи блога.



Сейчас у нас есть сущность Blog и обновлённая база данных для её представления, мы можем начать интегрировать модель в отображение. Мы начнем с создания страницы show нашего блога.

Маршрут Show Blog

Мы начнём с создания маршрута для blog show action. Блог будет идентифицировать запись по уникальному ID, таким образом этот ID должен быть представлен в URL. Обновите BloggerBlogBundle маршрут, расположенный src/Blogger/BlogBundle/Resources/config/routing.yml следующим:

# src/Blogger/BlogBundle/Resources/config/routing.yml
BloggerBlogBundle_blog_show:
    path:  /{id}
    defaults: { _controller: "BloggerBlogBundle:Blog:show" }
    requirements:
        methods:  GET
        id: \d+


Так как ID записи должен быть представлен в URL, мы определили id placeholder. Это означает что URL вида http://localhost:8000/1 и http://localhost:8000/my-blog будут соответствовать этому маршруту. Однако, мы знаем, что id блога должно быть целым числом, (он определён таким образом в сопоставлении сущности) поэтому мы можем добавить ограничение, определяющее что этот маршрут будет соответствовать только тогда, когда параметр id содержит целое число. Это достигается с помощью id: \d+ требованием маршрута. Теперь только первый пример URL будет соответствовать, а http://localhost:8000/my-blog уже нет. Вы также можете увидеть, как соответствующий маршрут выполнит showAction, метод контроллера Blogger\BlogBundle\Blog. Этот контроллер ещё должен быть создан.

Метод showAction



Клей связывающий Модель и отображение — это контроллер, это то место где мы начнём создание страницы. Мы могли бы добавить метод showAction к нашему существующему контроллеру Page, но так как эта страница посвящена отображению сущностей blog, будет лучше добавить их в свой собственный контроллер Blog.
Создайте новый файл, расположенный в src/Blogger/BlogBundle/Controller/BlogController.php и вставьте следующее.
<?php
// src/Blogger/BlogBundle/Controller/BlogController.php

namespace Blogger\BlogBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

/**
 * Blog controller.
 */
class BlogController extends Controller
{
    /**
     * Show a blog entry
     */
    public function showAction($id)
    {
        $em = $this->getDoctrine()->getManager();

        $blog = $em->getRepository('BloggerBlogBundle:Blog')->find($id);

        if (!$blog) {
            throw $this->createNotFoundException('Unable to find Blog post.');
        }

        return $this->render('BloggerBlogBundle:Blog:show.html.twig', array(
            'blog'      => $blog,
        ));
    }
}

Мы создали новый контроллер для сущности Blog и определили метод showAction, а также задали id параметр в BloggerBlogBundle_blog_show правиле маршрута, он будет вставлен в качестве аргумента в метод showAction. Если бы мы определили больше параметров в правиле маршрута, они бы так же были бы вставлены в качестве аргументов, через запятую.
Далее нам нужно получить сущность Blog из базы данных. Мы впервые используем другой вспомогательный метод класса Symfony\Bundle\FrameworkBundle\Controller\Controller, чтобы получить Doctrine2 Manager. Работой Manager является обрабатывание, извлечение и сохранение объектов в базу данных и из неё. Далее мы используем объект Manager для получения Doctrine 2 Репозитория для сущности BloggerBlogBundle:Blog. Синтаксис, указанный здесь, просто сокращения, которые могут быть использованы в Doctrine 2 вместо указания полного названия сущности Blogger\BlogBundle\Entity\Blog. Вместе с объектом репозитория мы вызываем метод find() и передаём в аргументе $id. Этот метод будет получать объект по его первичному ключу.
И в конце мы проверяем, что сущность была найдена и передаём её в отображение. Если сущности не будет найдено, то вернётся createNotFoundException. Это сформирует ответ 404 Not Found.

Заметка

Объект репозитория предоставляет доступ к ряду полезных вспомогательных методов, включая
// Return entities where 'author' matches 'dsyph3r'
$em->getRepository('BloggerBlogBundle:Blog')->findBy(array('author' => 'dsyph3r'));

// Return one entity where 'slug' matches 'symblog-tutorial'
$em->getRepository('BloggerBlogBundle:Blog')->findOneBySlug('symblog-tutorial');


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

Отображение



Мы создали метод showAction для контроллера Blog и можем сфокусироваться на отображении сущности Blog. Как указано в методе showAction будет выведен шаблон BloggerBlogBundle:Blog:show.html.twig. Давайте создадим этот шаблон src/Blogger/BlogBundle/Resouces/views/Blog/show.html.twig и вставим следующее.
{# src/Blogger/BlogBundle/Resouces/views/Blog/show.html.twig #}
{% extends 'BloggerBlogBundle::layout.html.twig' %}

{% block title %}{{ blog.title }}{% endblock %}

{% block body %}
    <article class="blog">
        <header>
            <div class="date"><time datetime="{{ blog.created|date('c') }}">{{ blog.created|date('l, F j, Y') }}</time></div>
            <h2>{{ blog.title }}</h2>
        </header>
        <img src="{{ asset(['images/', blog.image]|join) }}" alt="{{ blog.title }} image not found" class="large" />
        <div>
            <p>{{ blog.blog }}</p>
        </div>
    </article>
{% endblock %}

Как и следовало ожидать, мы начнем с расширения основного шаблона Blogger BlogBundle. Далее мы переопределим заголовок страницы заголовком для нашего блога. Это будет полезно для SEO, так как заголовок страницы блога является более описательным, чем заголовок по умолчанию. Наконец, мы переопределим блок body для вывода контента сущности Blog. Мы используем функцию asset снова, чтобы вывести изображение в блоге. Изображения блога должны быть помещены в директорию web/images. Скачать изображения можно по ссылке.

CSS



Для того сделать визуальное оформление, нам нужно добавить некоторый стили. Добавьте стили ниже в src/Blogger/BlogBundle/Resouces/public/css/blog.css.

.date { margin-bottom: 20px; border-bottom: 1px solid #ccc; font-size: 24px; color: #666; line-height: 30px }
.blog { margin-bottom: 20px; }
.blog img { width: 190px; float: left; padding: 5px; border: 1px solid #ccc; margin: 0 10px 10px 0; }
.blog .meta { clear: left; margin-bottom: 20px; }
.blog .snippet p.continue { margin-bottom: 0; text-align: right; }
.blog .meta { font-style: italic; font-size: 12px; color: #666; }
.blog .meta p { margin-bottom: 5px; line-height: 1.2em; }
.blog img.large { width: 300px; min-height: 165px; }


Заметка

Если вы не используете метод символических ссылок для обращения к assets бандла в папке web, вы должны повторно запустить команду установки assets.
$ php app/console assets:install web


Так как мы создали контроллер и отображение для метода showAction давайте взглянем на страницу. Введите в ваш браузер http://localhost:8000/1. Это не та страница, которую вы ожидали увидеть?



Symfony сгенерировал ответ 404 Not Found. Это произошло потому что у нас нет данных в базе, т.е. сущность с id равным 1 не может быть найдена. Можно просто вставить строку в таблицу blog вашей базы данных, но мы будем использовать метод значительно лучше: Фикстуры данных.

Фикстуры данных



Мы можем использовать фикстуры для заполнения базы данных некоторыми простыми тестовыми данными. Для этого мы используем doctrine-fixtures-bundle и data-fixtures. Расширение Doctrine Fixtures не поставляется с Symfony2, мы должны вручную его установить. К счастью это простая задача. Откройте файл composer.json расположенный в корне проекта и вставьте следующее:
"require": {
    // ...
    "doctrine/doctrine-fixtures-bundle": "dev-master",
    "doctrine/data-fixtures" : "dev-master"
}

Далее обновите библиотеки командой.
$ composer update


Это обновит все библиотеки с Github и установит их в необходимые директории.
Теперь давайте зарегистрируем DoctrineFixturesBundle в kernel расположенного в
app/AppKernel.php

// app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        // ...
        new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(),
        // ...
    );
    // ...
}

Фикстуры Блога



Теперь мы готовы определить фикстуры для нашего блога. Создайте файл фикстур src/Blogger/BlogBundle/DataFixtures/ORM/BlogFixtures.php и вставьте
следующее:
<?php
// src/Blogger/BlogBundle/DataFixtures/ORM/BlogFixtures.php

namespace Blogger\BlogBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Blogger\BlogBundle\Entity\Blog;

class BlogFixtures implements FixtureInterface
{
    public function load(ObjectManager $manager)
    {
        $blog1 = new Blog();
        $blog1->setTitle('A day with Symfony2');
        $blog1->setBlog('Lorem ipsum dolor sit amet, consectetur adipiscing eletra electrify denim vel ports.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ut velocity magna. Etiam vehicula nunc non leo hendrerit commodo. Vestibulum vulputate mauris eget erat congue dapibus imperdiet justo scelerisque. Nulla consectetur tempus nisl vitae viverra. Cras el mauris eget erat congue dapibus imperdiet justo scelerisque. Nulla consectetur tempus nisl vitae viverra. Cras elementum molestie vestibulum. Morbi id quam nisl. Praesent hendrerit, orci sed elementum lobortis, justo mauris lacinia libero, non facilisis purus ipsum non mi. Aliquam sollicitudin, augue id vestibulum iaculis, sem lectus convallis nunc, vel scelerisque lorem tortor ac nunc. Donec pharetra eleifend enim vel porta.');
        $blog1->setImage('beach.jpg');
        $blog1->setAuthor('dsyph3r');
        $blog1->setTags('symfony2, php, paradise, symblog');
        $blog1->setCreated(new \DateTime());
        $blog1->setUpdated($blog1->getCreated());
        $manager->persist($blog1);

        $blog2 = new Blog();
        $blog2->setTitle('The pool on the roof must have a leak');
        $blog2->setBlog('Vestibulum vulputate mauris eget erat congue dapibus imperdiet justo scelerisque. Na. Cras elementum molestie vestibulum. Morbi id quam nisl. Praesent hendrerit, orci sed elementum lobortis.');
        $blog2->setImage('pool_leak.jpg');
        $blog2->setAuthor('Zero Cool');
        $blog2->setTags('pool, leaky, hacked, movie, hacking, symblog');
        $blog2->setCreated(new \DateTime("2011-07-23 06:12:33"));
        $blog2->setUpdated($blog2->getCreated());
        $manager->persist($blog2);

        $blog3 = new Blog();
        $blog3->setTitle('Misdirection. What the eyes see and the ears hear, the mind believes');
        $blog3->setBlog('Lorem ipsumvehicula nunc non leo hendrerit commodo. Vestibulum vulputate mauris eget erat congue dapibus imperdiet justo scelerisque.');
        $blog3->setImage('misdirection.jpg');
        $blog3->setAuthor('Gabriel');
        $blog3->setTags('misdirection, magic, movie, hacking, symblog');
        $blog3->setCreated(new \DateTime("2011-07-16 16:14:06"));
        $blog3->setUpdated($blog3->getCreated());
        $manager->persist($blog3);

        $blog4 = new Blog();
        $blog4->setTitle('The grid - A digital frontier');
        $blog4->setBlog('Lorem commodo. Vestibulum vulputate mauris eget erat congue dapibus imperdiet justo scelerisque. Nulla consectetur tempus nisl vitae viverra.');
        $blog4->setImage('the_grid.jpg');
        $blog4->setAuthor('Kevin Flynn');
        $blog4->setTags('grid, daftpunk, movie, symblog');
        $blog4->setCreated(new \DateTime("2011-06-02 18:54:12"));
        $blog4->setUpdated($blog4->getCreated());
        $manager->persist($blog4);

        $blog5 = new Blog();
        $blog5->setTitle('You\'re either a one or a zero. Alive or dead');
        $blog5->setBlog('Lorem ipsum dolor sit amet, consectetur adipiscing elittibulum vulputate mauris eget erat congue dapibus imperdiet justo scelerisque.');
        $blog5->setImage('one_or_zero.jpg');
        $blog5->setAuthor('Gary Winston');
        $blog5->setTags('binary, one, zero, alive, dead, !trusting, movie, symblog');
        $blog5->setCreated(new \DateTime("2011-04-25 15:34:18"));
        $blog5->setUpdated($blog5->getCreated());
        $manager->persist($blog5);

        $manager->flush();
    }

}




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

$blog1 = new Blog();
$blog1->setTitle('A day in paradise - A day with Symfony2');
$blog1->setBlog('Lorem ipsum dolor sit d us imperdiet justo scelerisque. Nulla consectetur...');
$blog1->setImage('beach.jpg');
$blog1->setAuthor('dsyph3r');
$blog1->setTags('symfony2, php, paradise, symblog');
$blog1->setCreated(new \DateTime());
$blog1->setUpdated($this->getCreated());
$manager->persist($blog1);
// ..

$manager->flush();

Мы начинаем с создания объекта Blog и определения значений его свойств. На данный момент Doctrine 2 ничего не знает об объекте сущности. Это произойдёт только тогда, когда мы вызываем $manager->persist($blog1), этим мы поручаем Doctrine 2, приступить к управлению этого объекта сущности. Объект $manager здесь является экземпляром объекта Manager который мы видели ранее при получении объектов из базы данных. Важно отметить, что в то время как Doctrine 2 в настоящее время известно об объекте сущности, она до сих пор не сохраняется в базе данных. Для этого необходим вызов $manager->flush() Метод flush заставляет Doctrine 2, начать взаимодействовать с базой данных и задействовать все сущности, которыми она управляет. Для лучшей производительности следует группировать команды Doctrine 2 и выполнять все действия за один раз. Мы создали каждый объект, попросили Doctrine 2, начать управлять ими, а затем выполнили необходимые операции.

Загрузка фикстур



Теперь мы готовы загрузить фикстуры в базу данных
$ php app/console doctrine:fixtures:load


На вопрос продолжать ли выполнение команды отвечаем: yes

Если мы взглянем на страницу http://localhost:8000/1 мы увидим запись в блоге.



Попробуйте поменять id в URL на 2. Вы должны увидеть следующую запись.

Если вы перейдете по URL http://localhost:8000/100 вы увидите ошибку 404 Not Found. Это произошло, как и ожидалось так как нет сущности Blog с id равным 100. Теперь попробуйте ввести http://localhost:8000/symfony2-blog Почему нет ошибки 404 Not Found? Это связано с тем что метод showAction не был вызван. URL-адрес не соответствует ни одному маршруту в приложении в связи с \d+ требованием, которое мы установили в BloggerBlogBundle_blog_show маршруте. Вот почему вы видите не найден маршрут для исключения «GET /symfony2-blog».

Метка времени



Наконец, в этой части мы посмотрим на 2 свойства меток времени в сущности Blog: created и updated. Функциональные возможности для этих 2-х членов, обычно называют как поведение Timestampable. Эти свойства содержат время, когда была создана запись и время её последнего обновления. Поскольку мы не хотим, вручную устанавливать эти поля каждый раз, когда мы создаем или обновляем запись, мы можем использовать Doctrine 2 для этих целей.
Doctrine 2 поставляется с Event System, которая обеспечивает Lifecycle Callbacks. Мы можем использовать эти события обратного вызова, чтобы зарегистрировать наши сущности, получать уведомления о событиях в течение всего срока жизни объекта. Несколько примеров событий, о которых мы можем быть уведомлены, прежде чем произойдет обновление, после сохранения и удаления. Чтобы использовать Lifecycle Callbacks в нашей сущности, нам нужно зарегистрировать сущность для них. Это делается с помощью метаданных, в сущности. Обновите сущность Blog src/Blogger/BlogBundle/Entity/Blog.php
<?php
// src/Blogger/BlogBundle/Entity/Blog.php

// ..

/**
 * @ORM\Entity
 * @ORM\Table(name="blog")
 * @ORM\HasLifecycleCallbacks
 */
class Blog
{
    // ..
}

Теперь давайте добавим метод в сущности Blog который регистрирует событие PreUpdate. Мы также добавим конструктор, чтобы установить значения по умолчанию для свойств created и updated.

<?php
// src/Blogger/BlogBundle/Entity/Blog.php

// ..

/**
 * @ORM\Entity
 * @ORM\Table(name="blog")
 * @ORM\HasLifecycleCallbacks
 */
class Blog
{
    // ..

    public function __construct()
    {
        $this->setCreated(new \DateTime());
        $this->setUpdated(new \DateTime());
    }

    /**
     * @ORM\PreUpdate
     */
    public function setUpdatedValue()
    {
       $this->setUpdated(new \DateTime());
    }

    // ..
}

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

setCreated();
setUpdated();


в файле
src/Blogger/BlogBundle/DataFixtures/ORM/BlogFixtures.php


и повторно запустите задачу загрузки фикстур вы увидите, что created и updated свойства установлены автоматически.

Заметка

Так как свойства timestampable являются очень распространенной потребностью, есть бандл который их поддерживает. StofDoctrineExtensionsBundle предоставляет ряд полезных Doctrine 2 расширений, включая Timestampable, Sluggable и Sortable.
Мы рассмотрим интеграцию этого бандла позже. Кому не терпится посмотреть перейдите по ссылке.


Вывод



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

Источники и вспомогательные материалы:

https://symfony.com/
http://tutorial.symblog.co.uk/
http://twig.sensiolabs.org/
http://www.doctrine-project.org/
http://odiszapc.ru/doctrine/

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


Часть 1 — Конфигурация Symfony2 и шаблонов
Часть 2 — Страница с контактной информацией: валидаторы, формы и электронная почта
Часть 4 — Модель комментариев, Репозиторий и Миграции Doctrine 2
Часть 5 — Twig расширения, Боковая панель(sidebar) и Assetic
Часть 6 — Модульное и Функциональное тестирование


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

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

Пожалуйста, оцените качество руководства

Поделиться публикацией

Комментарии 94

    0

    Сеттеры в сущностях… репозитории не как сервисы… хотя для бложиков ок.

      0
      А куда сеттеры нужно убрать? И какая выгода будет от репозиториев как от сервисов? В теме symfony совсем новичек, так что извините если вопросы глупые.
        0
        имхо, по поводу сеттеров имеется ввиду, что все данные задаются через конструктор и сеттерами не меняются.
          0
          В разрезе работы с доктриной, сущности таки меняются и имеют сеттеры. Это вопрос уже не к автору статьи, а скорей к доктрине.
            0
            В разрезе работы с доктриной, сущности таки меняются и имеют сеттеры.

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

              +1
              Доктрина создаёт/меняет сущности без использования конструкторов и сеттеров. Они ей не нужны, она их не вызывает.
            0
            И какая выгода будет от репозиториев как от сервисов?

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

                зависит от подходов. Я никогда не наследуюсь от доктриновских репозиториев и всегда делаю свои сервисы, в которые внедряется entity manager. А что бы было удобно — autowire все решает.

                0
                А куда сеттеры нужно убрать?

                Для начала давайте различать сеттеры, которые сгенерированы вашей IDE или симфоневским генератором, и сеттеры которые реально нужны.


                Вот вам простой пример. У вас есть требование "пользователь должен иметь возможность сменить пароль" и у вас появляется метод:


                function changePassword(string $password, callable $hasher) : void 
                {
                     $this->password = $hash($password); // мы никогда не забудем захэшировать пароль
                }

                А если в рамках требований нужно сменить сразу 4 свойства — мы либо делаем метод с 4-мя аргументами, либо, что вероятнее, уберем эти 4 свойства в отдельный объект-значение (гуглить doctrine embeddable), и будем просто заменять оный.


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


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

                  +1
                  Спасибо за развернутый ответ. Согласен, что на маленьких проектах можно некоторыми правилами пренебречь, но когда изучаешь новый инструмент хочется учиться применять его правильно изначально, а потом уже когда появится понимание как и что нужно делать — тогда уже можно и «похамить» в коде в угоду той самой скорости разработки.
                    +1

                    Меня радует такой подход к изучению вещей) последовательный)


                    Суть в том что если вы хотите изучить доктрину — вам лучше не смотреть на ее использование в документации симфони. Так же чуть выше я дал ссылку на видео где преподносятся основные идеи когда и как использовать доктрину.

                    0
                    А если надо сразу все свойства изменить? Например, менеджер открыл заявку, позвонил пользователю, уточнил информацию, поменял некоторые поля и нажал «Сохранить». Как правильно сделать рендеринг формы с данными, прием запроса, валидацию и сохранение?
                      0
                      А если надо сразу все свойства изменить?

                      Я сомневаюсь что вам нужно менять ID сущности или поля вроде даты создания оной. С другой стороны вместо вызова десятка сеттеров можно вызвать один метод с комком данных а там уже внутри его разбирать. Или, что еще интереснее — запихнуть эти данные в имутабельный объект и заменять целиком.


                      Как правильно сделать рендеринг формы с данными, прием запроса, валидацию и сохранение?

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

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

                        В том то и дело. Сеттеры превращаются в обычные методы. Просто поведение внутри сущности, которое принимает решение менять состояние объекта или нет. Вывод — у нас нет сеттеров, хоть код этих методов часто и похож на обычные сеттеры.

                          +1
                          Это не обычная функция, а функция, единственным назначением которой является изменение состояния объекта. Тупые сеттеры (просто назначение свойству аргумента) — частный случай таких функций. Но если мы добавляем в тупой сеттер какой-то код (а именно для этого сеттеры и создаются, чтобы добавлять какой-то код в объект при изменении свойства), то чёткой грани между умным сеттером и обычной функцией нет.
                          +1

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


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

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


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

                            0
                            Лучше использовать классический сеттер и при сохранении сущности хэшировать пароль

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


                            Если вам хочется что бы просто и удобно, делаем так:


                            class Password
                            {
                                private $value;
                            
                                private function __constructor(string $password)
                                {
                                      $this->value = $password;
                                }
                            
                                public static function create(string $password, PasswordHasher $hasher)
                                {
                                     return new self($hasher->hash($password));
                                }
                            
                                public function __toString()
                                {
                                     return $this->value;
                                }
                            }

                            Собственно все. Вместо double-dispatch можно использовать просто фабрику… и тогда тоже все замечательно.


                            Например SonataAdminBundle использует PropertyAccessor для изменения полей сущности.

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

                              0

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


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

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

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

                                  нет.


                                  Сущность хеширует пароль;

                                  нет, хэширует пароль тот кто создает инстанс Password, а это не сущность.


                                  Сущность загружает файл;

                                  нет, в сущность приходит уже готовый инстанс FileReference или что-нибудь такое.


                                  Сущность изменяет свои загруженные файлы;

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


                                  Сущность удаляет свои загруженные файлы;

                                  смотри предыдущее.


                                  Сущность сохраняет последнего пользователя редактирующего ее;

                                  зависит от задачи. Иногда — да, иногда сущности столько информации давать не стоит и мы закрываем сверху все сервисом который запоминает + делигирует операцию сущности.


                                  Сущность сохраняет редактора который заапрувил ее;

                                  опять же возможно. Смотрим предыдущий пункт.


                                  Сущность изменяет свои даты в соответствии с часовым поясом пользователя полученным из запроса, которого кстати нет в консолм;

                                  эм… сущность получает готовый value object, не нужно ему ничего считать.


                                  Сущность привязывает к себе соседние сущности в цепочке сущностей.

                                  если эти сущности входят в агрегат сущностей для этой конкретной бизнес-трназакции.


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

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

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

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


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


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


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

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


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

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


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


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


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


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

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

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


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

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


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

                                      гуглить доменные ивенты. Тогда вопрос "кто что и как" отпадет. Пример: https://github.com/php-ddd/domain-event


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

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

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

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


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

                                          0
                                          мы можем, а… мы не можем.


                                          Именно. Сущности не нужен побочный эффект в виде удаления старого файла, это кому-то другому нужно. А захешированный пароль нужен самой сущности. Как минимум зачем вводить оверхид да ещё с циклическими зависимостями, если можно сделать простой вызов функции? А ещё события могут логироваться, а там plaintext.
                                            0
                                            А захешированный пароль нужен самой сущности.

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


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

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


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

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

                                              0
                                              Не нужен сущности сам пароль. Зачем он ей?

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

                                              Запросы к сущности особого отношения не имеют, а вот самой передавать пароль плэйнтекстом всему миру через события — не хорошо.
                                                0
                                                Может я чего-то не понимаю, но вроде говорили про то, что сделать выбрасывания события на $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());
                                                }
                                                  0

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


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

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

                                                  0

                                                  тут интереса ради заглянул под капот 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


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

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

                                                    Простите, но вот этот "кастыль" с листенерами и есть "вне пользователя и сеттеры". Вот как без них:


                                                    class UserBuilder
                                                    {
                                                        private $email;
                                                        private $password;
                                                        private $plainPassword;
                                                        private $firstName;
                                                        // ...
                                                        public function usingPassword(string $password, PasswordEncoder $encoder) 
                                                        {
                                                             $this->password = $encoder->encode($password);
                                                             $this->plainPassword = $password; // может для каких email-ов
                                                    
                                                             return $this;
                                                        }
                                                    
                                                        public function withEmail(string $email)
                                                        {
                                                             $this->email = $email;
                                                    
                                                             return $this;
                                                        }
                                                        // ...
                                                    
                                                        public function buildUser()
                                                        {
                                                              return new User($this);
                                                        }
                                                    }
                                                    
                                                    class User
                                                    {
                                                        private $email;
                                                        private $password;
                                                    
                                                        public function __construct(UserBuilder $builder)
                                                        {
                                                              // тут еще валидацию бы
                                                              $this->email = $builder->getEmail();
                                                              $this->password = $builder->getPassword();
                                                              // ...
                                                              // domain events, ну что б совсем упороться
                                                              $this->remember(new UserRegisteredEvent($this, $builder));
                                                        }
                                                    
                                                        public function changePassword(string $password, PasswordEncoder $encoder)
                                                        {
                                                             $this->password = $encoder->encode($password);
                                                        }
                                                    }
                                                    
                                                    $user = (new UserBuilder())
                                                         ->withEmail($email)
                                                         ->usingPassword($password, $passwordEncoder)
                                                         // ...
                                                         ->buildUser();

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

                                                      0

                                                      @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. Это конечно если бизнес логика нам важна именно на внешнем интерфейсе, а не в админке, а так по идее и должно быть ибо админка все таки для администраторов.


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

                                                        0
                                                        Чем-то вы мне напоминаете нашего общего знакомого G-M-A-X. Вместо того чтоб использовать готовое решение делаем свое.

                                                        Мне комфортнее когда мои бизнес сущности ничего не знают о фреймворках и т.д. Хотя сказать что я так делаю всегда и везде — нет конечно. У меня полно проектов где сеттеры в сущностях, и где толстые контроллеры бывают. Но это скорее как "компромис" когда нужно быстро и вроде как не страшно.


                                                        Вы не думали написать статью на тему использования DDD в Symfony?

                                                        DDD не про фреймворки. Можно скорее про DDD with Doctrine, но на эту тему можно просто сходить например к marco pivetta на воркшопы.


                                                        Я же предпочитаю опираться на Data Driven Design чтоб не усложнять себе жизнь и повышать уровень реиспользования кода.

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


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

                                                        Всякие API имеют такую штуку как версии. И вот у нас уже два способа сериализации одной и той же сущности. А еще есть нюансы кому что показывать можно а кому нет. И вот нам уже нужна прослойка, я в качестве оной использую fractal + symfony/serializer для всякой мелочи. все работает предсказуемо и лишнего кода нет.


                                                        Ну то есть идея та же что и у MVC как UI паттерна. Отделение ответственности преобразования представления данных из модели в то, что хочет видеть клиент.


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

                                                        нет, сущность ничего не должна знать об этом (так проще потом). Она может иметь метод который позволяет выплюнуть слепок состояния но это такое. Тут на самом деле я на 100% не могу сказать "как правильно", просто я уже пробовал "ваш" подход на протяжении 3-х лет и "мой" подход на протяжении последнего года и последний пока вызывает меньше проблем.


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

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


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

                                                        Так же пробовал, проигрыш. Даже symfony best practice говорит что это "так себе" идея. Они не дает никакого профита а все условное разделение и так можно сделать при помощи нэймспейсов.


                                                        Я допускаю использование бандлов если в будущем есть шанс разделить все на микросервисы, но в этом случае никаких CoreBundle-в (это как god object только бандл) и между бандлами не должно быть зависимостей на уровне сущностей (то есть данные одного бандла доступные только через сервисы, никаких сущностей, если что-то надо — данные дублируем). В таком случае мы потом сможем легко на микросервисы перейти. Но это нужно хорошо если 0.1% проектов.


                                                        Инфраструктуру выносить в бандлы — очень удобно. Например UploadBundle и т.д. Моя команда сейчас пытается это вообще в виде микросервисов отдельными докер контейнерами решать.


                                                        В таком случае фронтенд и api можно писать по DDD, а админку делать с тупым CRUD,

                                                        У меня как-то давно небыло проектов где админка это тупой CRUD. Да и для этого можно поднять какой-нибудь UI для прямого доступа в базу.


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

                                                        Короч вы предлагаете вместо "давайте разберемся что такое ООП, как проектировать системы, как использовать подходы и инструменты те что надо и там где это надо" фигачить все как тупой CRUD. Ну ок, я вас услышал.

                                                          0
                                                          слаб я как DBA

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


                                                          я уже пробовал "ваш" подход на протяжении 3-х лет и "мой" подход на протяжении последнего года

                                                          Не удивлен. Потому я и считаю что каждый должен заниматся своей задачей. Имхо сериализация и десериализация не относятся к задачам модели.


                                                          Я допускаю использование бандлов если в будущем есть шанс разделить все на микросервисы

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


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


                                                          никаких CoreBundle-в 
                                                          Инфраструктуру выносить в бандлы — очень удобно. Например UploadBundle и т.д. 

                                                          Полностью согласен. В одном из дакладов на framework day тоже говорили что CoreBundle это зло. Только я предпочитаю компоненты выносить не просто в бандлы, а в отдельные Composer пакеты.


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

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


                                                          Учитывая мощь Sonata, грешно не воспользоваться ею хотябы на первых этапах запуска проекта.

                                                            0
                                                            Вы не обращали внимание на то что схема генерируемая доктриной сильно не оптимальна?

                                                            А что есть "оптимально"? Схема там генерится так как описано, это все же не полностью все на магии.


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


                                                            Имхо сериализация и десериализация не относятся к задачам модели.

                                                            Согласен. И у меня за это отвечают отдельные штуки.


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

                                                            это то что я говорил про микросервисы. Если у вас есть CoreBundle — в этом нет смысла.


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

                                                            у меня репозиторий обычно содержит парочку методов (как правило add, и один-два метода на выборки). А если нужно делать много разных выборок и все может быть сложно — мне нравится паттерн спецификация. То есть за формирование запросов отвечают объекты спецификаци а не репозиторий. Это позволяет резко понизить сложность.


                                                            Полностью согласен. В одном из дакладов на framework day тоже говорили что CoreBundle это зло.

                                                            Если это было на тех выходных, то может даже я это и говорил)


                                                            Учитывая мощь Sonata, грешно не воспользоваться ею хотябы на первых этапах запуска проекта.

                                                            Возможно следует пояснить как выглядят мои проекты. У меня бэкэнд это чисто HTTP API, а админки, круды и т.д. у меня сделаны на ангулярах. На нем делать админки — просто в путь. И выходит очень гибко. А так как у меня всеравно будет HTTP API выходит так что нет лишних вещей.


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


                                                            Сонату года 4 не юзаю потому что "сейчас быстро наклепать" может и ок но потом просто слишком долго кастомизировать. Потому до перехода на ангуляр использовал стандартный CRUD генератор с кастомизированными шаблонами (нашел какие-то на гитхабе и чуть адаптировал под себя).

                                                              0
                                                              А что есть "оптимально"? Схема там генерится так как описано, это все же не полностью все на магии.

                                                              О, там много чего:


                                                              • индексы нужно описывать в аннотациях. Мелочь, а не приятно
                                                              • FOREIGN KEY доктрина не создает или я не нашел как это сделать
                                                              • Хотябы на уровне таблицы нужно ставить кодировку. Не стоит расчитывать на настройки сервера.
                                                              • кодировку для полей ставить тоже не очень удобно.
                                                              • комментарии к полям таблицы и самой таблицы добавляются не очень красиво. В русскоязычных проектах я всегда добавляю комментарии, чтоб любой разработчик, не знакомый с проектом, мог по базе понять как всё устроено.
                                                              • для интов тоже нужно указывать длинну. Если мы точно знаем что в конкретном случае число не будет больше 200, то нет необходимости использовать INT, тут больше подойдет TINYINT(2) UNSIGNED.
                                                              • если в базе используется TINYINT (2) доктрина всё равно преобразует его в boolean при генерации маппинга.
                                                              • для представления boolean в бд доктрина использует антипаттерн TINYINT(1) со значениями 1 и 0. Во первых в MySQL есть тип данных BOOLEAN который экономичней, но имеет значения TRUE и NULL, что не совсем логично (давно не заглядывал в доку) сейчас этого типа уже нет, а BOOL это синоним к TINYINT. В TINYINT(1) в действительности можно записать не только 0 и 1, а можно записать числа 0-9 и NULL, а если не стоит UNSIGNED то и отрицательные числа замечательно записываются. Это звучит смешно, но мне один раз достался проект в котором у пользователей в поле gender были значения 0, 1, 2, 3, 4 и NULL. Назначение некоторых значений не знали даже самые старые члены команды. И для каждого пола 10к+ пользователей. Поэтому я предпочитаю использовать тип ENUM. Он занимает столько же места что и TINYINT, но чётко ограничивает набор доступных значений и делает их более информативными. Согласитесь, status 0/1 выглядит менее информативно чем status enabled/disabled? И расширять такие статусы проще. Были у меня случаи когда при изменении требований boolean превращался в enum.

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


                                                              мне нравится паттерн спецификация

                                                              Согласе. Интересный паттерн. Хотя он решает только треть задач ставящихся перед репозиторием. Он позволяет управлять фильтрами, но запросы это не только выражение where, но и джойны, группировки, хевинги, кастомезированные селекты и т.д. Видимо нужно создавать классы для отдельных запросов, но что тогда делать с внешними зависимостями.


                                                              Если это было на тех выходных, то может даже я это и говорил)

                                                              Я смотрел доклад Олега Зинченко опубликованнный в 2014 году. На прошлой неделе что-то я таких замечаний не слышал.


                                                              У меня бэкэнд это чисто HTTP API, а админки, круды и т.д. у меня сделаны на ангулярах.

                                                              Это хорошо когда есть сильные фронтенд программисты. Я таким похвастаться не могу. Сам во фронтенд стараюсь не лезть.

                                                                0
                                                                индексы нужно описывать в аннотациях. Мелочь, а не приятно

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


                                                                FOREIGN KEY доктрина не создает или я не нашел как это сделать

                                                                создает же.


                                                                Хотябы на уровне таблицы нужно ставить кодировку. Не стоит расчитывать на настройки сервера.

                                                                эм… у меня это на уровне схемы делается, не нужно это делать на уровне таблицы.


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

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


                                                                для интов тоже нужно указывать длинну. Если мы точно знаем что в конкретном случае число не будет больше 200, то нет необходимости использовать INT, тут больше подойдет TINYINT(2) UNSIGNED.

                                                                ну так выставляйте, откуда доктрина то это узнает? Вы ее просите инты хранить — так храните. Доктрина не настолько волшебна что бы читать ваши мысли.


                                                                если в базе используется TINYINT (2) доктрина всё равно преобразует его в boolean при генерации маппинга.

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


                                                                для представления boolean в бд доктрина использует антипаттерн TINYINT(1) со значениями 1 и 0.

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


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

                                                                тогда вам не нужна доктрина. Об этом авторы доктрины твердят уже не первый год — если вам нравится ковыряться в базе данных и вы молитесь на 4-ую нормальную форму — доктрина не для вас.


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

                                                                Шаблон спецификация не регламентирует то как вы выборки делаете. И я не про Criteria доктриновскую. У меня спецификации работают с query builder-ом.


                                                                Я таким похвастаться не могу. Сам во фронтенд стараюсь не лезть.

                                                                ну я фулстэк разработчик. Мы сделали отдельно отдел фронтэндщиков что бы похапэшники не лезли во фронтэнд. И я могу сказать что это весьма профитно.

                                                                  0
                                                                  Шаблон спецификация не регламентирует то как вы выборки делаете. И я не про Criteria доктриновскую. У меня спецификации работают с query builder-ом.

                                                                  Я думал вы про шаблон проектирования, а тут выходит всё ещё хитрее. Не поделитесь примерчиком?


                                                                  Мы сделали отдельно отдел фронтэндщиков что бы похапэшники не лезли во фронтэнд. И я могу сказать что это весьма профитно.

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

                                                                    0
                                                                    Не поделитесь примерчиком?

                                                                    что-то такое: https://github.com/Happyr/Doctrine-Specification

                                                                      0

                                                                      Библиотеку видел, но дальше readme не ушёл и про QueryModifier не прочитал. Спасибо

                                                              0
                                                              в репозиториях у меня получается каша. Там есть методы которые используются только в админке, есть методы которые есть только в консоли, методы которые используются только на фронте и методы которые используются для мигрирования данных со старого проекта на новый. Всё это разные методы и ни как не пересикаются.
                                                              И вот эта каша мне савсем не нравится. Есть у вас какие мысли на этот счёт?


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

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

                                                              В общем и в целом, выделение кода приложения в отдельные бандлы имеет смысл только если они не зависят друг от друга, в частности, если говорить про модель и БД, если сущности бандлов не ссылаются друг друга, вернее могут ссылаться по, например, айдишникам, но без поддержки объектной связи на уровне модели и без поддержки внешних ключей на уровне схемы. Практически микросервисная архитектура, но без потерь на межпроцессное и межсерверное взаимодействие. При необходимости можно создавать отдельные бандлы агрегаторы, которые будут собирать данные с других и отдавать клиенту, чтобы избавить клиента от необходимости делать 1+N запросов к серверу, просто делать эти 1+N запросов со стороны бандла агрегатора (вернее он будет делегировать запросы другим бандлам, сам агрегатор про базу ничего не знает). Как оптимизация агрегатор (хотя в принципе и клиент) может делать два запроса, скажем, сначала выбрать все договора по какому-то критерию обратившись к одному бандлу, а затем выбрать всех физлиц (сотрудников, клиентов, поручителей и т. п.), относящихся к пулу договоров одним запросом типа WHERE id IN (/* 100500 айдишников */). Если же хочется использовать в запросах джойны, то лучше все сущности, которые предполагается джойнить держать в одном бандле. Поддержка межбандловых связей на уровне ORM/СУБД очень быстро превращается в ад, особенно двухсторонних. В общем если возникает мысль вынести какой-то код связанный с ORM в бандл, я задаю себе вопрос «если я захочу вынести данные этого бандла в другую базу, может даже другую СУБД, а может вообще не в СУБД, а решу в файлах хранить, то мне надо будет переделывать остальное приложение?». И если ответ «да», то не выношу, выношу только то, что можно спрятать за простыми публичными контрактами, никак не касающихся системы хранения, из которых не торчат уши Доктрины. Бывают исключения, когда выношу в бандлы куски кода с контрактом из которого торчит доктрина, но исключительно для использования в разных несвязанных через базу проектах, когда нужно повторять одни и те же таблицы (пользователи, логирование и т. п.) в разных базах, но это именно исключение.
                                                                0
                                                                Если стоит задача просто упорядочить код репозитория, разделить запросы фронта, бэка и т. п. для более удобной работы с ними, то можно просто использовать трейты.

                                                                @Fesor уже предложил решение получше. Использовать спецификацию из DDD (пример)


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

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

                                                0
                                                Про события я писал.

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


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

                                                нет, поскольку до момента отработки пароля наша сущность является "не валидно" (пароль не задан или задан в открытом виде).


                                                Почему мы не можем рассматривать пароль в сущности как всегда хешированный пароль

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


                                                $user = User::create($request->get('email'), Password::create($request->get('password'), $hasher);
                                                  0

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


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

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

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

                                                    Это не создание сущностей, а создание Data Transfer Object (обычно объект чисто с публичными свойствами и без методов, по сути сишная структура) с примерным циклом жизни в случае Create/Update запросов: создали DTO из запроса (с помощью формы или напрямую), провалидировали DTO (по сути провалидировали запрос), создали или обновили соответствующую сущность из DTO, удалили DTO.
                                                      +1
                                                      этот подход полностью не совместим с SonataAdminBundle.

                                                      и это хорошо, потому как с Sonata доктрина нам как бы тоже не особо нужна. Нам нужно что-то тупое вроде active record.


                                                      Этот подход не позволяет использовать оригинальные сущности в формах.

                                                      именно! И это тоже хорошо! потому что сущности это не DTO.


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

                                                      никто не говорит об отказе от стандартных (кто сказал что саната стандарт?) компонентов, просто другие подходы. Я просто говорю что если у вас такие потребности — вам не нужна доктрина.

                                                        0

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


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

                                                          0
                                                          Если Сонату легко прикрутить к приложению и проблем (например, приведение сущности в неконсистентное состояния руками администратора/менеджера) её использование не вызывает, значит возможности Доктрины и на 10% не используются в приложении, а сущности только лишь так называются, а по факту просто структуры данных для представления базы данных.
                                                +1
                                                Напоминаю что только сущность знает путь загрузки картинки относительно корня проекта, но она не знает путь к корню проекта.


                                                Это лишние знания для сущностей альбома и новости. Сущности должны хранить бизнес-знания и бизнес-логику, а в проекте типа сайта с альбомами и новостями, вещи типа путей, файлов и тем более манипуляции с ними — это инфраструктура. Сущность может хранить путь к файлу как неизменяемый ValueObject или строку, но это дело инфрастуктурных сервисов (например, контроллера) как-то создавать эти объекты и как-то синхронизировать их с файловой системой.
                                              0
                                              Сущность сохраняет последнего пользователя редактирующего ее;

                                              Если бизнес-логика требует сохранения последнего пользователя, то метод типа aplpyPatchAs($patch, User $user) вполне нормален в сущности
                                              Сущность сохраняет редактора который заапрувил ее;

                                              Аналогично, метод approveAs(User $editor)
                                              Сущность изменяет свои даты в соответствии с часовым поясом пользователя полученным из запроса, которого кстати нет в консолм;

                                              Сущность не изменяет свои даты для показа пользователю, это ответственность представления. Сущность хранит даты (вернее датывремя?) либо с часовым поясом, либо в UTC, либо в каком-то «дефолт-сити», а представления уже форматируют дату (через тот же \DateTime::format()) как нужно пользователю.
                                              Сущность привязывает к себе соседние сущности в цепочке сущностей.

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

                                              Прямое (через свойство/сеттер) привязывание к себе именно соседних по иерархии классов сущностей обычно говорит о нарушении инкапсуляции. И доктрина обычно в этом не виновата.
                                                0
                                                Аналогично, метод approveAs(User $editor)

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


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

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


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

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

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

                                                  Не нарушается инкапсуляция — имя метода отражает бизнес-операцию, а не работу со свойствами. Ну, если следовать соглашению, что имя метода set(.*) — это присваивание значения свойству $1, пускай и с дополнительными проверками.

                                                  И изменение часового пояса это не только задача представления

                                                  Изменение часового пояса в свойстве сущности, типа «а теперь это время будем считать по этому поясу» или использование часового пояса пользователя в расчётах, типа запроса к сущности «посчитай баланс счёта на 0:00:00 по московскому времени»?
                                                  А что если цепочка сущностей это целая таблица в которой 2кк записей? Агригатор тут не справится. Нужно как-то иначе связывать события.

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

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

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

                                                      0
                                                      Так это корень агрегата должен устанавливать свойства при добавлении агрегата. Грубо что-то вроде:
                                                      public function addEvent(string $event, \DateTime $time) {
                                                        $prevEvent = $this->events()->last();
                                                        $newEvent = new Event($this, $event, $time, $prevEvent);
                                                        $prevEvent->setNextEvent($newEvent);
                                                        $this->events->add($newEvent);
                                                      }
                                                      
                                    +1
                                    Что мешает сделать так, не думаю что сеттеры вам особо усложнят жизнь, и на больших проектах наоборот могут помочь.

                                    function changePassword(string $password, callable $hasher) : void 
                                    {
                                         $this->setPassword($hash($password)); // мы никогда не забудем захэшировать пароль
                                    }
                                    
                                      0

                                      1) сеттер будет приватным методом в этом случае, поскольку мы не хотим давать внешнему миру им пользоваться
                                      2) у вас и так есть доступ к состоянию объекта и этот метод вполне себе способен его менять. Дополнительные методы не нужны.
                                      3) на больших проектах с сеттерами, сущности доктрины превращаются в тупое хранилище данных, а вся бизнес логика вытекает в лучшем случае в сервисный слой. И в итоге толку тогда от доктрины? Да и даже ORM в этом случае нам вообще не нужен.

                                        0
                                        Это уже частные случаи, мы сейчас все же говорим относительно статьи.
                                        В большинстве случае entity доктрины генерируются из бд через консоль автоматически и мы уже получаем класс с сеттерами, а уже все действия над ним лучше вынести в репозиторий или модель. На сколько они нужны, не нужны — вопрос второй, но даже в IDE вам будет проще с сеттерами с автокомплитом.
                                          0
                                          В большинстве случае entity доктрины генерируются из бд через консоль автоматически

                                          Вот честно за 5 лет ни разу так не делал. Более того, разработчики доктрины не рекомендуют так делать.


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


                                          То есть если мы работаем с доктриной и представляем сущности как тупую проэкцию рядов таблиц на объекты — вам не нужна доктрина. Вам хватит какого-нибудь active record.


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

                                          Потому что у вас в сущностях сеттеры, а не потому что это просто лучше) Есть такой шаблон проектирования (или скорее даже принцип) под названием "информационный эксперт": информация должна обрабатываться там, где она есть. То есть в нашем случае пока бизнес логика находится в рамках одной сущности или мы можем легко выделить корень агрегата сущностей — лучше эту логику пихать в сущность. А уж если мы не можем выделить корень агрегата (например надо посчитать сумму всех заказов пользователя и сущность пользователя ничего не знает о заказах) то тогда да, выносим это дело в сервис. Но подсчет суммы одного заказа — дело самого заказа.

                                            0
                                            у меня щас жесткое дежавю. гдето я такое уже читал оО
                                              0

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

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

                                                тут нет никакого спора. Когда у вас больше действий в контроллере, у вас нет разделения бизнес логики и презентационной логики, и называется это SmartUI. Вполне себе годный вариант для CRUD-а например, и покрывает он подалвяющее большинство проектов. Это не плохо, просто на более сложных проектах с более сложной логикой вы проиграете. В лучшем случае все закончится нарушением DRY. В худшем — связанность системы будет настолько высока, что сопровождать проект будет болью (как для разработчика, так и для бизнеса)


                                                Вы можете выполнять действия в классе Entity или можете вынести эти функции в модель или репозиторий

                                                Просто приведите формулировку термина "модель". Что это у вас? Модель предметной области? Какая-то другая модель?


                                                Здесь скорее вопрос кому как нравится.

                                                Ну да, и принципы всякие вроде SOLID или GRASP придумали от скуки.

                                                  0
                                                  Вы понимаете что я могу ответить также что геттеры и сеттеры придумали не от скуки. Не важно нужны они вам или нет, это не значит что их, относительно данной статьи, не должно быть у других, и что это не правильный подход.
                                                  А на счет модели я думал вы знает для чего их придумали. В doctrine просто работу с данными можно вынести в repository, а так в модели.
                                                    +1
                                                    Вы понимаете что я могу ответить также что геттеры и сеттеры придумали не от скуки.

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


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


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

                                                    Культ карго. Люди будут делать так как показали вне зависимости от размеров проекта. Без какого либо понимания что они делают и зачем.


                                                    А на счет модели я думал вы знает для чего их придумали.

                                                    Понимаете ли, это не имеет значения. Меня интересует ваша интерпритация что бы мы говорили на одном языке. Ибо для меня модель — это модель. А сущность — представляет собой модель бизнес объекта, одного. Сервис представляет модель взаимодействия нескольких несвязанных между собой сущностей (ибо если они связаны они сами разберутся).


                                                    В doctrine просто работу с данными можно вынести в repository, а так в модели.

                                                    Репозитории это вещи, которые отвечают за хранение. В них может находиться бизнес ограничения вроде "можем ли мы ложить туда эту сущность или нет". Но репозитории НЕ мутируют состояние сущностей. Это просто склад. Склад который меняет состояние объектов там хранящихся — плохой склад.

                                                      0
                                                      Ладно, все это хорошо, вы тогда нам поведайте как лучше в fixture переписать это для начинающих
                                                      $blog1 = new Blog();
                                                      $blog1->setTitle('A day in paradise - A day with Symfony2');
                                                      $blog1->setBlog('Lorem ipsum dolor sit d us imperdiet justo scelerisque. Nulla consectetur...');
                                                      $blog1->setImage('beach.jpg');
                                                      $blog1->setAuthor('dsyph3r');
                                                      $blog1->setTags('symfony2, php, paradise, symblog');
                                                      $blog1->setCreated(new \DateTime());
                                                      $blog1->setUpdated($this->getCreated());
                                                      $manager->persist($blog1);
                                                      
                                                        0
                                                        $title =  'A day in paradise - A day with Symfony2';
                                                        $desc =  'Lorem ipsum dolor sit d us imperdiet justo scelerisque. Nulla consectetur...';
                                                        $image =  'beach.jpg';
                                                        $author = 'dsyph3r';
                                                        $tags = new ArrayCollection(['symfony2', 'php', 'paradise', 'symblog']);
                                                        $createdAt = new \DateTime();
                                                        $blog = new Blog($title, $desc, $image, $author, $tags, $createdAt, $createdAt);
                                                        
                                                          0
                                                          ну вот видите, сложно сказать что ваш вариант в данном случае лучше,
                                                          я же по памяти не помню какие у меня поля, а смотреть в описании к Blog не очень удобно.
                                                          Я это просто к тому, что все хорошо при определённых условиях — в статье к созданию блога сеттеры вполне уместны, да и не только.
                                                            0
                                                            я же по памяти не помню какие у меня поля

                                                            Вы как бы в таком случае не знаете и какие там есть сеттеры. А так вашу проблему с памятью в обоих случаях решает автокомлит.

                                                              0
                                                              как мне должен помочь пример выше и ваш с автокомплитом?
                                                                0

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

                                                            +1
                                                            А такой:
                                                            $blog = new Blog(
                                                              'A day in paradise - A day with Symfony2',
                                                              'Lorem ipsum dolor sit d us imperdiet justo scelerisque. Nulla consectetur...',
                                                              'beach.jpg',
                                                               'dsyph3r',
                                                               ['symfony2', 'php', 'paradise', 'symblog'] 
                                                            );
                                                            

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

                                                            Вот только ArrayCollection вне сущностей создаваться не должен. Они живут только внутри сущностей, наружу тоже не вылазят.

                                                              0

                                                              И да, такие поля как createdAt, updatedAt и т.д. создаются в конструкторе и в методах где действие и происходит. Именно в этом профит делать все операции по изменению состояния через один вызов какого-то метода. Вы всегда знаете когда действие началось и когда завершилось. И можете делать много интересных вещей внутри. Рефакторить не нашурая обратной совместимости и минимизируя возможность регрессиий....


                                                              Словом инкапсуляция.

                                                              0
                                                              $blog = Blog::fromArray([
                                                                  'title' => 'A day in paradise - A day with Symfony2',
                                                                  'description' => 'Lorem ipsum dolor sit d us imperdiet justo scelerisque. Nulla consectetur...',
                                                                  'image' => FileReference::local('beach.jpg'),
                                                                  'tags' => $this->tags(['symfony', 'php', 'paradise', 'symblog']) // получить референсы на тэги например,
                                                              ]);
                                                              
                                                              $blogRepository->add($blog); // никаких entity manager-ов.
                                                              

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

                                                                +1
                                                                Fesor Спасибо, очень познавательно. С удовольствием бы прочитал серию статей на тему best practice по Симфони и Доктрине от вас.
                                                                  0
                                                                  я бы даже добавил: о реальных best practice, а не о тех которые на оф сайте размещены, слишком упрощены и направлены на популяризацию фреймворка. местами там полный бред в примерах.

                                                                  Я согласен с тем, что мануалы нужны для того чтобы учиться, но мануалы и best practice это разные вещи, imho
                                                                    0
                                                                    местами там полный бред в примерах.


                                                                    А можно чуть подробнее?
                                                                      0
                                                                      Ну возможно словом «бред» я высказался слишком резко, просто меня очень расстроил тот факт, что они в best practices специально упрощают реализацию поставленных задач, те же формы которые создаются прямо в контроллерах. По моему мнению, best practices нужны именно тогда когда ты уже примерно понимаешь как готовить фреймворк, а best practices должны показывать как это делать красиво и правильно, а в тех доках что я успел прочитать получается, что авторы задачу решают красиво и правильно, а то что напрямую к вопросу задачи не относится — там код уже идет по проще и не всегда красивый.

                                                                      А еще вот есть видео https://youtu.be/Fu9j7w2hbW8?t=49 в тему
                                                                        0

                                                                        А, вы о симфони best practice, там да, очень много упрощений.

                                                                          0

                                                                          У меня в этом плане проще намного. Я не использую твиг (почти), не использую формы (вообще)… симфони валидатор натравливаю на запросы, и сейчас вообще планирую выкинуть потому что он ужесно неудобен для валидации динамических структур данных (либо надо просто написать нормальный валидатор вместо Collection, но это я еще думать буду).


                                                                          То есть сами понимаете что уже добрых две трети "бест практис" я могу выкинуть. Использую только app bundle, и то только потому что так рекомендовано, мне же нравится идея и AppBundle убрать оставив только кернел.


                                                                          По доктрине же все бэст практис от разработчиков доктрины вполне себе достойны внимания.

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


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


                                                                              Вы не должны напрямую использовать сущности. У вас должен быть ваш наследник что бы обезапасить себя хоть немного от изменений в бандле. Хотя лично мое мнение — я категорически против использования сущностей предоставляемых бандлами (за редкими исключениями). Мне больше нравятся embeddable объекты. Их проще поддерживать.
                                                                                0
                                                                                понял, поэтому и не нашел видимо примеров. Спасибо за ответ.
                                                                                0
                                                                                В симфони к бандлам не принято делать миграции если что-то должно храниться в базе?

                                                                                Не принято. Миграции относятся к глобальному уровню приложения, а не бандла. Их генерируют после установки/обновления бандла.
                                                                        0

                                                                        ну по поводу fromArray — это еще не факт что это самый удобный способ. Есть на самом деле куча вариантов (билдеры например), но суть у них все та же — не должно быть промежуточного состояния у самой сущности. И все что можно делать внутри (то есть хватает данных) лучше делать внутри. И смак весь в том, что все это никакого отношения к доктрине не имеет.


                                                                        Бэст практис по симфони от меня смысла не имеет слушать поскольку я использую очень урезанный вариант оного и сильно кастомизированный под себя. По доктрине — почти все есть у ocramius-а в его докладе на эту тему. Я практически полностью согласен с его тезисами и у него как одного из разработчиков доктрины намного больше опыта что бы подобное вещать)

                                                      0
                                                      Как раз на больших проектах сеттеры усложняют жизнь. В маленьких, «одноразовых» можно себе позволить держать в голове правила типа «не вызывать setPassword с plain text аргументами», но в больших в лучшем случае много времени будет тратиться на передачу подобных правил другим членам команды. А обычно даже сам начинаешь забываешь правила типа «эти два сеттера всегда нужно вызывать вместе, причём в строгой последовательности», а потом тратишь кучу времени на локализацию плавающих багов.
                                                      0
                                                      А почему не используется ParamConverter для преобразования id в объект?
                                                        +2
                                                        Мелкие замечания:
                                                        — protected свойства по умолчанию — нарушение инкапсуляции
                                                        — protected $comments = array(); — лучше в конструкторе держать

                                                        Расширение Doctrine Fixtures не поставляется с Symfony2, мы должны вручную его установить. К счастью это простая задача. Откройте файл composer.json расположенный в корне проекта и вставьте следующее:

                                                        К счатью есть ещё более простое решение:
                                                        composer require doctrine/doctrine-fixtures-bundle
                                                        

                                                        А даже если нравиться править compose.json руками, то после него нужно вызывать composer install, чтобы только установить новые/измененные пакеты, а не проводить вдобавок к установке ещё и глобальное обновление.
                                                            public function __construct()
                                                            {
                                                                $this->setCreated(new \DateTime());
                                                                $this->setUpdated(new \DateTime());
                                                            }
                                                        

                                                        Плохая практика создавать два разных инстанса \DateTime, когда их значение должно быть одинаковым. Как минимум:
                                                            public function __construct()
                                                            {
                                                                $this->setCreated(new \DateTime());
                                                                $this->setUpdated(clone $this->getCreated());
                                                            }
                                                        
                                                          0
                                                          Так же плохая практика использовать DateTime, лучше использовать DateTimeImmutable
                                                            0
                                                            Да, забыл как-то, что это новый проект, вероятно исключительно под новые версии PHP
                                                              0

                                                              5.5 уже далеко не новая версия. Но вообще с DateTimeImmutable есть свои нюансы в контексте доктрины.

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

                                                        Самое читаемое