Skip to content

Widgets

Widgets are reusable, self-contained components that encapsulate complex HTML generation logic. They provide a clean way to create configurable UI elements that can be used across different views and applications. Yii3 provides a flexible widget system through the yiisoft/widget package.

Installation

The widget functionality is provided by the yiisoft/widget package:

bash
composer require yiisoft/widget

For ready-made widgets (like forms, navigation, etc.), you can also install:

bash
composer require yiisoft/yii-widgets

These packages are included by default in the yiisoft/app application template.

Basic Concepts

Widget Class

A widget is a PHP class that extends the abstract Widget class and implements a render() method that returns HTML content as a string.

Widget Factory

The WidgetFactory is responsible for creating widget instances and can inject dependencies through the DI container.

Widget Configuration

Widgets can be configured with properties and methods, making them highly customizable and reusable across different contexts.

Creating Custom Widgets

Simple Widget

Here's a basic widget that displays an alert message:

php
<?php

declare(strict_types=1);

namespace App\Widget;

use Yiisoft\Html\Html;
use Yiisoft\Widget\Widget;

final class AlertWidget extends Widget
{
    private string $message = '';
    private string $type = 'info';
    private bool $closeable = false;

    public function message(string $message): self
    {
        $new = clone $this;
        $new->message = $message;
        return $new;
    }

    public function type(string $type): self
    {
        $new = clone $this;
        $new->type = $type;
        return $new;
    }

    public function closeable(bool $closeable = true): self
    {
        $new = clone $this;
        $new->closeable = $closeable;
        return $new;
    }

    protected function render(): string
    {
        if (empty($this->message)) {
            return '';
        }

        $classes = ['alert', 'alert-' . $this->type];
        
        if ($this->closeable) {
            $classes[] = 'alert-dismissible';
        }

        $content = Html::encode($this->message);
        
        if ($this->closeable) {
            $content .= Html::button('×', [
                'type' => 'button',
                'class' => 'btn-close',
                'data-bs-dismiss' => 'alert',
            ]);
        }

        return Html::div($content, ['class' => implode(' ', $classes)]);
    }
}

Using the Widget

php
<?php

declare(strict_types=1);

use App\Widget\AlertWidget;

/**
 * @var \Yiisoft\View\WebView $this
 */
?>

<div class="page-content">
    <?= AlertWidget::widget()
        ->message('Operation completed successfully!')
        ->type('success')
        ->closeable() ?>
    
    <?= AlertWidget::widget()
        ->message('Please review the form errors below.')
        ->type('danger') ?>
</div>

Widget with Dependencies

Widgets can use dependency injection for services:

php
<?php

declare(strict_types=1);

namespace App\Widget;

use App\Service\UserService;
use Yiisoft\Html\Html;
use Yiisoft\Router\UrlGeneratorInterface;
use Yiisoft\Widget\Widget;

final class UserMenuWidget extends Widget
{
    public function __construct(
        private UserService $userService,
        private UrlGeneratorInterface $urlGenerator,
    ) {}

    protected function render(): string
    {
        $currentUser = $this->userService->getCurrentUser();
        
        if ($currentUser === null) {
            return $this->renderLoginLink();
        }

        return $this->renderUserMenu($currentUser);
    }

    private function renderLoginLink(): string
    {
        return Html::a('Login', $this->urlGenerator->generate('login'), [
            'class' => 'btn btn-primary',
        ]);
    }

    private function renderUserMenu($user): string
    {
        $items = [
            Html::a('Profile', $this->urlGenerator->generate('user.profile'), [
                'class' => 'dropdown-item',
            ]),
            Html::a('Settings', $this->urlGenerator->generate('user.settings'), [
                'class' => 'dropdown-item',
            ]),
            '<div class="dropdown-divider"></div>',
            Html::a('Logout', $this->urlGenerator->generate('logout'), [
                'class' => 'dropdown-item',
            ]),
        ];

        return Html::div(
            Html::button(
                Html::encode($user->getName()) . ' <span class="caret"></span>',
                [
                    'class' => 'btn btn-secondary dropdown-toggle',
                    'type' => 'button',
                    'data-bs-toggle' => 'dropdown',
                ]
            ) .
            Html::div(implode('', $items), ['class' => 'dropdown-menu']),
            ['class' => 'dropdown']
        );
    }
}

Widget Factory Setup

Bootstrap Configuration

Initialize the widget factory in your application bootstrap:

config/bootstrap.php

php
<?php

declare(strict_types=1);

use Yiisoft\Widget\WidgetFactory;

/**
 * @var \Psr\Container\ContainerInterface $container
 */

// Widget factory defaults
$widgetDefaults = [
    App\Widget\AlertWidget::class => [
        'type()' => ['info'],
        'closeable()' => [true],
    ],
    App\Widget\CardWidget::class => [
        'headerClass()' => ['card-header bg-primary text-white'],
    ],
];

WidgetFactory::initialize($container, $widgetDefaults);

DI Container Configuration

config/web/di.php

php
use App\Widget\AlertWidget;
use App\Widget\UserMenuWidget;

