Встраиваем виджеты в текст страницы в 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 сейчас глючит. А пока можно взять на странице расширения.
У меня конфликтовала эта строка
После того как закоментировал всё заработало. Ясно одно, приложение спотыкается при попытке записи в кэш. Чем это может быть вызвано?
Наверное у Вас не указан компонент кэша в конфиге. Можете пока поставить заглушку:
Дмитрий, при использовании поведения возникает следующая проблема:
Если в методе init() виджета произвести запись в переменную класса, то эта переменная пустая в методе run().
Создается впечатление, что запись идет в локальную переменную.
Например
При вызове виджета по старинке всё отрабатывает нормально.
Тестировал на чистом приложении.
Как вариант конечно и в run весь код запихать, но всё же.
Добавил вызов метода init(). Теперь должно работать соответствующе.
Спасибо, всё работает.
Нормальный такой рецепт.
Дальше чистая теория.
Ваш виджет напрямую работает с моделью, т.к. как контроллер выступает, если смотреть на MVC логику.
В yii 1 хоть CWidget как и CController имеют общего CBaseController, что как бы подразумевает что можно как контроллер.
В новом yii2 yii\base\Widget таких намеков не имеет, т.е. как сущность только уровня вида позиционируется. Хотя сути своего функционала он не сменил и ничего другого тоже не появилось. Малость запутывающе.
А как передать параметр "массив"?
Пробовал так: {{w:wLink|text=Какой-то_текст;url=array('/site')}}
Не работает :(
Передаёт строками как есть. Массивы передавать не приходилось. Можно попробовать как-нибудь распарсить строку с помощью eval($value).
ошибочка /componetns/widgets/LastPostsWidget.php
Спасибо! Добавил в текст оговорку о возможном использовании модулей.
Дмитрий, большое спасибо за статью и за файл! Очень пригодится на будущее.
Дмитрий, мне очень нравится ваш блог, делаете из меня более опытного разработчика. Меня интересует момент самой вставки кода виджета {{w:youtube|id=qwer12345}}, как вы реализовали данную вставку через текстовый редактор со стороны админки, и какой редактор вы используете для редактирования текстовых данных? Вы "выдрали" из wordpress функционал вставки галереи ?
Простейший виджет:
Как редактор использовал TinyMCE. Вставляю вручную просто как есть. Как раз во втором абзаце про Wordpress упомянул.
>>> Вставляю вручную просто как есть
это я и хотел узнать, конечно хотелось реализовать доп кнопки к редактору (у "контент-менеджера" 100% будут сложности со вставкой в html режиме да + и еще и такой конструкции {{w:youtube|id=qwer12345}} ). Вот думаю как бы это сделать, есть у вас идеи по этому поводу, по поводу допиливания ckeditor или TinyMCE
Похоже придется заморочиться с созданием своего плагина как здесь http://habrahabr.ru/post/204176/
При подключении поведения к контроллеру есть маленькая синтаксическая ошибка
Спасибо! Исправил.
Отличный метод, Дмитрий.
Взял себе на вооружение.
может и нубский вопрос, но ничего не приходит в голову. У вас в примере, среди обрабатываемых виджетов, указан виджет комментариев.
Сам решил добавить обработку его, но в него, при вызове, нужно передавать id материала и его тип. Каким образом тут поступить не придумал.
Можете что-нибудь подсказать?
Можно вручную:
или в самом виджете извлекая значение id из GET-параметров и получая тип из имени контроллера Yii::app()->controller->id.
Но лучше просто вызывать виджет стандартным образом в представлении. А если это делается для вывода комментариев не у всех постов, то просто добавьте в модель поста галочку, выводить комментарии или нет.
Это если я Вас правильно понял.
да, вы меня правильно поняли, думал про все варианты.
Для себя выбрал последний вариант, т.к. редактировать содержимое будет человек не знающий что к чему.
Кроме того, подумал переделать ваш бихевиор и сделать вызов виджета с передачей модели, или отдельных параметров:
Дмитрий доброго дня.
Вы можете поделиться кодом для yii2 версии этого решения?
Для Yii2 не делал.
Добрый день!
Видимо обновили класс DInlineWidgetsBehavior. Скопипастил код, который указан в комментах. При выводе:
Добрый день! подскажите почему может не работать пагинация в ListView в виджете, который встроен по средствам вашего поведения?
Так если подключить это поведение к контроллеру, то оно автоматом подставляет виджеты в вывод контроллера, или надо вручную вызывать decodeWidgets ?
Вручную.