Привязки из вендорного бандла к рабочему проекту Symfony2

в 13:59, , рубрики: bundle, php, symfony, метки:

Среди 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

Источник

Поделиться новостью

* - обязательные к заполнению поля