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

О Doctrine 2


Для начала небольшое вступление о Doctrine 2. Проект теперь разбит на три пакета:
  • Common
  • DBAL
  • ORM

Common — содержит общие компоненты, которые используются в других пакетах.
DBAL — слой абстракции доступа к БД.
ORM — инструменты объектно-реляционного отображения

В Doctrine 2 нет привычных классов *Table и *Record, которые были раньше. Это так же означает, что теперь классам моделей не надо наследовать классы *Record, а классам таблиц наследовать классы *Table из пакета Doctrine. Вместо этого вводятся два новых понятия «Entity» и «EntityManager». «Entity» — это, грубо говоря, объект модели, а «EntityManager» — это соответственно класс позволяющий управлять объектами «Entity». Интересно то, что «Entity» может быть любой PHP класс и он не должен наследовать никаких классов из пакета Doctrine.

В Doctrine 1 модели описывались в Yaml файле, теперь же для этого можно использовать Yaml, XML или Docblock Annotations. Docblock annotations в том или ином виде приходилось наверное использовать всем, например для описания методов под PHPDocumentor. Так вот теперь так же можно описывать метаданные persistent объектов.
Пример:
Copy Source | Copy HTML
  1. <?php
  2.  
  3. /**<br/> * @Entity<br/> * @Table(name="my_model")<br/> */
  4. class MyModel
  5. {
  6.     /**<br/>     * @Id @Column(name="id", type="integer")<br/>     * @GeneratedValue(strategy="AUTO")<br/>     */
  7.     private $id;
  8.  
  9.     /**<br/>     * @Column(name="title", type="string", length=255)<br/>     */
  10.     private $title;
  11. }


Ну и конечно же обо всем этом и многом другом можно прочитать на сайте Doctrine

Взлетаем


Я решил рассмотреть использование Doctrine 2 на примере очень маленького и очень простого приложения. Это будет сервис Readlater, позволяющий сохранять ссылки с целью отложить чтение чего-либо на потом. То есть вся его функиональность сводится к трем простым действиям:
  • вывести список ссылок
  • добавить ссылку
  • удалить ссылку

Но я сразу же столкнулся с небольшой проблемой — и Doctrine 2 и Symfony 2 сейчас активно разрабатываются и ни о какой стабильноти и речи не идет. Более того все очень часто меняется и то что работает сегодня может не взлететь завтра. Зачастую это различные мелочи, вроде переноса кода в другую директорию. Поэтому для данного приложения я решил заморозить версии используемых библиотек. Для этого я взял:

Собрав это все воедино я заморозил все версии на 18 апреля 2010 года в том виде в котором они были и вместе со своим приложением выложил в общий доступ на github. Так что если интересно, то дерзайте.

Полетели


Итак, первое кратко о структуре. Примерно такой виде директорий:
  • readlater: — директория приложения
  • src
    • Application: — здесь будет лежать «бандл» ReadlaterBundle и в общем кодить будем там же.
    • Bundle: — в данном случае тут пусто
    • vendor: — здесь фреймворк и все библиотеки

Начнем с конфигов. В readlater/config/config.yml добавим:
Copy Source | Copy HTML
  1. kernel.config: ~
  2. web.web: ~
  3. web.templating: ~
  4. doctrine.dbal:
  5.   default_connection: default
  6.   connections:
  7.     default:
  8.       driver: PDOMySql
  9.       dbname: sfbox2
  10.       user: root
  11.       password: 123
  12.       host: localhost
  13.       event_manager_class: Doctrine\Common\EventManager
  14.       configuration_class: Doctrine\DBAL\Configuration
  15.  
  16. doctrine.orm:
  17.   default_entity_manager: default
  18.   cache_driver: array
  19.   entity_managers:
  20.     default:
  21.       connection: default

Я описал соединение с базой данных doctrine.dbal и настройки для пакета Doctrine\ORM doctrine.orm
Я не буду здесь описывать создание ядра приложения, так как оно практически не отличается от того что идет в symfony-sandbox.
Поэтому я перейду к ReadlaterBundle, который помещу в src/Application. Структура бандла такова:
  • Controller: — здесь контроллеры
  • Entities: — здесь те самые Entity, классы моделей
  • Resources
    • config: — здесь настройки
    • views: — здесь шаблоны

