Давным давно в тридевя… в шаблонизаторе моей второй по счёту старой CMS вставка виджетов в шаблон была релизована посредством использования старой доброй клинописи вида{{WidgetName|param1=val1;param2=val2}}. Этот код можно было вставлять даже в текст, что давало неоценимую возможность компоновать страницы любой сложности из виджетов прямо в текстовом редакторе админки.

Похожий функционал часто используют плагины WordPress (например, для вывода виджетов плагинов DDSitemapGen и ContactForm7 в текст страницы нужно добавить специфическую строку). Этот функционал я перенёс и в систему на Yii.

На любую страницу можно легко вывести форму обратной связи, блок комментирования или кнопки «Поделиться», просто добавив в её текст вызовы Contactform, Comments или Share. Вызов виджетов доступен только из разрешённого списка и только в тексте страницы. Это не позволяет злоумышленнику вызывать виджеты в комментариях или в личных сообщениях.

Перейдём к реализации. Для начала создаём наш виджет /componetns/widgets/LastPostsWidget.phpили, если мы используем модули, /modules/blog/widgets/LastPostsWidget.php:

class LastPostsWidget extends CWidget
{
    public $tpl='default';
    public $limit=3;
 
    public function run()
    {
        $posts = Post::model()->findAll(array(
            'condition'=>'public=1',
            'order'=>'date DESC',
            'limit'=>$this->limit,
        ));
        $this->render('LastPosts/' . $this->tpl, array(
            'posts'=>$posts,
        ));
    }
}

Виджет выбирает из базы 3 последних записи и передаёт в представление/componetns/widgets/views/LastPosts/default.php, которое, собственно, и выводит анонсы записей. Код его приводить не будем.

Теперь необходимо создать файл /components/DInlineWidgetsBehavior.php с нужным нам поведением:

<?php
/**
 * DInlineWidgetsBehavior allows render widgets in page content
 *
 * Config:
 * <code>
 * return array(
 *     // ...
 *     'params'=>array(
 *          // ...
 *         'runtimeWidgets'=>array(
 *             'Share',
 *             'Comments',
 *             'blog.widgets.LastPosts',
 *         }
 *     }
 * }
 * </code>
 *
 * Widget:
 * <code>
 * class LastPostsWidget extends CWidget
 * {
 *     public $tpl='default';
 *     public $limit=3;
 *
 *     public function run()
 *     {
 *         $posts = Post::model()->published()->last($this->limit)->findAll();
 *         $this->render('LastPosts/' . $this->tpl,array(
 *             'posts'=>$posts,
 *         ));
 *     }
 * }
 * </code>
 *
 * Controller:
 * <code>
 * class Controller extends CController
 * {
 *     public function behaviors()
 *     {
 *         return array(
 *             'InlineWidgetsBehavior'=>array(
 *                 'class'=>'DInlineWidgetsBehavior',
 *                 'location'=>'application.components.widgets',
 *                 'widgets'=>Yii::app()->params['runtimeWidgets'],
 *              ),
 *         );
 *     }
 * }
 * </code>
 *
 * For rendering widgets in View you must call Controller::decodeWidgets() method:
 * <code>
 * $text = '
 *     <h2>Lorem ipsum</h2>
 *     <p>[*LastPosts*]</p>
 *     <p>[*LastPosts|limit=4*]</p>
 *     <p>[*LastPosts|limit=5;tpl=small*]</p>
 *     <p>[*LastPosts|limit=5;tpl=small|cache=300*]</p>
 *     <p>Dolor...</p>
 * ';
 * echo $this->decodeWidgets($text);
 * </code>
 *
 * @author ElisDN <mail@elisdn.ru>
 * @link http://www.elisdn.ru
 * @version 1.2
 */

