BlogBundle для Symfony2

После реализации PortfolioBundle, я решил сделать простенький бандл для блога.

Вот что в нем реализовано на данный момент:

  • CRUD записей
  • Подключен редактор разметки markItUp!
  • Реализована подсветка исходного кода в тексте при помощи GeSHi
  • Для создания/редактирования тегов записи написан отдельный тип формы и преобразователь данных
  • Генерация RSS ленты записей при помощи \Zend\Feed (ZF2)
  • Модели и контроллеры покрыты тестами

Ниже я остановлюсь на некоторых моментах более детально.

Редактор разметки markItUp!

markItUp! я выбрал из-за того, что он реализован в виде плагина к jQuery, который у меня используется в качестве основного JS фреймворка. Также он довольно удобен в плане настройки.
Например чтобы добавить кнопку “More”, которая будет вставлять в код html комментарий <!–more–>, достаточно добавить такую строчку в конфиг set.js:

{name:'More', className:'mMore', key:'M', openWith:'\n<!--more-->\n' }

Добавление/редактировании тегов для записи

При создании/редактировании поста, для тегов выводится обычное текстовое поле в которое можно ввести теги разделенные запятыми. Очень не хотелось логику разбиения строки на теги и привязку тегов к записи выносить в контроллер/модель/форму. После некоторых раздумий я решил попробовать сделать свой DataTransformer по аналогии с EntitiesToArrayTransformer.

Забегая наперед покажу итоговый код формы добавления/редактирования поста:

<?php
 
namespace Stfalcon\Bundle\BlogBundle\Form;
 
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
 
/**
 * Post form
 */
class PostForm extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        // поля title/slug/text по умолчанию будут отрендерены как field
        $builder->add('title')
                ->add('slug')
                ->add('text')
        // для tags будет использоваться алиас tags, который привязан к классу TagsType
                ->add('tags', 'tags');
    }
 
    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'Stfalcon\Bundle\BlogBundle\Entity\Post',
        );
    }
}

Как видите здесь нет ничего лишнего 🙂

А все благодаря вот этому преобразователю данных, который выполняет всю грязную работу:

<?php
 
namespace Stfalcon\Bundle\BlogBundle\Bridge\Doctrine\Form\DataTransformer;
 
use Symfony\Component\Form\DataTransformerInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
 
class EntitiesToStringTransformer implements DataTransformerInterface
{
    protected $em;
 
    public function __construct($em)
    {
        $this->em = $em;
    }
 
    /**
     * Transforms tags entities into string (separated by comma)
     *
     * @param Collection|null $collection A collection of entities or NULL
     * @return string|null An string of tags or NULL
     */
    public function transform($collection)
    {
        if (null === $collection) {
            return null;
        }
 
        if (!($collection instanceof Collection)) {
            throw new UnexpectedTypeException($collection, 'Doctrine\Common\Collections\Collection');
        }
 
        // преобразовываем коллекцию сущности в массив значений
        $array = array();
        foreach ($collection as $entity) {
            array_push($array, $entity->getText());
        }
 
        // преобразовываем массив значений в строку
        return implode(', ', $array);
    }
 
    /**
     * Transforms string into tags entities
     *
     * @param string|null $data
     * @return Collection|null
     */
    public function reverseTransform($data)
    {
        $collection = new ArrayCollection();
 
        if ('' === $data || null === $data) {
            return $collection;
        }
 
        if (!is_string($data)) {
            throw new UnexpectedTypeException($data, 'string');
        }
 
        // разбиваем строку на массив значений
        $tags = explode(',', $data);
        // очищаем значения от лишних пробелов в начале и в конце строки
        foreach($tags as &$text) {
            $text = trim($text);
        }
        unset($text);
        // удаляем дубликаты
        $tags = array_unique($tags);
 
        foreach ($tags as $text) {
            // ищем сущность в репозитории
            $tag = $this->em->getRepository("StfalconBlogBundle:Tag")
                    ->findOneBy(array('text' => $text));
            // если её нет, тогда создаем новую
            if (!$tag) {
                $tag = new \Stfalcon\Bundle\BlogBundle\Entity\Tag($text);
                $this->em->persist($tag);
            }
            // добавляем сущность в коллекцию
            $collection->add($tag);
        }
 
        // метод setTags сущности Post получит в качестве аргумента коллекцию сущностей тегов
        // дальше Doctrine2 сама расставит нужные связи
        return $collection;
    }
 
}

Также пришлось сделать отдельный элемент TagsType в котором подключается этот DataTransformer:

<?php
 
namespace Stfalcon\Bundle\BlogBundle\Bridge\Doctrine\Form\Type;
 
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Bridge\Doctrine\RegistryInterface;
use Stfalcon\Bundle\BlogBundle\Bridge\Doctrine\Form\DataTransformer\EntitiesToStringTransformer;
 
class TagsType extends AbstractType
{
 
    protected $registry;
 
    public function __construct($registry)
    {
        $this->registry = $registry;
    }
 
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->prependClientTransformer(
                new EntitiesToStringTransformer($this->registry->getEntityManager()));
    }
 
    public function getParent(array $options)
    {
        // в качестве родителя подойдет простое поле
        return 'field';
    }
 
    public function getName()
    {
        // алиас, который используется при добавлении элементов в форму
        return 'tags';
    }
}

Подсветка кода при помощи GeSHi

