Часто встречающейся задачей при разработке веб-приложений является создание интерфейсов, административного и пользовательского, для редактирования и отображения некоторых записей БД. Соответственно в административном интерфейсе должна быть возможность заносить, искать и редактировать записи, а в пользовательском интерфейсе — искать и просматривать записи.
При разработке проекта на основе фреймворка Symfony 2.1 для создания такого типового решения будем использовать:
1. Разработка модели данных — Doctrine ORM, интегрированная с фреймворком Symfony 2
2. Разработка административного интерфейса — Sonata Admin Bundle
2. Разработка пользовательского интерфейса — стандартные возможности Symfony 2 + IphpCoreBundle
Таблица БД
Допустим у нас уже есть некоторые данные (таблица БД), которую нужно перенести в проект на Symfony 2. В примере используем следующую структуру таблицы БД:
CREATE TABLE `link` ( `id` INT(10) NOT NULL AUTO_INCREMENT, `name` VARCHAR(200) NOT NULL DEFAULT '', `url` VARCHAR(100) NOT NULL DEFAULT '', `annotation` text NOT NULL, `created_at` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `updated_at` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`link_id`) ) ;
В таблице должно быть поле id, которое по умолчанию используется как уникальный идентификатор записи (первичный ключ). Если поле называется иначе — переименуйте его. Если в таблице есть поля с датой создания и обновления записи — из имена должны быть created_at и updated_at соответственно.
Создание доменной модели Doctrine ORM
Предполагаем что в проекте уже есть бандл, который называется MyDbBundle. Для того чтобы на основе таблицы создать xml-описание сущности и php-класс сущности выполним команды:
#php app/console doctrine:mapping:import MyDbBundle xml --filter=Link #php app/console doctrine:generate:entities MyDbBundle:Link
Первая команда создаст файл src/My/DbBundle/Resources/config/doctrine/Link.orm.xml вида
<?xml version="1.0" encoding="utf-8"?> <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> <entity name="My\DbBundle\Entity\Link" TABLE="link"> <id name="id" TYPE="integer" COLUMN="id"> <generator strategy="IDENTITY"/> </id> <field name="title" TYPE="string" COLUMN="title" LENGTH="200"/> <field name="url" TYPE="string" COLUMN="url" LENGTH="100"/> <field name="annotation" TYPE="text" COLUMN="annotation"/> <field name="createdAt" TYPE="datetime" COLUMN="created_at"/> <field name="updatedAt" TYPE="datetime" COLUMN="updated_at"/> </entity> </doctrine-mapping>
Добавление сущности symfony
Вторая на основе xml-описания сущности создает PHP класс сущности src/My/DbBundle/Entity/Link
<?php namespace My\DbBundle\Entity; class Link { /** * @var string $title */ private $title; /** * @var string $url */ private $url; /** * @var string $annotation */ private $annotation; /** * @var \DateTime $createdAt */ private $createdAt; /** * @var \DateTime $updatedAt */ private $updatedAt; /** * @var integer $id */ private $id; /** * Set title * * @param string $title * @return Link */ public function setTitle($title) { $this->title = $title; return $this; } /* ... Геттеры и сеттеры для cущности ... */ }
Xml-описание , PHP-класс и таблицу в БД также можно создать при помощи команды
doctrine:generate:entity
подробнее про эту команду можно посмотреть здесь
Админинистративный интерфейс с Sonata Admin Bundle
Установка и начало работы с Sonata Admin Bundle подробно описана в статье. В нашем случае нужно создать один административный класс и один сервис.
Административный класс
<?php # src/My/DbBundle/Admin/LinkAdmin.php namespace My\DbBundle\Admin; use Sonata\AdminBundle\Admin\Admin; use Sonata\AdminBundle\Form\FormMapper; use Sonata\AdminBundle\Datagrid\DatagridMapper; use Sonata\AdminBundle\Datagrid\ListMapper; use My\DbBundle\Entity\Link; class LinkAdmin extends Admin { protected function configureDatagridFilters(DatagridMapper $datagridMapper) { $datagridMapper->add('title') ->add('url'); } protected function configureListFields(ListMapper $listMapper) { $listMapper->addIdentifier('title') ->add('url') ->add('updatedAt'); } protected function configureFormFields(FormMapper $formMapper) { $formMapper->add('title') ->add('url') ->add('annotation'); } }
Описание сервиса
<!-- src/My/DbBundle/Resources/config/services.xml --> <?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="sonata.admin.my.db.link" class="My\DbBundle\Admin\LinkAdmin"> <tag name="sonata.admin" manager_type="orm" group="Базы данных" label="Ссылки"/> <argument/> <argument>My\DbBundle\Entity\Link</argument> <argument>SonataAdminBundle:CRUD</argument> </service> </services> </container>
После этого в административном меню Sonata Admin должен появится пункт «Базы данных» / «Ссылки» (если список пунктов административного меню формируются вручную — нужно добавить идентификатор сервиса sonata.admin.my.db.link в список отображаемых пунктов меню)
Для того чтобы названия служебных сообщений показывались на русском языке, нужно создать файл переводов
<!-- src/My/DbBundle/Resources/tranlations/messages.ru.xliff --> <?xml version="1.0"?> <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> <file source-language="ru" datatype="plaintext" original=""> <body> <trans-unit id="Link List"> <source>Link List</source> <target>Список ссылок</target> </trans-unit> <trans-unit id="Link Edit"> <source>Link Edit</source> <target>Редактирование ссылки</target> </trans-unit> <trans-unit id="Link Create"> <source>Link Create</source> <target>Занесение информации о ссылке</target> </trans-unit> <trans-unit id="Link Delete"> <source>Link Delete</source> <target>Удаление ссылки</target> </trans-unit> </body> </file> </xliff>
Автоматическое сохранение даты создания и обновления записи
Добавим в модель данных автоматическое заполнение даты создания и обновления записи. Для этого нужно модифицировать xml-описание сущности Doctrine ORM
#src/My/DbBundle/Resources/config/doctrine/Link.orm.xml
.. <field name="createdAt" type="datetime" column="created_at"> <gedmo:timestampable on="create"/> </field> <field name="updatedAt" type="datetime" column="updated_at"> <gedmo:timestampable on="update"/> </field> ...
В данном случае используются директивы библиотеки Doctrine Extensions, которые интегрируются в проект на Symfony 2 с помощью StofDoctrineExtensionsBundle. Для обновления даты применяется поведение Timestampable.
Для того, чтобы в административном интерфейсе отображались дата и время создание и обновления записи, изменим административный класс, унаследовав его от класса Iphp\CoreBundle\Admin\Admin:
<?php # src/My/DbBundle/Admin/LinkAdmin.php use Iphp\CoreBundle\Admin\Admin as IphpAdmin; .. class LinkAdmin extends IphpAdmin { ...
Класс Iphp\CoreBundle\Admin\Admin дополняет стандартный метод формирования меню кодом, который в случае наличия у редактируемой сущности методов getUpdatedAt и/или getCreatedAt добавляет в левое меню соответствующие записи. В результате на странице с формой редактирования записи в левом столбце отображаются дата и время создания и редактирования информации о ссылке.
Пользовательский интерфейс
При создании пользовательского интерфейса в данном примере используем возможности IphpCoreBundle: Модули и базовый EntityController.
Сначала нужно создать класс-модуль, который будет размещен в рубрике. В конструкторе класса указывается название модуля («Ссылки») и название сущности, которая будет отображаться на сайте (MyDbBundle:Link). Класс EntityModule по умолчанию заносит два маршрута — index и view для отображения списка записей и вывода одной записи.
<?php #src/My/DbBundle/Module/LinkModule.php namespace My\DbBundle\Module; use Iphp\CoreBundle\Module\EntityModule; class LinkModule extends EntityModule { function __construct() { $this->setName('Ссылки'); $this->setEntityName ('MyDbBundle:Link'); } }
Затем создаем контроллер, который будет выводить список ссылок и информацию о ссылке.
В качестве базового класса используем Iphp\CoreBundle\Controller\EntityController, в котором базовый контроллер Symfony дополнен методами, с помощью которых базовые операции (список, отображение) уже заложены в код класса.
<?php #src/My/DbBundle/Controller/LinkController.php namespace My\DbBundle\Controller; use Iphp\CoreBundle\Controller\EntityController; class LinkController extends EntityController { function __construct() { $this->entityName = 'MyDbBundle:Link'; } }
Шаблоны для пользовательского интерфейса
Далее нужно создать шаблоны для вывода списка записей и отображения информации о записи. В базовом классе EntityModule для методов indexAction и viewAction используется аннотация /* @template */ из бандла SensioFrameworkExtraBundle которая автоматически назначает имя шаблона для действия (action). В нашем случае нужно создать шаблоны
- src/My/DbBundle/Resources/views/Link/index.html.twig
- src/My/DbBundle/Resources/views/Link/view.html.twig
Шаблон списка записей
{# src/My/DbBundle/Resources/views/Link/index.html.twig #}
{% extends '::base.html.twig' %}
{% block body %}
{% for link in entities %}
<div>
<a target="blank" href="{{ link.url }}"><strong>{{ link.title }}</strong></a>
{{ link.annotation|raw }}
<a href="{{ entitypath(link)}}">Информация о ссылке </a>
</div>
{% endfor %}
{% endblock body %}В шаблоне списка записей используется добавленная бандлом IphpCoreBundle функция шаблонизатора Twig entitypath, которая выводит значение, возращаемое методом getSitePath объекта, передаваемого в качестве аргумента. Соответственно, чтобы в нашем шаблоне работала ссылка «Информация о ссылке», нужно добавить в класс Link метод getSitePath. Этот метод может возвращать путь на сайте, жестко определенный в коде класса, например:
... function getSitePath() { return '/links/'.$this->getId(); }
Однако, в нашем случае модуль LinkModule, выдающих список ссылок и информацию о ссылках, может быть размещен в любой рубрике сайта с любым адресом, и мы не можем жестко зашивать в коде класса путь к странице с информацией о ссылке. Поэтому мы будем использовать следующий код для метода Link::getSitePath:
<?php namespace My\DbBundle\Entity; class Link { ... public function getSitePath(\Iphp\CoreBundle\Routing\EntityRouter $entityRouter) { return $entityRouter->generateEntityActionPath ($this,'view'); } ... }
Объект EntityRouter на основании данных объекта сущности ($this) и названия действия («view») автоматически сгенерирует путь на сайте к текущему объекту. Данный подход работает только если при создании модуля используется класс EntityModule — в данном классе маршруты автоматически именуются так, чтобы их мог найти EntityRouter по объекту и названию действия. Путь на сайте генерируется на основе рубрики, в котором размещен модуль и идентификатора объекта.
Шаблон вывода информации о записи
{# src/My/DbBundle/Resources/views/Link/view.html.twig #}
{% extends '::base.html.twig' %}
{% block body %}
<h1>{{ entity.title }}</h1>
<div><a href="{{ entity.url }}">{{ entity.url }}</a></div>
<div>{{ entity.annotation | raw }}</div>
{% endblock body %}Наличие метода получения ссылки на объект и страницы с выводом информации об объекте позволяет при поиске объектов различного типа, например через ElasticSearch, в списке результатов давать ссылки на объекты, используя функцию twig entitypath.

Здравствуйте.
Где еще спросить не нашел (может плохо искал?), но возникла проблемка.
Например есть сущность Product. Это товар для запчасти автомобиля.
Продукт связан с сущностью Model, которая является моделью автомобиля.
При добавлении с этим проблем как бы нет. Обычный выпадающий список. Sonata с этим отлично справляется.
Но вот Model связана с сущностью марки автомобиля. И продукт связан с маркой только через модель.
Хотелось бы в форму добавления продукта добавить не только модель, но еще и указание марки, а список моделей формировался при изменении указанной марки. Либо подгружался аяксом, либо просто фильтровался и выводились модели только выбранной марки, это не важно…
Может есть уже готовые решения для этого? Если нет, то как можно лучше это решить?
С симфони всего пару дней работаю, много всего нового и интересного =)