[Из песочницы] Привязки из вендорного бандла к рабочему проекту Symfony2
Среди php-разработчиков последнее время все сильнее набирает популярность Symfony2. Этот фреймворк позволяет использовать любые модули (в симфони они называются бандлы) для создания базовых фич проекта. По сути стандартная поставка симфони и является набором модулей. Но что если у вас несколько проектов, и вам необходим одинаковый набор функций на них, но подходящего модуля среди открытых нет? Не беда, можно написать свой.По поводу создания бандла на Хабре есть статья «Создание собственного вендорного бандла в Symfony2», в которой описаны базовые моменты. В своей статье я хотел бы рассказать о некоторых методах работы из внешнего бандла с проектом, на которой он устанавливается. Предложенные мной решения буду показывать на основе своего бандла лайков.Связь внешних энтитиВ этой части мы попробуем определить возможные механизмы для работы с энтити из проекта внутри нашего бандла.Интерфейсы Интерфейсы позволяют описать, какими методами должен обладать класс, и мы можем проверять, удовлетворяет ли энтити данному интерфейсу или нет. В случае бандла лайков мы создаем интерфейс, описывающий методы добавления лайка, удаления лайка и получение лайков. Эти методы позволят нам работать с лайками независимо от энтити, к которой они привязаны, главное, чтобы соответствовал интерфейс. interface LikeableInterface { public function getId (); public function addLike (Like $like); public function removeLike (Like $like); public function getLikes (); } Мапинг Реализация интерфейса еще не гарантирует, что пользователь нашего бандла реализовал ту связь в таблице, которая нам нужна. Но это мы можем проверить с помощью doctrine и metadata. Метадата хранит в себе информацию о всех связях между объектами, воспользуемся ей: class LikeHelper { /* @var EntityManager */ private $em;
protected function checkAssociation (LikeableInterface $entity) { $metadata = $this→em→getClassMetadata (get_class ($entity)); $mapping = false;
if ($metadata→hasAssociation ('likes')) { $mapping = $metadata→getAssociationMapping ('likes'); }
if (!$mapping || ($mapping['targetEntity'] != 'Undelete\LikesBundle\Entity\Like')) { throw new NoLikeAssociationException ( sprintf ('Association with like entity not found in entity %s', get_class ($entity)) ); } } Динамическое создание привязки В симфони не существует класса для пользователей и в каждом проекте может быть свой класс. Но нам нужно учитывать, какие пользователи ставили лайки. Поэтому мы используем динамическое создание связи в БД через доктрину для уже существующего поля: namespace Undelete\LikesBundle\Mapping;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs; use Doctrine\ORM\Mapping\ClassMetadataInfo;
class Like { private $userClass;
public function __construct ($userClass) { $this→userClass = $userClass; }
public function loadClassMetadata (LoadClassMetadataEventArgs $eventArgs) { /* @var $metadata ClassMetadataInfo */ $metadata = $eventArgs→getClassMetadata ();
if ($metadata→getName () == 'Undelete\LikesBundle\Entity\Like') { $metadata→mapManyToOne ([ 'targetEntity' => $this→userClass, 'fieldName' => 'user', ]); } } } Обратная связь (event dispatching) Так же был необходим механизм, который позволял бы отслеживать установку лайка. Для этого мы будем использовать тэгированые сервисы. Всё, что нужно сделать нашему бандлу — это пройтись по контейнеру и записать помеченные сервисы, которые надо вызывать при действиях с лайками. class LikePass implements CompilerPassInterface { public function process (ContainerBuilder $container) { $definition = $container→getDefinition ( 'undelete.likes.event.dispatcher' );
$taggedServices = $container→findTaggedServiceIds ( 'like_listener' );
foreach ($taggedServices as $id => $tags) { $onLike = isset ($tags[0]['onLike']) ? $tags[0]['onLike'] : false; $onLikeRemove = isset ($tags[0]['onLikeRemove']) ? $tags[0]['onLike'] : false;
$definition→addMethodCall ( 'addListener', array (new Reference ($id), $onLike, $onLikeRemove) ); } } } Для работы с этими сервисами сделаем небольшой диспетчер: class LikeEventDispatcher { private $listeners = [];
public function addListener ($service, $onLike, $onLikeRemove) { $this→listeners[] = [ 'service' => $service, 'onLike' => $onLike, 'onLikeRemove' => $onLikeRemove, ]; }
public function dispatchEvent ($kind, LikeEvent $event) { foreach ($this→listeners as $listener) { $method = false;
if ($kind == LikeEvent: ON_LIKE) { $method = $listener['onLike']; } elseif ($kind == LikeEvent: ON_LIKE_REMOVE) { $method = $listener['onLikeRemove']; }
if ($method) { $listener['service']→$method ($event); } } } } Front end Помимо какой-то серверной логики на внешний проект иногда приходится отдавать и файлы для браузера (стили, картинки и javascript). Эти файлы мы храним в папке Resource/public. В симфони есть assets для подключения файлов из бандла. Собственно, его (assets: install) и используем чтобы файлы были доступны в публичной папке.Для некоторых проектов мы используем assetic как более гибкое решение. Но здесь приходиться мириться с тем, что js и css лежат в публичной части, но не используются.ЗЫ Надеюсь, что эти небольшие советы помогут вам при создании своих бандлов. Если у вас есть замечания, аргументы за или против этих вариантов — пишите их в комментариях, с радостью с вами побеседую.Бандл лайков можно найти здесь: github.com/UnDeleteRU/LikesBundle