class DInlineWidgetsBehavior extends CBehavior
{
    /**
     * @var string marker of block begin
     */
    public $startBlock = '[*';
    /**
     * @var string marker of block end
     */
    public $endBlock = '*]';
    /**
     * @var string alias if needle using default location 'path.to.widgets'
     */
    public $location = '';
    /**
     * @var string global classname suffix like 'Widget'
     */
    public $classSuffix = '';
    /**
     * @var array of allowed widgets
     */
    public $widgets = array();

    protected $_widgetToken;

    public function __construct()
    {
        $this->_initToken();
    }

    /**
     * Content parser
     * Use $this->decodeWidgets($model->text) in view
     * @param $text
     * @return mixed
     */
    public function decodeWidgets($text)
    {
        $text = $this->_clearAutoParagraphs($text);
        $text = $this->_replaceBlocks($text);
        $text = $this->_processWidgets($text);
        return $text;
    }

    /**
     * Content cleaner
     * Use $this->clearWidgets($model->text) in view
     * @param $text
     * @return mixed
     */
    public function clearWidgets($text)
    {
        $text = $this->_clearAutoParagraphs($text);
        $text = $this->_replaceBlocks($text);
        $text = $this->_clearWidgets($text);
        return $text;
    }

    protected function _processWidgets($text)
    {
        if (preg_match('|\{' . $this->_widgetToken . ':.+?' . $this->_widgetToken . '\}|is', $text))
        {
            foreach ($this->widgets as $alias)
            {
                $widget = $this->_getClassByAlias($alias);

                while (preg_match('#\{' . $this->_widgetToken . ':' . $widget . '(\|([^}]*)?)?' . $this->_widgetToken . '\}#is', $text, $p))
                {
                    $text = str_replace($p[0], $this->_loadWidget($alias, isset($p[2]) ? $p[2] : ''), $text);
                }
            }
            return $text;
        }
        return $text;
    }

    protected function _clearWidgets($text)
    {
        return preg_replace('|\{' . $this->_widgetToken . ':.+?' . $this->_widgetToken . '\}|is', '', $text);
    }

    protected function _initToken()
    {
        $this->_widgetToken = md5(microtime());
    }

    protected function _replaceBlocks($text)
    {
        $text = str_replace($this->startBlock, '{' . $this->_widgetToken . ':', $text);
        $text = str_replace($this->endBlock, $this->_widgetToken . '}', $text);
        return $text;
    }

    protected function _clearAutoParagraphs($output)
    {
        $output = str_replace('<p>' . $this->startBlock, $this->startBlock, $output);
        $output = str_replace($this->endBlock . '</p>', $this->endBlock, $output);
        return $output;
    }

    protected function _loadWidget($name, $attributes='')
    {
        $attrs = $this->_parseAttributes($attributes);
        $cache = $this->_extractCacheExpireTime($attrs);

        $index = 'widget_' . $name . '_' . serialize($attrs);

        if ($cache && $cachedHtml = Yii::app()->cache->get($index))
        {
            $html = $cachedHtml;
        }
        else
        {
            ob_start();
            $widgetClass = $this->_getFullClassName($name);
            $widget = Yii::app()->getWidgetFactory()->createWidget($this->owner, $widgetClass, $attrs);
            $widget->init();
            $widget->run();
            $html = trim(ob_get_clean());
            Yii::app()->cache->set($index, $html, $cache);
        }

        return $html;
    }

    protected function _parseAttributes($attributesString)
    {
        $params = explode(';', $attributesString);
        $attrs = array();

        foreach ($params as $param)
        {
            if ($param)
            {
                list($attribute, $value) = explode('=', $param);
                if ($value) $attrs[$attribute] = trim($value);
            }
        }

        ksort($attrs);
        return $attrs;
    }

    protected function _extractCacheExpireTime(&$attrs)
    {
        $cache = 0;
        if (isset($attrs['cache']))
        {
            $cache = (int)$attrs['cache'];
            unset($attrs['cache']);
        }
        return $cache;
    }

