Среди 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'] != 'UndeleteLikesBundleEntityLike')) {
throw new NoLikeAssociationException(
sprintf('Association with like entity not found in entity %s', get_class($entity))
);
}
}
Динамическое создание привязки
В симфони не существует класса для пользователей и в каждом проекте может быть свой класс. Но нам нужно учитывать, какие пользователи ставили лайки. Поэтому мы используем динамическое создание связи в БД через доктрину для уже существующего поля:
namespace UndeleteLikesBundleMapping;
use DoctrineORMEventLoadClassMetadataEventArgs;
use DoctrineORMMappingClassMetadataInfo;
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() == 'UndeleteLikesBundleEntityLike') {
$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
Автор: UnDelete