return [
    // Widget configurations can be defined here if needed
    AlertWidget::class => static fn() => new AlertWidget(),
    
    UserMenuWidget::class => static fn(
        App\Service\UserService $userService,
        Yiisoft\Router\UrlGeneratorInterface $urlGenerator
    ) => new UserMenuWidget($userService, $urlGenerator),
];

Advanced Widget Examples

Data List Widget

A widget that displays a list of items with pagination:

php
<?php

declare(strict_types=1);

namespace App\Widget;

use Yiisoft\Data\Paginator\PaginatorInterface;
use Yiisoft\Html\Html;
use Yiisoft\Widget\Widget;

final class DataListWidget extends Widget
{
    private ?PaginatorInterface $paginator = null;
    private ?callable $itemRenderer = null;
    private string $containerClass = 'data-list';
    private string $itemClass = 'data-item';
    private string $emptyText = 'No items found.';

    public function paginator(PaginatorInterface $paginator): self
    {
        $new = clone $this;
        $new->paginator = $paginator;
        return $new;
    }

    public function itemRenderer(callable $renderer): self
    {
        $new = clone $this;
        $new->itemRenderer = $renderer;
        return $new;
    }

    public function containerClass(string $class): self
    {
        $new = clone $this;
        $new->containerClass = $class;
        return $new;
    }

    public function itemClass(string $class): self
    {
        $new = clone $this;
        $new->itemClass = $class;
        return $new;
    }

    public function emptyText(string $text): self
    {
        $new = clone $this;
        $new->emptyText = $text;
        return $new;
    }

    protected function render(): string
    {
        if ($this->paginator === null) {
            return '';
        }

        $items = $this->paginator->read();
        
        if (empty($items)) {
            return Html::div($this->emptyText, ['class' => 'empty-message']);
        }

        $itemsHtml = '';
        foreach ($items as $item) {
            $content = $this->itemRenderer 
                ? ($this->itemRenderer)($item)
                : Html::encode((string) $item);
                
            $itemsHtml .= Html::div($content, ['class' => $this->itemClass]);
        }

        return Html::div($itemsHtml, ['class' => $this->containerClass]);
    }
}

Usage:

php
<?php

use App\Widget\DataListWidget;

/**
 * @var \Yiisoft\Data\Paginator\PaginatorInterface $postPaginator
 */
?>

<?= DataListWidget::widget()
    ->paginator($postPaginator)
    ->containerClass('posts-list row')
    ->itemClass('col-md-4 mb-3')
    ->itemRenderer(function ($post) {
        return $this->render('_post_card', ['post' => $post]);
    }) ?>

Form Widget

A widget that simplifies form rendering:

php
<?php

declare(strict_types=1);

namespace App\Widget;

use Yiisoft\Form\FormModelInterface;
use Yiisoft\Html\Html;
use Yiisoft\Widget\Widget;

final class FormWidget extends Widget
{
    private ?FormModelInterface $model = null;
    private string $action = '';
    private string $method = 'POST';
    private array $options = [];
    private array $fields = [];

    public function model(FormModelInterface $model): self
    {
        $new = clone $this;
        $new->model = $model;
        return $new;
    }

    public function action(string $action): self
    {
        $new = clone $this;
        $new->action = $action;
        return $new;
    }

    public function method(string $method): self
    {
        $new = clone $this;
        $new->method = $method;
        return $new;
    }

    public function options(array $options): self
    {
        $new = clone $this;
        $new->options = $options;
        return $new;
    }

    public function field(string $attribute, array $options = []): self
    {
        $new = clone $this;
        $new->fields[$attribute] = $options;
        return $new;
    }

    protected function render(): string
    {
        if ($this->model === null) {
            return '';
        }

        $formOptions = array_merge([
            'action' => $this->action,
            'method' => $this->method,
            'class' => 'form',
        ], $this->options);

        $content = '';
        foreach ($this->fields as $attribute => $options) {
            $content .= $this->renderField($attribute, $options);
        }

        return Html::form($content, $formOptions);
    }

    private function renderField(string $attribute, array $options): string
    {
        $label = $options['label'] ?? ucfirst($attribute);
        $type = $options['type'] ?? 'text';
        $class = $options['class'] ?? 'form-control';

        $field = Html::div(
            Html::label($label, null, ['class' => 'form-label']) .
            Html::input($type, $attribute, $this->getModelValue($attribute), [
                'class' => $class,
                'id' => $attribute,
            ]),
            ['class' => 'mb-3']
        );

        return $field;
    }

    private function getModelValue(string $attribute): mixed
    {
        return $this->model->getAttributeValue($attribute);
    }
}

A widget for navigation breadcrumbs:

php
<?php

declare(strict_types=1);

namespace App\Widget;

use Yiisoft\Html\Html;
use Yiisoft\Router\UrlGeneratorInterface;
use Yiisoft\Widget\Widget;

final class BreadcrumbWidget extends Widget
{
    private array $items = [];
    private string $separator = '';
    private array $options = [];

