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

Давным давно в тридевя... в шаблонизаторе моей второй по счёту старой CMS вставка виджетов в шаблон была релизована посредством использования старой доброй клинописи вида {{WidgetName|param1=val1;param2=val2}}. Этот код можно было вставлять даже в текст, что давало неоценимую возможность компоновать страницы любой сложности из виджетов прямо в текстовом редакторе админки.
Похожий функционал часто используют плагины WordPress (например, для вывода виджетов плагинов DDSitemapGen и ContactForm7 в текст страницы нужно добавить специфическую строку). Этот функционал я перенёс и в систему на Yii.
Вот так, например, выглядит главная страница сайта при редактировании в админке:
На любую страницу можно легко вывести форму обратной связи, блок комментирования или кнопки «Поделиться», просто добавив в её текст вызовы Contactform, Comments или Share. Вызов виджетов доступен только из разрешённого списка и только в тексте страницы. Это не позволяет злоумышленнику вызывать виджеты в комментариях или в личных сообщениях.
Итак, попробуем вставить виджет последних записей в блоге в текст этой записи:
[{widget:LastPosts|limit=2;cache=3600}]Как видите, этот виджет удачно вставился.
Перейдём к реализации. Для начала создаём наш виджет /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 с нужным нам поведением:
...и подключить наше поведение к контроллеру:
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 ожил.
Дмитрий ЕлисеевGitHub сейчас глючит. А пока можно взять на странице расширения.
МаксимУ меня конфликтовала эта строка
После того как закоментировал всё заработало. Ясно одно, приложение спотыкается при попытке записи в кэш. Чем это может быть вызвано?
Дмитрий ЕлисеевНаверное у Вас не указан компонент кэша в конфиге. Можете пока поставить заглушку:
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', ),
Кантёхин ДмитрийДмитрий доброго дня.
Вы можете поделиться кодом для yii2 версии этого решения?
Дмитрий ЕлисеевДля Yii2 не делал.
ДмитрийДобрый день!
Видимо обновили класс DInlineWidgetsBehavior. Скопипастил код, который указан в комментах. При выводе:
{5948511ef5518f73235d09ce65fb07d4:LastPosts5948511ef5518f73235d09ce65fb07d4}
ДмитрийДобрый день! подскажите почему может не работать пагинация в ListView в виджете, который встроен по средствам вашего поведения?
tarikТак если подключить это поведение к контроллеру, то оно автоматом подставляет виджеты в вывод контроллера, или надо вручную вызывать decodeWidgets ?
Дмитрий ЕлисеевВручную.