В директории Entities я создам единственный класс Link, потому что больше и не надо. Doctrine 2 по умолчанию предлагает создавать Entity в директории Entities бандла, то есть каждый бандл может иметь свой набор классов моделей. Путем настроек можно попросить Doctrine искать классы в отличной от Entities директории, но я этого делать не буду.
Итак приведу код класса Link:
Copy Source | Copy HTML
  1. <?php
  2.  
  3. namespace Application\ReadlaterBundle\Entities;
  4. use Doctrine\ORM\EntityRepository;
  5.  
  6. /**<br/> * @Entity<br/> * @Table(name="readlater_link")<br/> */
  7. class Link
  8. {
  9.   /**<br/>   * @Id @Column(name="id", type="integer")<br/>   * @GeneratedValue(strategy="AUTO")<br/>   */
  10.   private $id;
  11.  
  12.   /**<br/>   * @Column(name="url", type="string", length=255)<br/>   */
  13.   private $url;
  14.  
  15.   /**<br/>   * @Column(name="created_at", type="datetime")<br/>   */
  16.   private $createdAt;
  17.  
  18.   /**<br/>   * constructor<br/>   */
  19.   public function __construct()
  20.   {
  21.     $this->createdAt = new \DateTime();
  22.   }
  23.  
  24.   /**<br/>   * to string<br/>   */
  25.   public function __toString()
  26.   {
  27.     return $this->getUrl();
  28.   }
  29. }
  30.  
  31.  

Я определил у класса три поля:
  • $id — индентификатор
  • $url — URL ссылки
  • $createdAt — дата добавления

А так же добавил два метода, которые и так понятны. Как видно из примера для описания модели я воспользовался Docblock.
Теперь выполнил несколько команд:

readlater/console doctrine:database:create
readlater/console doctrine:generate:entities
readlater/console doctrine:schema:create

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



Теперь если заглянем в наш класс Link то увидим, что он немного преобразился, а имеено теперь он выглядит так:
Copy Source | Copy HTML
  1. <?php
  2.  
  3. namespace Application\ReadlaterBundle\Entities;
  4. use Doctrine\ORM\EntityRepository;
  5.  
  6. /**<br/> * @Entity<br/> * @Table(name="readlater_link")<br/> */
  7. class Link
  8. {
  9.   /**<br/>   * @Id @Column(name="id", type="integer")<br/>   * @GeneratedValue(strategy="AUTO")<br/>   */
  10.   private $id;
  11.  
  12.   /**<br/>   * @Column(name="url", type="string", length=255)<br/>   */
  13.   private $url;
  14.  
  15.   /**<br/>   * @Column(name="created_at", type="datetime")<br/>   */
  16.   private $createdAt;
  17.  
  18.   /**<br/>   * constructor<br/>   */
  19.   public function __construct()
  20.   {
  21.     $this->createdAt = new \DateTime();
  22.   }
  23.  
  24.   /**<br/>   * to string<br/>   */
  25.   public function __toString()
  26.   {
  27.     return $this->getUrl();
  28.   }
  29.   /**<br/>   * Get id<br/>   *<br/>   * @return integer $id<br/>   */
  30.   public function getId()
  31.   {
  32.     return $this->id;
  33.   }
  34.  
  35.   /**<br/>   * Set url<br/>   *<br/>   * @param string $url<br/>   */
  36.   public function setUrl($url)
  37.   {
  38.     $this->url = $url;
  39.   }
  40.  
  41.   /**<br/>   * Get url<br/>   *<br/>   * @return string $url<br/>   */
  42.   public function getUrl()
  43.   {
  44.     return $this->url;
  45.   }
  46.  
  47.   /**<br/>   * Set createdAt<br/>   *<br/>   * @param datetime $createdAt<br/>   */
  48.   public function setCreatedAt($createdAt)
  49.   {
  50.     $this->createdAt = $createdAt;
  51.   }
  52.  
  53.   /**<br/>   * Get createdAt<br/>   *<br/>   * @return datetime $createdAt<br/>   */
  54.   public function getCreatedAt()
  55.   {
  56.     return $this->createdAt;
  57.   }
  58.  
  59. }
  60.  
  61.  

В нем появились методы для доступа к свойствам класса.

Что ж теперь добавим правила маршрутизации в routing.yml
Copy Source | Copy HTML
  1. link_list:
  2.   pattern: /
  3.   defaults: { _bundle: ReadlaterBundle, _controller: Readlater, _action: index }
  4.  
  5. add_link:
  6.   pattern: /add
  7.   defaults: { _bundle: ReadlaterBundle, _controller: Readlater, _action: add }
  8.  
  9. read_link:
  10.   pattern: /read/:id
  11.   defaults: { _bundle: ReadlaterBundle, _controller: Readlater, _action: read }