    protected function _getFullClassName($name)
    {
        $widgetClass = $name . $this->classSuffix;
        if ($this->_getClassByAlias($widgetClass) == $widgetClass && $this->location)
            $widgetClass = $this->location . '.' . $widgetClass;
        return $widgetClass;
    }

    protected function _getClassByAlias($alias)
    {
        $paths = explode('.', $alias);
        return array_pop($paths);
    }
}

…и подключить наше поведение к контроллеру:

class Controller extends CController
{
    public function behaviors()
    {
        return array(
            'InlineWidgetsBehavior'=>array(
                'class'=>'DInlineWidgetsBehavior',
                'location'=>'application.components.widgets',
                'startBlock'=> '{{w:',
                'endBlock'=> '}}',
                'widgets'=>array(
                    'Share',
                    'Comments',
                    'blog.widgets.LastPostsWidget',
                },
            ),
        );
    }
}

Удобнее вывести список доступных виджетов в конфигурационный файл:

'params'=>array(
    'runtimeWidgets'=>array(
        'Share',
        'Comments',
        'ContactWidget',
        'blog.widgets.LastPostsWidget',
    ),
),

и передавать список поведению в виде:

class Controller extends CController
{
    public function behaviors()
    {
        return array(
            'InlineWidgetsBehavior'=>array(
                'class'=>'DInlineWidgetsBehavior',
                'location'=>'application.components.widgets',
                'startBlock'=> '{{w:',
                'endBlock'=> '}}',
                'widgets'=>Yii::app()->params['runtimeWidgets'],
            ),
        );
    }
}

Параметр location нужно указать лишь в случае, когда виджеты находятся в отдельной папке, не указанной в директиве import файла конфигурации приложения. При его указании поведение будет само вызывать метод Yii::import() для подключения каждого виджета. Этот параметр игнорируется для виджетов, указанных с полным путём (вроде ‘blog.widgets.LastPosts’). ПоляstartBlock и endBlock можно использовать для указания своих начальных и конечных блоков.

Теперь мы можем включить в текст страниц команды подстановки виджетов в любом удобном нам виде. Кроме того, в поведении предусмотрено кеширование. Пример текста страницы:

<h2>Последние записи</h2>
<p>{{w:LastPostsWidget}}</p>

Если в проекте имена всех публичных виджетов имеют одинаковый суффикс Widget, то его можно вынести в параметр classSuffix

class Controller extends CController
{
    public function behaviors()
    {
        return array(
            'InlineWidgetsBehavior'=>array(
                'class'=>'DInlineWidgetsBehavior',
                'classSuffix'=> 'Widget',
                'startBlock'=> '{{w:',
                'endBlock'=> '}}',
                'widgets'=>Yii::app()->params['runtimeWidgets'],
            ),
        );
    }
}

и вызывать виджеты по имени без суффикса Widget

'params'=>array(
    'runtimeWidgets'=>array(
        'Share',
        'Comments',
        'Contact',
        'blog.widgets.LastPosts',
    ),
),

Всем виджетам можно указывать простые параметры и время кэширования:

<h2>Последние записи</h2>
<p>{{w:LastPosts}}</p>
 
<h2>Последние 4 записи</h2>
<p>{{w:LastPosts|limit=4}}</p>
 
<h2>Последние записи списком</h2>
<p>{{w:LastPosts|tpl=list}}</p>
 
<h2>Последние 5 записей списком, кешируемые на 300 секунд</h2>
<p>{{w:LastPosts|limit=5;tpl=list|cache=300}}</p>
 
<h2>Фантазия...</h2>
<p>{{w:youtube|id=qwer12345}}</p>
<p>{{w:flash|file=/banners/banner1.swf;w=320;h=240}}</p>
<p>{{w:gallery|folder=vecherinka2012}}</p>
<p>{{w:submenu|parent=services}}</p>

В представлении достаточно теперь пропустить текст страницы через обработчик:

<?php echo $this->decodeWidgets($model->text); ?>

…и текст выведется с выполненными виджетами.

 

Взято отсюда

Комментарии

comments powered by Disqus