Свой виджет для Yii 2: как сделать правильно с первого раза
Разбор рабочих приёмов создания виджетов в Yii 2: где хранить класс, как отдавать данные в шаблон, когда наследовать InputWidget и чем платить за неправильную регистрацию ассетов.

Виджет в Yii 2 — это переиспользуемый блок с логикой и разметкой: хлебные крошки, форма подписки, галерея, таблица с фильтрами. Проблема начинается, когда первый же виджет пишут «как проще»: логику суют в шаблон представления, CSS подключают через echo прямо в методе, а параметры принимают через глобальный массив. Через месяц такой виджет невозможно вставить на вторую страницу без побочных эффектов. Разберём, как устроить его так, чтобы он пережил рефакторинг.
Материал для Yii 2.0.x (проверялось на 2.0.49). Yii 3 — отдельная история с другим неймспейсом и контейнером, здесь его не трогаем.
Из чего состоит виджет и где он живёт
Виджет — это класс, наследник yii\base\Widget. Минимальный жизненный цикл — два метода:
init()— вызывается при старте, здесь нормализуют входные параметры и задают значения по умолчанию;run()— вызывается при завершении, возвращает готовую HTML-строку.
Вызывают виджет одним из двух способов. Короткий — через Widget::widget([...]), когда у виджета нет тела. Парный — через Widget::begin() / Widget::end(), когда между ними лежит контент (так работают ActiveForm и Pjax). Если ваш виджет просто рендерит блок по параметрам — вам нужен первый способ, второй не усложняйте на пустом месте.
Класс кладут в неймспейс приложения. В стандартном basic-шаблоне это app\components или app\widgets — каталог создаёте сами, автозагрузка по PSR-4 подхватит его без правки конфига, пока имя каталога совпадает с частью неймспейса.
Каркас, который работает
<?php
// файл: widgets/RecentPosts.php
namespace app\widgets;
use yii\base\Widget;
use yii\helpers\Html;
class RecentPosts extends Widget
{
// Публичные свойства = входные параметры виджета
public $limit = 5;
public $items = [];
public $title = 'Последние записи';
public function init()
{
parent::init();
// Нормализуем на входе, а не в шаблоне
if ($this->limit < 1) {
$this->limit = 5;
}
$this->items = array_slice($this->items, 0, $this->limit);
}
public function run()
{
// Логика собрана здесь, разметка вынесена в представление
return $this->render('recent-posts', [
'title' => $this->title,
'items' => $this->items,
]);
}
}Вызов на странице выглядит так:
<?= \app\widgets\RecentPosts::widget([
'limit' => 3,
'items' => $posts, // массив или ActiveRecord-объекты
]) ?>Где чаще всего ломают правильную структуру
1. Логика в шаблоне вместо run()
Соблазн: собрать SQL-запрос или отфильтровать массив прямо в файле представления. Цена: виджет становится непроверяемым, а представление — нечитаемым. Правило простое: run() готовит данные, render() получает их уже готовыми и только выводит. В шаблоне допустимы foreach, if и экранирование через Html::encode() — не больше.
2. echo вместо return в run()
Если внутри run() напечатать что-то через echo, вывод уйдёт в буфер раньше, чем Yii ожидает, и в парном режиме (begin/end) порядок блоков поедет. Виджет должен вернуть строку, а не печатать её. Метод render() как раз возвращает результат — используйте его.
Виджет, который сам печатает HTML вместо возврата строки, работает ровно до тех пор, пока он на странице один. Второй такой же виджет ниже — и вы полдня ищете, почему разметка разъехалась.
3. Представление без экранирования
Данные, пришедшие в виджет извне, экранируйте в шаблоне. Заголовок из базы, имя пользователя, любой текст — через Html::encode(). Пропущенное экранирование в переиспользуемом блоке опаснее, чем в одноразовом контроллере: виджет вставят на десять страниц, и XSS уедет вместе с ним.
Шаблон представления
<?php
// файл: widgets/views/recent-posts.php
/** @var string $title */
/** @var array $items */
use yii\helpers\Html;
?>
<div class="recent-posts">
<h3><?= Html::encode($title) ?></h3>
<ul>
<?php foreach ($items as $item): ?>
<li><?= Html::encode($item['title']) ?></li>
<?php endforeach; ?>
</ul>
</div>По умолчанию render() ищет файл в подкаталоге views рядом с классом виджета. Если структура другая — переопределите getViewPath(), но без нужды не трогайте: конвенция экономит нервы тому, кто откроет код после вас.
Ассеты: CSS и JS виджета
Самая частая ошибка новичка — вывести <style> и <script> прямо в шаблоне. При двух виджетах на странице стили и скрипты задублируются. Правильный путь — asset bundle: Yii сам подключит файлы один раз и разрешит зависимости.
<?php
// файл: widgets/RecentPostsAsset.php
namespace app\widgets;
use yii\web\AssetBundle;
class RecentPostsAsset extends AssetBundle
{
public $sourcePath = '@app/widgets/assets';
public $css = ['recent-posts.css'];
public $js = ['recent-posts.js'];
// Зависимости, например от jQuery-бандла Yii:
public $depends = [
'yii\\web\\JqueryAsset',
];
}Регистрируют бандл в init() или в шаблоне через объект вида ($this->view внутри представления, Yii::$app->view внутри класса). В шаблоне это выглядит так:
<?php app\widgets\RecentPostsAsset::register($this); ?>Метод register() идемпотентен: сколько бы виджетов на странице ни вызвали его, файлы попадут в разметку один раз.
Виджеты для форм: InputWidget вместо Widget
Если виджет рисует поле ввода, привязанное к атрибуту модели (кастомный datepicker, поле с маской), наследуйте не Widget, а yii\widgets\InputWidget. Он даёт готовые свойства $model, $attribute, $name, $value и логику генерации корректных name/id для полей, которые понимает ActiveForm и валидация на клиенте.
Тогда виджет встраивается в форму штатно:
<?= $form->field($model, 'birthday')->widget(\app\widgets\DatePicker::class) ?>На Widget такой виджет тоже можно собрать вручную, но вы будете руками воспроизводить генерацию имён атрибутов и связь с валидацией — то, что InputWidget уже делает правильно.
Что выбрать под задачу
| Задача | Базовый класс | Способ вызова |
|---|---|---|
| Блок вывода по параметрам (список, карточка) | yii\base\Widget | ::widget([...]) |
| Обёртка вокруг контента (форма, панель) | yii\base\Widget | ::begin() / ::end() |
| Поле ввода, привязанное к модели | yii\widgets\InputWidget | $form->field()->widget() |
Если не получилось
- «Getting unknown property» при вызове
::widget([...])— вы передали ключ, которого нет среди публичных свойств класса. Yii настраивает виджет через сеттеры конфигурации, любое имя в массиве должно совпадать сpublic-свойством или сеттером. - «The view file does not exist» — файл представления лежит не там, где ищет
render(). По умолчанию это каталогviewsрядом с классом. Проверьте имя файла и регистр — на Linux он чувствителен. - CSS подключился, но стили не применились — часто дело в порядке подключения или в том, что бандл не попал на страницу. Откройте исходник страницы и найдите ваш файл в разметке; если его нет —
register()не вызвался. - Виджет выводит контент дважды или в неверном месте — почти всегда
echoвнутриrun()вместоreturn. Верните строку. - Стили дублируются — файлы выводятся из шаблона напрямую, а не через asset bundle. Перенесите их в бандл.
Что настроить дальше
Когда каркас работает, следующие шаги: вынести виджет в отдельный composer-пакет, если он нужен в нескольких проектах; добавить кеширование фрагмента через $this->view->beginCache(), если run() тянет данные из базы на каждый запрос; написать функциональный тест на рендер через Codeception, чтобы падение виджета ловил CI, а не пользователь. Свойства виджета, которые приходят из базы, всегда проверяйте на экранирование — это первое, на что смотрят при ревью переиспользуемого блока.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: Yii 2 Guide — Widgets, Yii 2 API — yii\base\Widget
Частые вопросы
Чем виджет отличается от частичного представления (partial)?
А если у меня Yii 3, а не Yii 2?
Виджет не находит файл представления — что проверить?
Как подключить jQuery или другую библиотеку для виджета?
Можно ли кешировать вывод виджета?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.