Встраиваем виджеты в текст страницы в Yii

Виджеты

Давным давно в тридевя... в шаблонизаторе моей второй по счёту старой 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 с нужным нам поведением:

Код на GitHub

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

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); ?>

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

Комментарии

 

Виталий Иванов

Дмитрий, а что за форум на сайте школы? самописный?

Ответить

 

Виталий Иванов

ээ, увидел снизу, извини за оффтоп.

Ответить

 

Виталий Иванов

хмм, у меня почему-то выводится сначала код, генерируемый виджетом, а уже потом - код страницы.
и получается фигня....
даже если вызываю не так:
echo $this->decodeWidgets($model->text);
а так:
$this->decodeWidgets($model->text);

такое ощущение, что где-то что-то с ob_start ob_end_clean напутано....
Разбираюсь...

Ответить

 

Стас

Дмитрий, не сильно разбираюсь в преимуществе работы с бихавером. Почему то из-за этого пробела, не могу его использовать... не осознаю его нужность. тогда как в ваших работах, я удивляюсь, почти повсеместное применение. откройте секрет, в чем тотальное преимущество, в отличие от стандартного подхода построения виджета? Вот, не использую я данное поведение, и мне кажется так же удобно работает виджет.., может что недопонимаю?
было б вообще замечательно, если бы вы про поведение разжевали тему на простых примерах, думаю было-бы многим интересно.
спасибо.

Ответить

 

Дмитрий Елисеев

Виджеты и так стандартные. А именно это поведение предоставляет шаблонизатор и позволяет вставлять те же виджеты прямо в текст статьи из БД. Или Вам интересно, для чего вообще поведения нужны?

Ответить

 

Стас

ну, я так и понял, просто дело в выводе.
а по поводу "поведения"... Ваша подача написания, действительно очень импонирует, и если бы вы нашли время для создания хорошей статьи по этой теме, многим было б, полезно.
просто фр-к хочется использовать максимально, а такие пробелы - не очень хорошо.

Ответить

 

Дмитрий Елисеев

Спасибо за тему. Может скоро и напишу.

Ответить

 

Максим

Дмитрий, не могли бы вы выложить код на какой-нибудь другой ресурс. Репозиторий на GitHub по какой-то причине выдает ошибку 500. А поведение выглядит очень интересным. Как раз под задачу, которая возникла буквально вот...

Ответить

 

Максим

Спасибо. Уже не надо репозиторий GitHub ожил.

Ответить

 

Максим

У меня конфликтовала эта строка

Yii::app()->cache->set($index, $html, $cache);

После того как закоментировал всё заработало. Ясно одно, приложение спотыкается при попытке записи в кэш. Чем это может быть вызвано?

Ответить

 

Дмитрий Елисеев

Наверное у Вас не указан компонент кэша в конфиге. Можете пока поставить заглушку:

return array(
    'components'=>array(
        ...
        'cache'=>array(
            'class'=>'system.caching.CDummyCache',
        ),
    ),
);
Ответить

 

Максим

Дмитрий, при использовании поведения возникает следующая проблема:
Если в методе init() виджета произвести запись в переменную класса, то эта переменная пустая в методе run().
Создается впечатление, что запись идет в локальную переменную.
Например

class TestWidget extends CWidget {

public $var;

public function init() { 
    $this->var = 13;
    echo $this->var; // выводит 13
}

public function run() {
    echo $this->var; // пусто
}

При вызове виджета по старинке всё отрабатывает нормально.
Тестировал на чистом приложении.
Как вариант конечно и в run весь код запихать, но всё же.

Ответить

 

Дмитрий Елисеев

Добавил вызов метода init(). Теперь должно работать соответствующе.

Ответить

 

Максим

Спасибо, всё работает.

Ответить

 

maleks

Нормальный такой рецепт.

Дальше чистая теория.

Ваш виджет напрямую работает с моделью, т.к. как контроллер выступает, если смотреть на MVC логику.
В yii 1 хоть CWidget как и CController имеют общего CBaseController, что как бы подразумевает что можно как контроллер.

В новом yii2 yii\base\Widget таких намеков не имеет, т.е. как сущность только уровня вида позиционируется. Хотя сути своего функционала он не сменил и ничего другого тоже не появилось. Малость запутывающе.

Ответить

 

kosenka

А как передать параметр "массив"?

Пробовал так: {{w:wLink|text=Какой-то_текст;url=array('/site')}}
Не работает :(

Ответить

 

Дмитрий Елисеев

Передаёт строками как есть. Массивы передавать не приходилось. Можно попробовать как-нибудь распарсить строку с помощью eval($value).

Ответить

 

Илья

ошибочка /componetns/widgets/LastPostsWidget.php

Ответить

 

Дмитрий Елисеев

Спасибо! Добавил в текст оговорку о возможном использовании модулей.

Ответить

 

Сергей

Дмитрий, большое спасибо за статью и за файл! Очень пригодится на будущее.

Ответить

 

Алексей Парников

Дмитрий, мне очень нравится ваш блог, делаете из меня более опытного разработчика. Меня интересует момент самой вставки кода виджета {{w:youtube|id=qwer12345}}, как вы реализовали данную вставку через текстовый редактор со стороны админки, и какой редактор вы используете для редактирования текстовых данных? Вы "выдрали" из wordpress функционал вставки галереи ?

Ответить

 

Дмитрий Елисеев

Простейший виджет:

class YoutubeWidget extends CWidget
{
    public $id = '';
    public $width = 480;
    public $height = 360;

    public function run() {
        echo CHtml::tag('iframe', array(
            'src' => '//www.youtube.com/embed/' . $this->id,
            'width' => $this->width,
            'height' => $this->height,
            'frameborder' = 0,
            'allowfullscreen' => true,
        ));
    }
}

Как редактор использовал TinyMCE. Вставляю вручную просто как есть. Как раз во втором абзаце про Wordpress упомянул.

Ответить

 

Алексей Парников

>>> Вставляю вручную просто как есть
это я и хотел узнать, конечно хотелось реализовать доп кнопки к редактору (у "контент-менеджера" 100% будут сложности со вставкой в html режиме да + и еще и такой конструкции {{w:youtube|id=qwer12345}} ). Вот думаю как бы это сделать, есть у вас идеи по этому поводу, по поводу допиливания ckeditor или TinyMCE

Ответить

 

Алексей Парников

Похоже придется заморочиться с созданием своего плагина как здесь http://habrahabr.ru/post/204176/

Ответить

 

idel

При подключении поведения к контроллеру есть маленькая синтаксическая ошибка

  'widgets'=>array(
    'Share',
    'Comments',
    'blog.widgets.LastPostsWidget',
  }, // <- фигурная скобка
Ответить

 

Дмитрий Елисеев

Спасибо! Исправил.

Ответить

 

Utyabaev Idel

Отличный метод, Дмитрий.

Взял себе на вооружение.

Ответить

 

Александр

может и нубский вопрос, но ничего не приходит в голову. У вас в примере, среди обрабатываемых виджетов, указан виджет комментариев.
Сам решил добавить обработку его, но в него, при вызове, нужно передавать id материала и его тип. Каким образом тут поступить не придумал.
Можете что-нибудь подсказать?

Ответить

 

Дмитрий Елисеев

Можно вручную:

{{w:Comments|id=4;type=Post}}

или в самом виджете извлекая значение id из GET-параметров и получая тип из имени контроллера Yii::app()->controller->id.

Но лучше просто вызывать виджет стандартным образом в представлении. А если это делается для вывода комментариев не у всех постов, то просто добавьте в модель поста галочку, выводить комментарии или нет.

Это если я Вас правильно понял.

Ответить

 

Александр

да, вы меня правильно поняли, думал про все варианты.
Для себя выбрал последний вариант, т.к. редактировать содержимое будет человек не знающий что к чему.
Кроме того, подумал переделать ваш бихевиор и сделать вызов виджета с передачей модели, или отдельных параметров:

'widgets'=>array(
    'Share',
    'Comments' = > array('id'=>$model->id),
    'blog.widgets.LastPostsWidget',
),
Ответить

Оставить комментарий

Войти | Завести аккаунт


(никто не увидит)



Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>