Изначально планировал реализовать подсветку кода на JS, но потом отказался от этой затеи — когда кода много, то процесс подсветки довольно заметен.
PHP библиотека GeSHi дает возможность подсветить код на этапе генерации страницы. Правда, подсвечивать код на лету довольно накладно (по времени работы скрипта), поэтому для хранения уже обработанного кода я добавил новое свойство $text_as_html к сущности Post. Сеттер для установки этого свойства сделал приватным — он дергается только при изменении основного текста:

    /**
     * Set post text
     *
     * @param string $text
     * @return void
     */
    public function setText($text)
    {
        $this->text = $text;
        $this->setTextAsHtml($text);
    }
 
    private function setTextAsHtml($text)
    {
        // update text html code
        require_once __DIR__ . '/../Resources/vendor/geshi/geshi.php';
 
        $text = preg_replace_callback(
                    '/<pre lang="(.*?)">\r?\n?(.*?)\r?\n?\<\/pre>/is',
                    /**
                     * @param string $data
                     * @return string
                     */
                    function($data) {
                        $geshi = new \GeSHi($data[2], $data[1]);
                        return $geshi->parse_code();
                    }, $text);
 
        $this->textAsHtml = $text;
    }

GeSHi ещё удобна тем, что не нужно подключать отдельный css файл для подсветки кода.

RSS лента

Для генерации RSS ленты я решил использовать \Zend\Feed.
В ZF2 этот компонент претерпел значительные изменения, но даже без документации разобраться с ним получилось довольно быстро (в отличии от большинства вещей в Symfony2 :)). В результате получился вот такой экшн:

    /**
     * RSS feed
     *
     * @Route("/{_locale}/blog/rss", name="blog_rss",
     *      defaults={"_locale"="ru"}, requirements={"_locale"="ru|en"})
     */
    public function rssAction()
    {
        $feed = new \Zend\Feed\Writer\Feed();
 
        // получаем секцию настроек для бандла
        $config = $this->container->getParameter('stfalcon_blog.config');
 
        // эти три свойства обязательны для rss фида
        $feed->setTitle($config['rss']['title']);
        $feed->setDescription($config['rss']['description']);
        $feed->setLink($this->generateUrl('blog_rss', array(), true));
 
        // достаем список постов из репозитория
        $posts = $this->get('doctrine')->getEntityManager()
                ->getRepository("StfalconBlogBundle:Post")->getAllPosts();
        // формируем в цикле объекты для ленты
        foreach($posts as $post) {
            $entry = new \Zend\Feed\Writer\Entry();
            $entry->setTitle($post->getTitle());
            $entry->setLink($this->generateUrl('blog_post_view', array('slug' => $post->getSlug()), true));
 
            $feed->addEntry($entry);
        }
 
        // готовый xml код отдаем как Response
        return new Response($feed->export('rss'));
    }

Название и описание блога вынес в конфиг:

stfalcon_blog:
    rss:
        title: "Блог веб-студии stfalcon.com"
        description: "Заметки используемых технологиях, реализованных проектах а также наших трудовых буднях и отдыхе"

Эта заметка вам пригодилась? Напишите комментарий 🙂

8 thoughts on “BlogBundle для Symfony2

  1. Круто! Ед. может EntitiesToStringTransformer переименовать? А то как то ассоциация с тегами не прослеживается, я даже подумал, что ошибка и полез смотреть название трансформера 🙂
    Кент Бек в своей книге Implementation Patterns пишет о том, что название для класса очень важная штука (глава 5 вроде)

      • Может, что бы слово Tag присутствовало, а то он же не просто разбивает а работает с StfalconBlogBundle:Tag

        • Когда в другом месте нужно будет сделать привязку тегов, то я это дело как-то кастомизирую.

  2. Думаю сам бандл почти никто юзать не будет.
    Но вот куски по типу rss или трансформер для тэгов многим бы пригодились, спасибо.

  3. Спасибо!
    Отталкивался от Вашего туториала при решении похожей проблемы.

    Пара замечаний:
    1. Ссылка на EntitiesToArrayTransformer битая
    2. Нет кода с регистрацией кастомного типа в файле конфигурации сервисов

    • 3. Трансформер то кастомный, имхо, самое подходящее имя: “TagsToStringTransformer”

  4. Your requirements could not be resolved to an installable set of packages.

    Problem 1
    – Can only install one of: sonata-project/doctrine-orm-admin-bundle[2.2.x-de
    v, 2.4.x-dev].
    – Can only install one of: sonata-project/doctrine-orm-admin-bundle[2.4.x-de
    v, 2.2.x-dev].
    – Can only install one of: sonata-project/doctrine-orm-admin-bundle[2.2.x-de
    v, 2.4.x-dev].
    – stfalcon/blog-bundle dev-master requires sonata-project/doctrine-orm-admin
    -bundle 2.2.*@dev -> satisfiable by sonata-project/doctrine-orm-admin-bundle[2.2
    .x-dev].
    – Installation request for stfalcon/blog-bundle dev-master -> satisfiable by
    stfalcon/blog-bundle[dev-master].
    – Installation request for sonata-project/doctrine-orm-admin-bundle == 2.4.9
    999999.9999999-dev -> satisfiable by sonata-project/doctrine-orm-admin-bundle[2.
    4.x-dev].

    Выдало вот так – почему композеру не нравится доктрин-админ-орм?

Leave a Reply

Your email address will not be published. Required fields are marked *