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

Виджеты

Программирование

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

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

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

Вот так, например, выглядит главная страница сайта glazschool.ru при редактировании в админке:

Виджеты в редакторе

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

Итак, попробуем вставить виджет последних записей в блоге в текст этой записи:

Последние записи:

Начиная проект на Symfony2, в отличие от Yii, у разработчика наверняка возникает чувство недостатка встроенной реализации де-факто стандартных обыденных вещей вроде паджинатора и хлебных крошек. Но это сложно назвать проблемой, так как за много лет существования фреймворка написано множество готовых компонентов.

Продолжаем разработку нашего чудо-сервиса на Yii2. На прошлом уроке мы создали через Composer новый проект и дополнили его раздельной системой конфигурационных файлов. Сегодня мы внедрим в проект модульную структуру и немного лучше познакомимся с базовыми настройками и некоторыми возможностями авторефакторинга в PhpStorm IDE.

Как видите, этот виджет удачно вставился.

Перейдём к реализации. Для начала создаём наш виджет /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); ?>

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

Комментарии

2013-01-18 07:32:39

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

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

Ответить

2013-01-18 07:34:43

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

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

Ответить

2013-01-18 09:37:20

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

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

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

Ответить

2013-04-12 19:48:26

Стас

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

Ответить

2013-04-13 09:30:00

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

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

Ответить

2013-04-13 15:36:39

Стас

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

Ответить

2013-04-13 20:18:38

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

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

Ответить

2013-04-16 14:24:05

Максим

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

Ответить

2013-04-16 14:25:24

Максим

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

Ответить

2013-04-16 15:40:39

Максим

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

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

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

Ответить

2013-04-16 15:45:50

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

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

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

2013-07-23 10:19:21

Максим

Дмитрий, при использовании поведения возникает следующая проблема:
Если в методе 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 весь код запихать, но всё же.

Ответить

2013-07-23 10:49:51

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

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

Ответить

2013-07-23 11:28:15

Максим

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

Ответить

2014-01-11 16:34:30

maleks

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

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

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

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

Ответить

2014-01-27 15:36:06

kosenka

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

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

Ответить

2014-01-27 20:08:16

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

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

Ответить

2014-04-08 14:21:12

Илья

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

Ответить

2014-04-10 16:46:37

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

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

Ответить

2014-04-24 22:57:57

Сергей

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

Ответить

2014-06-12 19:56:08

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

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

Ответить

2014-06-12 21:01:02

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

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

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 упомянул.

Ответить

2014-06-12 22:13:55

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

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

Ответить

2014-06-12 22:21:43

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

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

Ответить

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

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


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



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