Из кода думаю все понятно, есть 3 правила, вывод списка, добавление и удаление.

Теперь наконец контроллер, в котором можно и посмотреть как работать с объектами под управлением Doctrine. Сразу привожу полностью весь код:
Copy Source | Copy HTML
  1. <?php
  2.  
  3. namespace Application\ReadlaterBundle\Controller;
  4.  
  5. use Symfony\Framework\WebBundle\Controller,
  6.   Application\ReadlaterBundle\Entities\Link,
  7.   Doctrine\ORM\QueryBuilder;
  8.  
  9. class ReadlaterController extends Controller
  10. {
  11.   public function indexAction()
  12.   {
  13.     $em = $this->container->getDoctrine_ORM_EntityManagerService();
  14.  
  15.     $links = $em->getRepository('Application\\ReadlaterBundle\Entities\Link')->findAll();
  16.     return $this->render('ReadlaterBundle:Readlater:index', array('links' => $links));
  17.   }
  18.  
  19.   public function addAction()
  20.   {
  21.     $request = $this->getRequest();
  22.  
  23.     $url = $request->request->get('link');
  24.     $link = new Link();
  25.     $link->setUrl($url);
  26.  
  27.     $em = $this->container->getDoctrine_ORM_EntityManagerService();
  28.     $em->persist($link);
  29.     $em->flush();
  30.  
  31.     return $this->redirect($this->generateUrl('link_list'));
  32.   }
  33.  
  34.   public function readAction($id)
  35.   {
  36.     $em = $this->container->getDoctrine_ORM_EntityManagerService();
  37.  
  38.     $link = $em->find('Application\\ReadlaterBundle\\Entities\\Link', $id);
  39.     $em->remove($link);
  40.  
  41.     $em->flush();
  42.  
  43.     return $this->redirect($this->generateUrl('link_list'));
  44.   }
  45. }

Пару слов о коде.
$em = $this->container->getDoctrine_ORM_EntityManagerService(); таким образом используя DI Container мы получаем доступ к EntityManager
$em->getRepository() — так мы получаем хранилище объектов определенного типа, то есть некий аналог *Table из Doctrine 1.x.
$em->persist($link); — так сохраняем объект
$em->flush() так завершаем транзакцию, то есть реально отправляем данные в БД.
$em->remove($link) — ну а так удалем объект.
Кстати если бы я унаследовал свой контроллер от Symfony\Framework\DoctrineBundle\Controller\DoctrineController, то были бы доступны методы:
  • getDatabaseConnection — возвращает объект соединения с БД
  • getEntityManager — и мне бы не пришлось его получать из DI Container'a
  • createQueryBuilder
  • createQuery

Вот собственно и все о коде, остальное думаю очевидно.

Но возможно возник вопрос, если теперь нет классов таблиц, то где создавать методы извлечения модельных объектов. Например в данном случае хорошо бы чтоб метод findAll получа�� список объектов отсортированных по дате добавления по убыванию. Для этого необходимо создать свой класс репозиторий хранения для объектов определенного типа.
Для этого откроем наш класс Link и в конец файла допишем:
Copy Source | Copy HTML
  1. /**<br/> * LinkRepository<br/> */
  2. class LinkRepository extends EntityRepository
  3. {
  4.   public function findAll()
  5.   {
  6.     return $this->_em->createQuery('SELECT l FROM Application\\ReadlaterBundle\\Entities\\Link l ORDER BY l.createdAt DESC')->getResult();
  7.   }
  8. }

это и будет репозиторий.
Теперь скажем какой репозиторий использовать для класса Link, для этого немного изменим Docblock у класс Link
Copy Source | Copy HTML
  1. /**<br/> * @Entity(repositoryClass="Application\ReadlaterBundle\Entities\LinkRepository")<br/> * @Table(name="readlater_link")<br/> */
  2. class Link
  3. {
  4.   // ...
  5. }

Вот в общем и все. Теперь список ссылок будет сортироваться в нужном порядке и выглядеть это будет примерно так:


Код шаблонов я не стал приводить, но все это вместе можно посмотреть в исходниках на github
а так же ссылки по теме: