Доступна оплата мастер-классов российскими и иностранными картами, ЮMoney, быстрыми платежами СБП и SberPay

Живой Layout или Упрощаем темизацию в Yii

Файлы темы

Каждый «ленивый» разработчик в глубине души желает упростить себе работу с создаваемой им системой. Как вы, наверное, заметили, в каждом совете этого блога автор руководствуется привычкой выносить всё общее и всё изменяемое в отдельные самодостаточные компоненты (в поведения, действия, валидаторы, фильтры, виджеты, переопределённые базовые классы). В итоге такая практика приводит к состоянию, когда порой несколько разнородных проектов работают на одном и том же коде с различием только в теме оформления. В этот раз поговорим именно о темах.

Использование тем в Yii

Известно, что для включения темы оформления необходимо указать её имя в параметре theme:

return array(
    'basePath'=>dirname(dirname(__FILE__)),
    'name'=>'My Site',
    'theme'=>'classic',
    // ...
);

Работа с темами описывается в руководстве, но мы рассмотрим несколько моментов и здесь.

Итак, если у нас есть контроллер ShopController

class ShopController extends Controller
{    
    public function actionIndex(){
        $this->render('index');
    }    
    public function actionCategory($id){...}    
    public function actionSearch(){...}    
    public function actionCart(){...}    
    public function actionShow($id){...}
}

то мы можем положить представление index.php либо в исходную директорию protected/views:

protected/views/shop/index.php

либо в директорию представлений темы protected/views:

themes/classic/views/shop/index.php

Надеюсь, что Вы уже перешли на использование модулей в своих проектах, поэтому дальше будем говорить о них.

Пусть у нас имеется модуль shop, содержащий три контроллера:

class DefaultController extends Controller
{    
    public function actionIndex(){
        $this->render('index');
    }    
    public function actionCategory($id){...}    
    public function actionSearch(){...}  
}
 
class ProductController extends Controller
{       
    public function actionShow($id){...}
}
 
class CartController extends Controller
{    
    public function actionIndex(){...}    
    public function actionOrder(){...}    
}

Представления для модуля можно также помещать либо в папку views самого модуля, либо в папку views/shop темы.

Если в модуле есть файл представления

protected/modules/shop/views/default/index.php

то для его замены нужно создать одноимённый файл в папке темы:

themes/classic/views/shop/default/index.php

И при вызове

$this->render('index');

вместо представления модуля подставится представление из темы.

Здесь «/shop/» – имя модуля, а «/default/» – имя контроллера.

То есть если нам нужно сделать несколько тем для одного сайта (например, полная и мобильная версия) или несколько сайтов на одном движке, то достаточно создать несколько тем и перекрывать в них представления любых модулей.

Какие нюансы нам нужно знать

Используя $this->render(...) или $this->renderPartial(...) можно порой пойти на некоторые хитрости.

Стандартное включение – указание имени файла представления:

$this->render('index', array('model'=>$model));

Если представлений много, то их можно разложить по папкам и вызывать по относительному пути, используя точку или слэш:

<?php
<?php $this->renderPartial('_filter'); ?>
<?php $this->renderPartial('forms/_form1'); ?>
<?php $this->renderPartial('forms._form2'); ?>

Лучше использовать сразу слэши, так как иначе Yii попытается по точкам разобрать псевдоним (а именно будет пробовать разные варианты: искать модуль forms и т.д.).

Аналогично можно «гулять» по иерархиям папок:

<?php
<?php $this->renderPartial('../filters/_filter'); ?>

Можно в папке protected/views или themes/classic/views насоздавать папок и накидать туда общих шаблонов, и включать их вызовом от корня, используя два слэша:

<?php
<?php $this->renderPartial('//sidebars/shop_sidebar_with_categories'); ?>

Например, там удобно хранить лэйауты и сайдбары для них. Мы, кстати, это уже рассматривали для организации разных сайдбаров.

Представим, что на сайте есть комментарии для записей блога, для товаров и для новостей, а мы хотим управлять ими отдельно в разделе новостей, блогов и товаров.

В простейшем случае можно просто скопировать контроллеры и представления в каждый модуль. Но вместо этого можно использовать один общий контроллер и общие представления из модуля comment.

Создадим базовый класс CommentAdminControllerBase для контроллеров, управляющих комментариями, и создадим общее представление comment.views.commentAdmin.index:

// protected/modules/comment/components/CommentAdminControllerBase.php
 
abstract class CommentAdminControllerBase
{
    protected $type = '';
 
    public function actionIndex()
    {
        $model = new Comment('search');        
        $model->unsetAttributes();
 
        if(isset($_POST['Comment']))
            $model->attributes = $_POST['Comment'];
 
        $model->type = $this->type;
 
        $this->render('comment.views.commentAdmin.index', array('model'=>$model);
    }    
}

Теперь в модулях блога, новостей и товаров поместим пустые контоллеры-наследники:

// protected/modules/blog/controllers/CommentAdminController.php
 
Yii::import('comment.components.CommentAdminControllerBase');
 
class CommentAdminController extends CommentAdminControllerBase
{
    protected $type = 'Post';
}

Теперь в каком бы модуле мы не управляли комментариями, будет использоваться для вывода единственное представление comment.views.commentAdmin.index.

Таким образом, мы можем использовать стандартную адресацию с использованием псевдонимов для подключения представлений, расположенных в другом месте.

Нужно иметь в виду тот факт, что при доступе к представлениям модулей по жёстким псевдонимам

'comment.views.commentAdmin.index'
'application.views.superview'
'web.foo.bar'

не будет работать темизация. То есть не будет возможности переопределить файл index.php в теме. Если возникнет такая необходимость, то лучше полностью вынести ленту комментариев в файл в теме themes/<имя_темы>/views/comment/commentAdmin/index.php и подключать по пути от корня уже его:

'//comment/commentAdmin/index'

От этого теперь можно перейти непосредственно к сути проблемы.

Где указывать layout в Yii

От отдельных представлений пора перейти к работе с шаблонами. На разных сайтах, да и порой в разных разделах одного сайта могут быть разные шаблоны. Стандартной пары column1.php и column2.php здесь явно не хватит. Где же хранить шаблоны? В общей папке layouts или внутри модулей? Где именно указывать, какой шаблон на нужен?

Рассмотрим несколько подходов к задаче и выберем наиболее удобный.

Указание шаблона в контроллере

Демо-блог и руководство учит нас указывать layout в контроллере. Это естественно, так как это на самом деле и есть публичное поле контроллера. Стандартное подключение шаблона column2.php из папки protected/views/layouts или themes/<theme>/views/layouts выглядит так:

class Controller extends СController
{
    public $layout = '//layouts/column2';
}

Все наши контроллеры наследуются от Controller, поэтому мы можем легко изменить шаблон оформления для любого контроллера. Например, магазин мы выведем в шаблоне views/layouts/shop.php:

class ShopController extends Controller
{
    public $layout = '//layouts/shop';
}

И даже больше: для корзины, заказа, поиска и страницы товара сделаем свои шаблоны:

class ShopController extends Controller
{
    public $layout = '//layouts/shop/all';
 
    public function actionIndex(){...}
 
    public function actionCategory($id){...}
 
    public function actionBrand($id){...}
 
    public function actionSearch(){
        $this->layout = '//layouts/shop/search';
        // ...
    }
 
    public function actionCart(){
        $this->layout = '//layouts/shop/cart';
        // ...
    }
 
    public function actionOrder(){
        $this->layout = '//layouts/shop/order';
        // ...
    }
 
    public function actionShow($id){
        $this->layout = '//layouts/shop/show';
        // ...
    }
}

А ещё мы используем модули, поэтому можем запросто создать папку protected/modules/shop/views/layouts внутри модуля, накидать туда наши лэйауты и брать прямо оттуда:

class DefaultController extends Controller
{
    public $layout = 'all';
 
    public function actionIndex(){...}    
    public function actionCategory($id){...}    
    public function actionBrand($id){...}
 
    public function actionSearch(){
        $this->layout = 'search';
        // ...
    }
}
 
class ProductController extends Controller
{
    public $layout = 'all';
 
    public function actionShow($id){
        $this->layout = 'show';
        // ...
    }
} 
 
class CartController extends Controller
{
    public $layout = 'all';
 
    public function actionIndex(){
        $this->layout = 'cart';
        // ...
    }
    public function actionOrder(){
        $this->layout = 'order';
        // ...
    }
}

Теперь для другой темы можно переопределить необходимые файлы в папке темы, а именно в themes/<имя_темы>/views/shop/layouts.

Но что если в другой теме для другого сайта нужно указать специфический шаблон для вывода списка производителей (действие actionBrand)?

Придётся поместить строку

$this->layout = 'brand'

в метод DefaultController::actionBrand и добавить заглушку brand.php во все остальные сайты. чтобы на них не выскакивала ошибка, что представление brand.php не найдено.

Вот мы и подошли к проблеме. Одному сайту нужно «украсить» десять действий в трёх контроллерах, второму только корзину магазина, а третьему и одного шаблона хватит. Но всё равно в каждую тему нужно добавить десять. Если новая тема «захочет» использовать одиннадцатый лэйаут, то контроллер опять изменится, и во все остальные темы снова придётся добавлять одиннадцатую заглушку.

Указание шаблонов в представлении

Контроллеры у нас общие для всех сайтов, поэтому трогать их не стоит. А что если указывать шаблон в самих представлениях?

Действительно, в представлении themes/<theme>/views/shop/default/index.php присваиваем нужное значение полю $this->layout и всё:

<?php
<?php
$this->layout = 'index';
$this->pageTitle = 'Магазин';
$this->breadcrumbs=array(
    'Магазин',
);
?>

Теперь вместо themes/<theme>/views/layouts/column2.php главная страница каталога товаров выведется в шаблоне themes/<theme>/views/shop/layouts/index.php из папки модуля в теме.

Такое избавление от жёстко вписанных литералов в контроллере позволяет нам использовать один и тот же контроллер в разных сайтах без изменения его кода.

Но вот в чём парадокс:

Мы договорились раньше, что у нас есть три контроллера

class DefaultController extends Controller
{    
    public function actionIndex(){...}    
    public function actionCategory($id){...}    
    public function actionBrand($id){...}   
    public function actionSearch(){...} 
}
 
class ProductController extends Controller
{    
    public function actionShow($id){...}
}
 
class CartController extends Controller
{    
    public function actionCart(){...}    
    public function actionOrder(){...}
}

На самом деле в модуле интернет-магазина их может быть намного больше. Но так как присвоение значения полю $this->layout производится в представлениях, то чтобы изменить шаблон всего модуля магазина в нашей теме нам нужно переопределить абсолютно все представления всех наших контроллеров. То есть не один-два, а несколько десятков! И так при необходимости для каждого модуля.

Недостатки статической темизации

Итак, мы рассмотрели присваивание имени шаблона в контроллере и в представлении.

Первый способ слишком «хардкорный», так как литералы (имена шаблонов) вписаны прямо в код нашего приложения. А это сама по себе не очень хорошая практика в разработке системы, которую ожидается использовать в более чем одном проекте.

Второй способ более гибкий, так как представления легко переопределяются в теме (в отличие от контроллеров). Но представлений очень много, и порой их надо переопределять большими пачками, так как чтобы изменить тему любого раздела сайта нужно переопределить все представления этого модуля.

Какой может быть выход?

Выход – наличие возможности указывать шаблон вне конкретного контроллера и вне представления.

Это может быть таблица соответствия шаблонов конкретным модулям, контроллерам и действиям, оформленная в виде хэш-массива в конфигурационном файле:

array(
    'Модуль1' => 'Шаблон1',
    'Модуль1:Контроллер1' => 'Шаблон2',
    'Модуль1:Контроллер1:Действие1' => 'Шаблон3',
    'Модуль2' => 'Шаблон1',
    'Модуль3:Контроллер1:Действие1' => 'Шаблон4',
);

Соответственно, эта таблица должна храниться в файле, помещённом в папку темы, а базовый контроллер в событии beforeRender должен выбрать по этой таблице нужный шаблон.

Автозагрузка layout'ов

В идеале система темизации должна всё делать автоматически. С одноимёнными представлениями и шаблонами это так и происходит. Достаточно закинуть в папку с темой переопределённое представление, так оно сразу используется вместо оригинального.

По подобному пути «автоподхвата» мы и пойдём.

Для начала уберём присваивание $this->layout из контроллеров и из представлений.

В простейшем случае наш модуль магазина будет иметь такую структуру:

shop/
    controllers/
        DefaultController.php
        ProductController.php
        CartController.php
    models/
    views/
        default/
        product/
        cart/
    ShopModule.php

И в текущей теме мы переопределили, например, представление действия DefaultController::actionIndex:

images/
css/
views/
    shop/
        default/
            index.php

А теперь было бы неплохо создать папку layouts для шаблонов и поместить их туда:

images/
css/
views/
    shop/
        default/
            index.php
        layouts/
            shop.php
            shop_default_search.php
            shop_cart.php

Для удобства мы назвали файлы по принципу

<модуль>
<модуль>_<контроллер>
<модуль>_<контроллер>_<действие>

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

Теперь напишем систему автоподгрузки шаблона:

/**
 * @author ElisDN <mail@elisdn.ru>
 * @link https://elisdn.ru
 */
class DLiveLayoutBehavior extends CBehavior
{
    public function initLayout()
    {
        $owner = $this->getOwner();
 
        if (empty($owner->layout))
        {
            $theme = Yii::app()->theme->getName();
            $module = $owner->getModule()->getId();
            $controller = $owner->getId();
            $action = $owner->getAction()->getId();
 
            $cacheId = "layout_{$theme}_{$module}_{$controller}_{$action}";
 
            if (!$owner->layout = Yii::app()->cache->get($cacheId))
            {
                $layouts = array(
                    "webroot.themes.{$theme}.views.{$module}.layouts.{$module}_{$controller}_{$action}",
                    "application.modules.{$module}.views.layouts.{$module}_{$controller}_{$action}",
                    "webroot.themes.{$theme}.views.{$module}.layouts.{$module}_{$controller}",
                    "application.modules.{$module}.views.layouts.{$module}_{$controller}",
                    "webroot.themes.{$theme}.views.{$module}.layouts.{$module}",
                    "application.modules.{$module}.views.layouts.{$module}",
                    "webroot.themes.{$theme}.views.layouts.default",
                    "application.views.layouts.default",
                );
 
                foreach ($layouts as $layout)
                {
                    if (file_exists(Yii::getPathOfAlias($layout) . '.php'))
                    {
                        $owner->layout = $layout;
                        break;
                    }
                }
 
                Yii::app()->cache->set($cacheId, $owner->layout, 3600 * 24);
            }
        }
    }
}

Это поведение добавляет к контроллеру метод initLayout, который, как мы видим, по очереди ищет подходящий шаблон в теме и в оригинальном модуле и присваивает первый найденный. Последние две строки списка должны указывать на шаблон по умолчанию. Также, чтобы не искать каждый раз заново, история найденных файлов кешируется.

Поведение мы должны подключить к базовому контроллеру и вызывать его метод перед генерацией страницы, то есть в событии beforeRender контроллера:

class Controller extends СController
{
    public function behaviors()
    {
        return array_merge(parent::behaviors(), array(
            'DLiveLayoutBehavior'=>array('class'=>'DLiveLayoutBehavior'),
        ));
    }
 
    protected function beforeRender($viev)
    {
        $this->initLayout();
        return parent::beforeRender($viev);
    }
}

Заметим, что метод сработает только когда CController::layout пустой. Так что Вы как и раньше сможете присвоить $this->layout любое значение в контроллере или в представлении вручную и оно не перезапишется.

Если Вы не хотите возиться с поведением, то скопируйте код метода initLayout прямо в контроллер, заменив в нём $onwer на $this.

Итак, чтобы заменить шаблон магазина на сайте с двухколоночного на трёхколоночный, просто добавьте в тему один файл:

themes/<имя_темы>/views/shop/layouts/shop.php

А чтобы сделать страницу корзины во всю ширину добавьте один шаблон конкретно для соответствующего действия:

themes/<имя_темы>/views/shop/layouts/shop_cart_index.php

В итоге теперь шаблоны подхватываются из папки layuots самого модуля и из папки layuots темы этого модуля автоматически, и почти всё оформление любого из ваших сайтов можно настроить прямо в теме.

Комментарии

 

Виталий Иванов

Интересное решение

Ответить

 

Михаил

А где тогда используется файл с хэш массивом соответствий модулей/контроллера/действий шаблонам? В итоге ведь поведение ищет шаблон по маске имени файла...

Ответить

 

Дмитрий Елисеев

Нигде, так как файлы ищутся прямо через file_exists() по именам модуля/контроллера/действия:

"layouts.{$module}_{$controller}_{$action}",
"layouts.{$module}_{$controller}",
"layouts.{$module}",

То есть для страницы поста блога (BlogModule/PostController/actionView) шаблон выбирается из вариантов

/layouts/blog_post_view.php
/layouts/blog_post.php
/layouts/blog.php

И если ни в теме, ни в модуле ни одного из них не нашлось, то используется default.php из темы или ядра.

Ответить

 

icemen – icemen.ru

Вы будете смеяться, но вы написали единственный толковый пост по темизации YII во всем рунете). Разрешите остаться вашим преданным читателем.

Ответить

 

Дмитрий Елисеев

Ухты! Разрешаю :)

Ответить

 

Евгений – e-aktec.org

Нет, действительно замечательный, думаю почти полностью исчерпывающий пост. Прекрасно разясняете!)

Кстате в чем-то теперь способ выбора шаблона напоминает Drupal-овскую систему)

Ответить

 

Igor – www.y-wave.ru

А куда пихать код системы автоподгрузки шаблонов?

Ответить

 

Дмитрий Елисеев

В отдельный класс. Потом подключить к базовому контроллеру как поведение и вызывать его в beforeRender. Всё как в последнем примере кода.

Ответить

 

Игорь

Глупый вопрос, а класс в какую папку пихать?

Ответить

 

Дмитрий Елисеев

protected/components

Ответить

 

Igor – www.y-wave.ru

Еще вопросик. А как заменить шаблоны которые в project/protected/views/layouts/column1.php или column2.php
В смысле в какю папку в themes.
Я правильно понимаю project/protected/themes/theme_name/views/layouts/column1.php?

Ответить

 

Igor – www.y-wave.ru

Все заработало!

Ответить

 

almix – loco.ru

Дмитрий, спасибо за классную статью. Какой вариант лучше, если несколько сайтов и они используют одну и ту же тему, но различаются их layouts?

Ответить

 

Дмитрий Елисеев

Можно добавить выбор по имени домена:

switch (Yii::app()->request->hostInfo) {
    case 'http://site1.com':
        $this->layout = '//layouts/layout1';
        break;
    case 'http://site2.com':
        $this->layout = '//layouts/layout2';
        break;
}
Ответить

 

kevin7

Дмитрий, у меня два вопроса:

1) Файлы в папке site/views и site/themes/theme_name/views должны быть полностью одинаковыми или site/views должен только обращаться за всем необходимым к site/themes/theme_name/views?

2) Если у меня главный сайт и субдомены, тогда основная папка с темой должна находиться на главном сайте, а на субдоменах должно быть просто обращение к теме на главном сайте или как? (смысл: на главном сайте должна быть общая тема, а на субдоменах она может использоваться полностью или частично).

Ответить

 

Дмитрий Елисеев

1) Одинаковые не нужны. Yii ищет каждый файл сначала в теме, а если не находит, то берёт из protected/views. Как для render(), так и для renderPartial().

2) Yii не поддерживает наследование одной темы от другой. Общие файлы оставьте прямо в protected/views, тему главного сайта оставьте без файлов представлений, а в темы themes/subdomain/views для субдоменов добавьте только те файлы, которые нужно перекрыть.

Ответить

 

kevin7

Спасибо, сразу стало все понятно)

Ответить

 

Максим

В DLiveLayoutBehavior стоит добавить проверку:

if (empty($owner->layout) && ($owner->getModule() instanceof CModule)) {...}

Тогда при наследовании Controller-а контроллерами из //protected/controllers не будет выдана ошибка, мол нет $owner->getModule()

Ответить

 

Павел

public $layout - везде $ забыл:)

Ответить

 

Дмитрий Елисеев

Спасибо! Исправил.

Ответить

 

Akulenok

Спасибо, отличный пост, выручили!

Ответить

 

bobpps

Большое спасибо!

Ответить

 

Игорь

А есть для YII2?

Ответить

 

des1roer

По моему, вы не знаете что означает слово лень.
Ленивая это - качаем тему, устанавливаем

return array(
    'theme'=>'shadow_dancer',
);

PROFIT!

Ответить

 

Игорь Мастер

Дмитрий, возможно ли написать статью про организацию контроллеров, сервисов к ним и репеозиториев с моделями?

Ответить

 

Дмитрий Елисеев

Много чего об этом уже на форуме обсудили.

Ответить

 

Игорь Мастер

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

Ответить

 

Дмитрий Елисеев

Уже написаны кучи книг и статей по принципам программирования и архитектуре ПО за 20+ лет. Копипастить всё в ещё одну статью не вижу смысла.

Ответить

 

Игорь Мастер

Нет, так нет .

Ответить

Оставить комментарий

Войти | Завести аккаунт | Войти через


(никто не увидит)





Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>