    public function __construct(
        private UrlGeneratorInterface $urlGenerator,
    ) {}

    public function items(array $items): self
    {
        $new = clone $this;
        $new->items = $items;
        return $new;
    }

    public function separator(string $separator): self
    {
        $new = clone $this;
        $new->separator = $separator;
        return $new;
    }

    public function options(array $options): self
    {
        $new = clone $this;
        $new->options = $options;
        return $new;
    }

    protected function render(): string
    {
        if (empty($this->items)) {
            return '';
        }

        $links = [];
        $itemCount = count($this->items);
        
        foreach ($this->items as $index => $item) {
            if ($index === $itemCount - 1) {
                // Last item (current page) - no link
                $links[] = Html::span($item['label'], ['class' => 'breadcrumb-item active']);
            } else {
                // Regular breadcrumb item with link
                $url = isset($item['url']) 
                    ? (is_array($item['url']) 
                        ? $this->urlGenerator->generate($item['url'][0], $item['url'][1] ?? [])
                        : $item['url'])
                    : '#';
                    
                $links[] = Html::span(
                    Html::a($item['label'], $url),
                    ['class' => 'breadcrumb-item']
                );
            }
        }

        $options = array_merge(['class' => 'breadcrumb'], $this->options);

        return Html::nav(
            Html::ol(implode('', $links), ['class' => 'breadcrumb']),
            $options
        );
    }
}

Widget Best Practices

Configuration Pattern

Use immutable configuration methods:

php
public function someProperty($value): self
{
    $new = clone $this;
    $new->someProperty = $value;
    return $new;
}

Validation

Validate widget configuration in the render method:

php
protected function render(): string
{
    if (empty($this->items)) {
        throw new InvalidArgumentException('Items cannot be empty.');
    }
    
    // ... render logic
}

HTML Encoding

Always encode user data:

php
protected function render(): string
{
    return Html::div(Html::encode($this->userContent), [
        'class' => 'user-content',
    ]);
}

Asset Management

Register widget-specific assets:

php
<?php

declare(strict_types=1);

namespace App\Widget;

use App\Asset\ChartWidgetAsset;
use Yiisoft\Assets\AssetManager;
use Yiisoft\Widget\Widget;

final class ChartWidget extends Widget
{
    public function __construct(
        private AssetManager $assetManager,
    ) {}

    protected function render(): string
    {
        // Register required assets
        $this->assetManager->register(ChartWidgetAsset::class);
        
        // Render widget content
        return $this->renderChart();
    }
}

Using Widgets in Layouts

Widgets are particularly useful in layouts for common UI elements:

views/layout/main.php

php
<?php

declare(strict_types=1);

use App\Widget\NavigationWidget;
use App\Widget\FooterWidget;
use App\Widget\UserMenuWidget;

/**
 * @var \Yiisoft\View\WebView $this
 * @var string $content
 */
?>
<!DOCTYPE html>
<html>
<head>
    <!-- head content -->
</head>
<body>
    <header>
        <?= NavigationWidget::widget() ?>
        <?= UserMenuWidget::widget() ?>
    </header>
    
    <main>
        <?= $content ?>
    </main>
    
    <footer>
        <?= FooterWidget::widget() ?>
    </footer>
</body>
</html>

Testing Widgets

Unit Testing

Test widget rendering logic:

php
<?php

declare(strict_types=1);

namespace App\Tests\Widget;

use App\Widget\AlertWidget;
use PHPUnit\Framework\TestCase;

final class AlertWidgetTest extends TestCase
{
    public function testRenderWithMessage(): void
    {
        $widget = AlertWidget::widget()
            ->message('Test message')
            ->type('success');
            
        $output = $widget->render();
        
        $this->assertStringContainsString('Test message', $output);
        $this->assertStringContainsString('alert-success', $output);
    }
    
    public function testRenderEmpty(): void
    {
        $widget = AlertWidget::widget();
        $output = $widget->render();
        
        $this->assertEmpty($output);
    }
}

Common Widget Patterns

Conditional Rendering

php
protected function render(): string
{
    if (!$this->shouldRender()) {
        return '';
    }
    
    return $this->doRender();
}

private function shouldRender(): bool
{
    return !empty($this->items) && $this->isVisible;
}

Template-based Rendering

php
protected function render(): string
{
    return $this->renderTemplate('widget-template', [
        'items' => $this->items,
        'options' => $this->options,
    ]);
}

private function renderTemplate(string $template, array $data): string
{
    // Use a view renderer to render template files
    return $this->viewRenderer->renderPartialAsString($template, $data);
}

Event Integration

php
protected function render(): string
{
    $event = new BeforeWidgetRender($this);
    $this->eventDispatcher->dispatch($event);
    
    $content = $this->doRender();
    
    $afterEvent = new AfterWidgetRender($this, $content);
    $this->eventDispatcher->dispatch($afterEvent);
    
    return $afterEvent->getContent();
}

Widgets provide a powerful way to create reusable, configurable UI components in Yii3 applications. They help maintain clean separation of concerns and make your views more maintainable and testable.