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

Виджет в Yii вы вставляете одной строкой, а получаете инкапсулированный блок с собственной логикой, разметкой и иногда состоянием между вызовами. Это удобно, пока вы не наткнётесь на две типичные ловушки: виджет тянет за собой ассеты, которые вы не заказывали, а статические свойства виджета живут между двумя вызовами на одной странице. Ниже — что нужно понять до того, как вы напишете первый extends Widget, и чем виджет отличается от простого render().
Материал написан для Yii 2 (2.0.x). В Yii 1 виджеты устроены иначе (CWidget, beginWidget/endWidget через $this->widget()), и переносить примеры напрямую нельзя.
Виджет — это не вьюха
Соблазн смотреть на виджет как на «вьюху с параметрами» приводит к неверным решениям. Разница принципиальная.
Вьюха — это PHP-файл, который вы рендерите вызовом $this->render('list', ['items' => $items]). У неё нет собственного класса, нет жизненного цикла, вся логика подготовки данных остаётся в контроллере или в том, кто её вызвал.
Виджет — это класс, наследник yii\base\Widget. У него есть свойства (публичные — это его входные параметры), метод init() и метод run(). Виджет сам решает, какую разметку отдать, сам подключает нужные ему CSS и JS, сам может держать состояние. Контроллеру про его внутренности знать не нужно.
Если блок нужен один раз и не имеет логики — это вьюха или партиал. Если блок повторяется, подключает свои ассеты или инкапсулирует поведение — это кандидат в виджеты. Не превращайте каждый <div> в класс.
Два способа вызвать виджет
У виджета два интерфейса вызова, и путать их — распространённая ошибка новичка.
- Одиночный вызов —
Menu::widget(['items' => $items]). Виджет создаётся, отрабатываетinit(), потомrun(), возвращает строку. Всё за один вызов. - Парный вызов —
ActiveForm::begin([...])иActiveForm::end(). Между ними вы выводите содержимое сами.begin()вызываетinit()и запоминает объект во внутреннем стеке,end()достаёт его и вызываетrun(). Так работают формы и блоки вродеBreadcrumbsтолько в первом варианте.
Парный вызов удобен, когда содержимое блока — это обычная разметка страницы, которую неудобно передавать параметром. Но за удобство платят: если забыли end(), стек виджетов останется несбалансированным, и Yii выбросит исключение о незакрытом виджете.
Жизненный цикл: init() и run()
Оба метода вы переопределяете сами. Разница в том, когда они вызываются.
init()— подготовка. Здесь задают значения по умолчанию для незаполненных свойств, нормализуют входные данные, генерируютid, если он не задан. К моментуinit()все публичные свойства уже проставлены из массива, который вы передали вwidget().run()— вывод. Здесь виджет либо возвращает строку напрямую, либо рендерит свою вьюху через$this->render()и возвращает результат.
Ключевой момент: виджет должен возвращать строку, а не печатать её через echo. Если внутри run() вы напишете echo, разметка выведется раньше времени и попадёт не туда, где вы её ждали, — до окружающих тегов или в середину другого виджета.
<?php
// widgets/Alert.php — Yii 2.0.x
namespace app\widgets;
use yii\base\Widget;
use yii\helpers\Html;
class Alert extends Widget
{
// Публичные свойства — это входные параметры виджета
public $message = '';
public $type = 'info'; // info | warning | danger
public $options = [];
public function init()
{
parent::init();
// Нормализуем: гарантируем класс-обёртку
Html::addCssClass($this->options, ['widget' => 'alert', 'type' => 'alert-' . $this->type]);
}
public function run()
{
if ($this->message === '') {
return ''; // Пустое сообщение — ничего не выводим
}
// Возвращаем строку, а не печатаем
return Html::tag('div', Html::encode($this->message), $this->options);
}
}
Вызов в шаблоне:
<?= \app\widgets\Alert::widget([
'message' => 'Профиль сохранён',
'type' => 'info',
]) ?>
Обратите внимание на Html::encode() вокруг сообщения. Виджет получает данные снаружи, а значит должен считать их небезопасными. Забыли encode — открыли XSS через параметр, который вы передаёте из формы или из базы.
Ассеты: виджет подключает то, что ему нужно, сам
Главное преимущество виджета перед партиалом — он умеет заявлять о своих зависимостях. Если виджету нужен свой CSS или JS, он регистрирует ассет-бандл в init() или run(), а не заставляет вас руками добавлять теги в лейаут.
<?php
namespace app\widgets;
use yii\base\Widget;
use app\assets\AlertAsset;
class Alert extends Widget
{
public function run()
{
// Регистрируем ассеты именно этого виджета
AlertAsset::register($this->getView());
// ... вывод разметки
}
}
AssetBundle сам следит, чтобы один и тот же бандл не подключился дважды, даже если виджет вызван на странице десять раз. Файл подключается один раз. Это и есть причина не тащить <link> и <script> прямо в разметку виджета: при повторном выводе вы получите дубли тегов, а через ассет-бандл — нет.
Ловушка: состояние между вызовами
Виджет — обычный PHP-объект, каждый вызов widget() создаёт новый экземпляр. Но статические свойства класса общие для всех экземпляров в пределах одного запроса. Именно на них Yii строит автогенерацию id.
Базовый Widget держит статический счётчик. Если вы не задали id явно, метод getId() вернёт w0, w1, w2 — по порядку появления виджетов на странице. Отсюда две неприятности:
- Если вы жёстко прописали
idв JS, а порядок виджетов на странице поменялся — привязка съедет. Задавайтеidявно, если завязываетесь на него в скриптах. - Если вы сами храните что-то в
static-свойстве своего виджета (например, чтобы посчитать, сколько раз он вызван), помните: это состояние переживёт все вызовы в текущем запросе, но обнулится в следующем. Для кеша между запросами это не подходит.
Виджет или партиал: когда что
| Критерий | Партиал (render) | Виджет |
|---|---|---|
| Своя логика подготовки данных | Нет, готовит вызывающий | Да, внутри init/run |
| Свои CSS/JS | Подключаете вручную | Регистрирует сам через AssetBundle |
| Повторное использование в разных местах | Возможно, но параметры дублируются | Инкапсулировано, вызов одной строкой |
| Парный вызов begin/end | Нет | Да |
| Порог входа | Низкий | Выше: нужен класс |
Что ломается первым у новичков
- Незакрытый
begin(). ЗабылиWidget::end()— исключениеExpecting end() of .... Проверяйте баланс парных вызовов. echoвместоreturnвrun(). Разметка вылезает не в том месте. Виджет всегда возвращает строку.- Данные без экранирования. Всё, что пришло снаружи и попадает в HTML, прогоняйте через
Html::encode(). - Дубли ассетов. Подключаете
<script>прямо в разметке виджета вместоAssetBundle::register()— на странице с несколькими вызовами получаете дубли. - Тяжёлая логика в
init().init()вызывается при создании даже для парного вызова до вывода содержимого. Тяжёлые запросы к базе там замедлят рендер, даже если виджет в итоге ничего не покажет.
Что делать дальше
Разберите исходники встроенных виджетов Yii — yii\bootstrap5\Nav или yii\widgets\Menu: там видно, как устроены нормализация свойств в init(), генерация id и работа с ассетами на живом коде. Дальше два направления: если виджету нужна отдельная разметка — вынесите её во вьюху виджета через $this->render(), это чище, чем длинные конкатенации строк в run(); если виджет становится сложным и тянет много данных — подумайте, не должна ли часть логики уехать в отдельный сервис, а виджет пусть только отображает. Инвалидация фрагментного кеша внутри виджетов через beginCache/endCache — отдельная тема, здесь мы её не разворачиваем.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Yii 2 Guide — Widgets, Yii 2 API — yii\base\Widget
Частые вопросы
У меня Yii 1, эти примеры подойдут?
Почему разметка виджета выводится не в том месте?
Как передать в виджет содержимое, а не строку-параметр?
Виджет вызывается на странице несколько раз — не будет ли дублей CSS/JS?
Стоит ли делать виджет из любого повторяющегося блока?
Можно ли в init() делать запрос к базе данных?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.