Обзор
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/
Всем спасибо за внимание и замечания сделанные по проекту, если у вас возникли сложности или вопросы, отписывайтесь в комментарии или личные сообщения, добавляйтесь в друзья.
Часть 1 — Конфигурация Symfony2 и шаблонов
Часть 2 — Страница с контактной информацией: валидаторы, формы и электронная почта
Часть 4 — Модель комментариев, Репозиторий и Миграции Doctrine 2
Часть 5 — Twig расширения, Боковая панель(sidebar) и Assetic
Также, если Вам понравилось руководство вы можете поставить звезду репозиторию проекта или подписаться. Спасибо.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (45)
siomkin
03.06.2016 10:37+1Что мешает сделать так, не думаю что сеттеры вам особо усложнят жизнь, и на больших проектах наоборот могут помочь.
function changePassword(string $password, callable $hasher) : void { $this->setPassword($hash($password)); // мы никогда не забудем захэшировать пароль }
Fesor
03.06.2016 10:441) сеттер будет приватным методом в этом случае, поскольку мы не хотим давать внешнему миру им пользоваться
2) у вас и так есть доступ к состоянию объекта и этот метод вполне себе способен его менять. Дополнительные методы не нужны.
3) на больших проектах с сеттерами, сущности доктрины превращаются в тупое хранилище данных, а вся бизнес логика вытекает в лучшем случае в сервисный слой. И в итоге толку тогда от доктрины? Да и даже ORM в этом случае нам вообще не нужен.siomkin
03.06.2016 10:51Это уже частные случаи, мы сейчас все же говорим относительно статьи.
В большинстве случае entity доктрины генерируются из бд через консоль автоматически и мы уже получаем класс с сеттерами, а уже все действия над ним лучше вынести в репозиторий или модель. На сколько они нужны, не нужны — вопрос второй, но даже в IDE вам будет проще с сеттерами с автокомплитом.Fesor
03.06.2016 10:59В большинстве случае entity доктрины генерируются из бд через консоль автоматически
Вот честно за 5 лет ни разу так не делал. Более того, разработчики доктрины не рекомендуют так делать.
Использование доктрины подразумевает что схема базы данных генерируется из сущностей, а не наоборот (хотя так и можно просто сложно и теряется профит). То есть мы сначала проектируем сущности (бизнес-объекты) со своей бизнес логикой, моделируем предметную область а уже потом занимаемся второстепенными вещами вроде "схему базы генерим".
То есть если мы работаем с доктриной и представляем сущности как тупую проэкцию рядов таблиц на объекты — вам не нужна доктрина. Вам хватит какого-нибудь active record.
а уже все действия над ним лучше вынести в репозиторий или модель.
Потому что у вас в сущностях сеттеры, а не потому что это просто лучше) Есть такой шаблон проектирования (или скорее даже принцип) под названием "информационный эксперт": информация должна обрабатываться там, где она есть. То есть в нашем случае пока бизнес логика находится в рамках одной сущности или мы можем легко выделить корень агрегата сущностей — лучше эту логику пихать в сущность. А уж если мы не можем выделить корень агрегата (например надо посчитать сумму всех заказов пользователя и сущность пользователя ничего не знает о заказах) то тогда да, выносим это дело в сервис. Но подсчет суммы одного заказа — дело самого заказа.
siomkin
03.06.2016 11:16этот спор из той же области, где выполнять больше действий в моделе или контроллере.
Вы можете выполнять действия в классе Entity или можете вынести эти функции в модель или репозиторий, где у вас будет тот же самый объект entity. Здесь скорее вопрос кому как нравится.Fesor
03.06.2016 11:55этот спор из той же области, где выполнять больше действий в моделе или контроллере.
тут нет никакого спора. Когда у вас больше действий в контроллере, у вас нет разделения бизнес логики и презентационной логики, и называется это SmartUI. Вполне себе годный вариант для CRUD-а например, и покрывает он подалвяющее большинство проектов. Это не плохо, просто на более сложных проектах с более сложной логикой вы проиграете. В лучшем случае все закончится нарушением DRY. В худшем — связанность системы будет настолько высока, что сопровождать проект будет болью (как для разработчика, так и для бизнеса)
Вы можете выполнять действия в классе Entity или можете вынести эти функции в модель или репозиторий
Просто приведите формулировку термина "модель". Что это у вас? Модель предметной области? Какая-то другая модель?
Здесь скорее вопрос кому как нравится.
Ну да, и принципы всякие вроде SOLID или GRASP придумали от скуки.
siomkin
03.06.2016 13:09Вы понимаете что я могу ответить также что геттеры и сеттеры придумали не от скуки. Не важно нужны они вам или нет, это не значит что их, относительно данной статьи, не должно быть у других, и что это не правильный подход.
А на счет модели я думал вы знает для чего их придумали. В doctrine просто работу с данными можно вынести в repository, а так в модели.Fesor
03.06.2016 14:36+1Вы понимаете что я могу ответить также что геттеры и сеттеры придумали не от скуки.
Геттеры придумали в java что бы иметь возможность получать состояние. С ними все хорошо поскольку они не мутируют состояние объектов.
Сеттеры придумали потому что нужно было дать возможность извне менять состояние объектов. При этом мы нарушаем инкапсуляцию но это все же лучше чем публичные свойства и никакого контроля.
Не важно нужны они вам или нет, это не значит что их, относительно данной статьи, не должно быть у других, и что это не правильный подход.
Культ карго. Люди будут делать так как показали вне зависимости от размеров проекта. Без какого либо понимания что они делают и зачем.
А на счет модели я думал вы знает для чего их придумали.
Понимаете ли, это не имеет значения. Меня интересует ваша интерпритация что бы мы говорили на одном языке. Ибо для меня модель — это модель. А сущность — представляет собой модель бизнес объекта, одного. Сервис представляет модель взаимодействия нескольких несвязанных между собой сущностей (ибо если они связаны они сами разберутся).
В doctrine просто работу с данными можно вынести в repository, а так в модели.
Репозитории это вещи, которые отвечают за хранение. В них может находиться бизнес ограничения вроде "можем ли мы ложить туда эту сущность или нет". Но репозитории НЕ мутируют состояние сущностей. Это просто склад. Склад который меняет состояние объектов там хранящихся — плохой склад.
siomkin
03.06.2016 15:49Ладно, все это хорошо, вы тогда нам поведайте как лучше в 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);
oxidmod
03.06.2016 16:00$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);
siomkin
03.06.2016 16:16ну вот видите, сложно сказать что ваш вариант в данном случае лучше,
я же по памяти не помню какие у меня поля, а смотреть в описании к Blog не очень удобно.
Я это просто к тому, что все хорошо при определённых условиях — в статье к созданию блога сеттеры вполне уместны, да и не только.VolCh
06.06.2016 06:56+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'] );
?
Его плюсы:
— не забудем проинициализировать обязательные свойства
— внутреннюю логику объекта типа установки времени создания/обновления инкапсулируем в объекте
— упрощаем объект, как минимум упрощая его публичный интерфейс, убирая из него геттеры
Его минусы:
— если будут другие варианты создания, то надо будет думать, как их элегантно совместить
Fesor
03.06.2016 16:32Вот только ArrayCollection вне сущностей создаваться не должен. Они живут только внутри сущностей, наружу тоже не вылазят.
Fesor
03.06.2016 16:51И да, такие поля как
createdAt
,updatedAt
и т.д. создаются в конструкторе и в методах где действие и происходит. Именно в этом профит делать все операции по изменению состояния через один вызов какого-то метода. Вы всегда знаете когда действие началось и когда завершилось. И можете делать много интересных вещей внутри. Рефакторить не нашурая обратной совместимости и минимизируя возможность регрессиий....
Словом инкапсуляция.
Fesor
03.06.2016 16:35$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
. Ну и в целом фикстуры отстой, фабрики фикстур рулят.
VolCh
06.06.2016 06:47Как раз на больших проектах сеттеры усложняют жизнь. В маленьких, «одноразовых» можно себе позволить держать в голове правила типа «не вызывать setPassword с plain text аргументами», но в больших в лучшем случае много времени будет тратиться на передачу подобных правил другим членам команды. А обычно даже сам начинаешь забываешь правила типа «эти два сеттера всегда нужно вызывать вместе, причём в строгой последовательности», а потом тратишь кучу времени на локализацию плавающих багов.
VolCh
06.06.2016 07:15+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()); }
Fedot
06.06.2016 22:43Так же плохая практика использовать DateTime, лучше использовать DateTimeImmutable
Fesor
Сеттеры в сущностях… репозитории не как сервисы… хотя для бложиков ок.
padlyuck
А куда сеттеры нужно убрать? И какая выгода будет от репозиториев как от сервисов? В теме symfony совсем новичек, так что извините если вопросы глупые.
oxidmod
имхо, по поводу сеттеров имеется ввиду, что все данные задаются через конструктор и сеттерами не меняются.
IncorrecTSW
В разрезе работы с доктриной, сущности таки меняются и имеют сеттеры. Это вопрос уже не к автору статьи, а скорей к доктрине.
Fesor
Авторы доктрины говорят что в сущностях должны быть методы которые нужны в рамках бизнес логики, а тупые сеттеры это так себе. Пруф
VolCh
Доктрина создаёт/меняет сущности без использования конструкторов и сеттеров. Они ей не нужны, она их не вызывает.
maximkou
Да собственно такая же, как и от любых сервисов — слабая связанность c вытекающими.
padlyuck
понял, спасибо.
conio
Репозитории-сервисы легче инжектировать в другие сервисы, ну и зависимости указывать у самих репо. Другой вопрос, что это не всегда необходимо.
Fesor
зависит от подходов. Я никогда не наследуюсь от доктриновских репозиториев и всегда делаю свои сервисы, в которые внедряется entity manager. А что бы было удобно — autowire все решает.
Fesor
Для начала давайте различать сеттеры, которые сгенерированы вашей IDE или симфоневским генератором, и сеттеры которые реально нужны.
Вот вам простой пример. У вас есть требование "пользователь должен иметь возможность сменить пароль" и у вас появляется метод:
А если в рамках требований нужно сменить сразу 4 свойства — мы либо делаем метод с 4-мя аргументами, либо, что вероятнее, уберем эти 4 свойства в отдельный объект-значение (гуглить doctrine embeddable), и будем просто заменять оный.
И в итоге различие будет только в том, что эти "сеттеры" будут иметь адекватные названия, соответствующие поведению и бизнес логике. И мы всегда будем иметь полный контроль за состоянием, сущности никогда не будут иметь частичного состояния и "валидация" будет происходить в рамках бизнес ограничений.
Доктриновцы сильно расстраиваются когда в сущностях появляются сеттеры, а в документации к симфони они используются в основном из-за форм, что бы не нагружать людей лишними знаниями. Ну мол что на самом деле формы должны работать с DTO и сущностей не касаться. Но на маленьких проектах на это все можно забить в угоду скорости разработки.
padlyuck
Спасибо за развернутый ответ. Согласен, что на маленьких проектах можно некоторыми правилами пренебречь, но когда изучаешь новый инструмент хочется учиться применять его правильно изначально, а потом уже когда появится понимание как и что нужно делать — тогда уже можно и «похамить» в коде в угоду той самой скорости разработки.
Fesor
Меня радует такой подход к изучению вещей) последовательный)
Суть в том что если вы хотите изучить доктрину — вам лучше не смотреть на ее использование в документации симфони. Так же чуть выше я дал ссылку на видео где преподносятся основные идеи когда и как использовать доктрину.
michael_vostrikov
А если надо сразу все свойства изменить? Например, менеджер открыл заявку, позвонил пользователю, уточнил информацию, поменял некоторые поля и нажал «Сохранить». Как правильно сделать рендеринг формы с данными, прием запроса, валидацию и сохранение?
Fesor
Я сомневаюсь что вам нужно менять ID сущности или поля вроде даты создания оной. С другой стороны вместо вызова десятка сеттеров можно вызвать один метод с комком данных а там уже внутри его разбирать. Или, что еще интереснее — запихнуть эти данные в имутабельный объект и заменять целиком.
формы не должны использовать сущности напрямую (по хорошему, конечно можно идти на компромис), и мы должны иметь какие-то промежуточные объекты, DTO. Их мы можем валидировать, в них можем записывать чушь. А сущности всегда должны быть валидны и не должны иметь возможности вызвать какой-то метод и сделать ее состояние невалидным.
siomkin
зачем путать людей и называть сеттерами, то что таковым не является, это уже не сеттер, а обычная функция. Другое дело куда вы ее запихнёте.
Fesor
В том то и дело. Сеттеры превращаются в обычные методы. Просто поведение внутри сущности, которое принимает решение менять состояние объекта или нет. Вывод — у нас нет сеттеров, хоть код этих методов часто и похож на обычные сеттеры.
VolCh
Это не обычная функция, а функция, единственным назначением которой является изменение состояния объекта. Тупые сеттеры (просто назначение свойству аргумента) — частный случай таких функций. Но если мы добавляем в тупой сеттер какой-то код (а именно для этого сеттеры и создаются, чтобы добавлять какой-то код в объект при изменении свойства), то чёткой грани между умным сеттером и обычной функцией нет.