Yii1.1 docs ru

GitHub - sbadulin/yii1.1-docs-ru
Repo URL: https://github.com/sbadulin/yii1.1-docs-ru
Edited by:
Cover image: Cover image
Share this using: email, Google+, Twitter, Facebook.
Exports: EPUB

1 Приложение

Объект приложения (application) инкапсулирует контекст выполнения запроса.
Основная задача приложения — собрать информацию о запросе и передать
её соответствующему контроллеру для дальнейшей обработки. Также приложение
является централизованным хранилищем конфигурации приложения. Именно поэтому объект
приложения также называют фронт-контроллером.

Объект приложения создаётся входным скриптом как одиночка (singleton).
Экземпляр приложения доступен из любой его точки посредством [Yii::app()|YiiBase::app].

1.1 Конфигурация приложения

По умолчанию объект приложения — это экземпляр класса [CWebApplication], который
может быть настроен с использованием конфигурационного файла (или массива).
Необходимые значения свойств устанавливаются в момент создания экземпляра приложения.
Альтернативный путь настройки приложения — расширение класса [CWebApplication].

Конфигурация — это массив пар ключ-значение, где каждый ключ представляет собой
имя свойства экземпляра приложения, а значение — начальное значение соответствующего свойства.
Например, следующая конфигурация устанавливает значения свойств приложения
[name|CApplication::name] и [defaultController|CWebApplication::defaultController]:

[php]
array(
    'name'=>'Yii Framework',
    'defaultController'=>'site',
)

Стоит отметить, что приложение, как и большинство классов Yii, является компонентом.
Это означает что:

  • Вы не можете присваивать значения не объявленным в классе свойствам.
  • Приложение поддерживает свойства, объявленные через геттеры и сеттеры, то есть можно сконфигурировать свойство, заданное
    [setImport|CModule::setImport] следующим образом:
[php]
array(
    'import'=>array(
        'application.components.*',
    ),
)

Обычно конфигурация хранится в отдельном PHP-скрипте
(например, protected/config/main.php). Скрипт возвращает конфигурационный массив:

[php]
return array(…);

Чтобы воспользоваться конфигурацией, необходимо передать имя конфигурационного
файла в качестве аргумента конструктору приложения или методу
[Yii::createWebApplication()], как показано ниже.
Обычно это делается во входном скрипте:

[php]
$app=Yii::createWebApplication($configFile);

Tip|Подсказка: Если конфигурация очень громоздкая, можно разделить ее на несколько файлов,
каждый из которых возвращает часть конфигурационного массива. Затем в основном конфигурационном
файле необходимо подключить эти файлы, используя include(), и соединить массивы-части в единый
конфигурационный массив.

1.2 Базовая директория приложения

Базовой директорией приложения называется корневая директория, содержащая все основные, с точки
зрения безопасности, PHP-скрипты и данные. По умолчанию это поддиректория protected, находящаяся
в директории, содержащей входной скрипт. Изменить её местоположение можно, установив свойство
[basePath|CWebApplication::basePath] в конфигурации приложения.

Содержимое базовой директории должно быть закрыто от доступа из веб.
При использовании веб-сервера Apache HTTP server
это можно сделать путем добавления в базовую директорию файла .htaccess следующего содержания:

deny from all

1.3 Компоненты приложения

Функциональность объекта приложения может быть легко модифицирована и расширена благодаря компонентной архитектуре.
Приложение управляет набором компонентов, каждый из которых реализует набор определённых возможностей.
Например, приложение производит предварительную обработку запроса пользователя, используя компоненты [CUrlManager] и [CHttpRequest].

Изменяя значение свойства [components|CApplication::components], можно настроить классы и значения свойств
любого компонента, используемого приложением. Например, можно сконфигурировать компонент CMemCache так, чтобы
он использовал несколько memcache-серверов для кэширования:

[php]
array(
    …
    'components'=>array(
        …
        'cache'=>array(
            'class'=>'CMemCache',
            'servers'=>array(
                array('host'=>'server1', 'port'=>11211, 'weight'=>60),
                array('host'=>'server2', 'port'=>11211, 'weight'=>40),
            ),
        ),
    ),
)

В данном примере мы добавили элемент cache к массиву components.
Элемент cache указывает, что классом компонента является CMemCache, а также устанавливает его свойство
[servers|CMemcache::servers].

Для доступа к компоненту приложения используйте Yii::app()->ComponentID, где
ComponentID — это идентификатор компонента (например, Yii::app()->cache).

Компонент может быть деактивирован путем установки параметра enabled в его конфигурации равным false.
При обращении к деактивированному компоненту будет возвращен null.

Tip|Подсказка: По умолчанию компоненты приложения создаются по требованию.
Это означает, что экземпляр компонента может быть не создан вообще в случае,
если это не требуется при обработке пользовательского запроса. В результате общая
производительность приложения может не пострадать, даже если в конфигурации
указано множество компонентов.

При необходимости обязательного создания экземпляров компонентов (например, [CLogRouter])
вне зависимости от того, используются они или нет, укажите их идентификаторы в
значении конфигурационного свойства [preload|CApplication::preload].

1.4 Ключевые компоненты приложения

Yii предопределяет набор компонентов ядра, которые предоставляют возможности,
необходимые для большинства веб-приложений. Например, компонент [request|CWebApplication::request]
используется для сбора информации о запросе пользователя и предоставляет
различную информацию, такую как URL и cookies. Задавая свойства компонентов,
можно изменять стандартное поведение Yii практически как угодно.

Далее перечислены ключевые компоненты, предопределенные классом [CWebApplication]:

  • [assetManager|CWebApplication::assetManager]: [CAssetManager] — управляет публикацией файлов ресурсов (asset files);

  • [authManager|CWebApplication::authManager]: [CAuthManager] — контролирует доступ на основе ролей (RBAC);

  • [cache|CApplication::cache]: [CCache] — предоставляет возможности кэширования данных; учтите, что вы
    должны указать используемый класс (например, CMemCache, CDbCache), иначе при обращении к компоненту будет возвращен null;

  • [clientScript|CWebApplication::clientScript]: [CClientScript] — управляет клиентскими скриптами (JavaScript и CSS);

  • [coreMessages|CApplication::coreMessages]: CPhpMessageSource — предоставляет переводы системных сообщений Yii-фреймворка;

  • [db|CApplication::db]: CDbConnection — обслуживает соединение с базой данных; обратите внимание, что
    для использования компонента необходимо установить свойство [connectionString|CDbConnection::connectionString];

  • [errorHandler|CApplication::errorHandler]: [CErrorHandler] — обрабатывает не пойманные ошибки и исключения PHP;

  • [format|CApplication::format]: [CFormatter] — форматирует данные для их последующего отображения.

  • [messages|CApplication::messages]: CPhpMessageSource — предоставляет переводы сообщений, используемых в Yii-приложении;

  • [request|CWebApplication::request]: [CHttpRequest] — содержит информацию о пользовательском запросе;

  • [securityManager|CApplication::securityManager]: [CSecurityManager] — предоставляет функции,
    связанные с безопасностью (например, хеширование, шифрование);

  • [session|CWebApplication::session]: [CHttpSession] — обеспечивает функциональность, связанную с сессиями;

  • [statePersister|CApplication::statePersister]: [CStatePersister] — предоставляет метод для сохранения
    глобального состояния;

  • [urlManager|CWebApplication::urlManager]: [CUrlManager] — предоставляет функции парсинга и формирования URL;

  • [user|CWebApplication::user]: [CWebUser] — предоставляет идентификационную информацию текущего пользователя;

  • [themeManager|CWebApplication::themeManager]: [CThemeManager] — управляет темами оформления.

1.5 Жизненный цикл приложения

Жизненный цикл приложения при обработке пользовательского запроса выглядит следующим образом:

  1. Предварительная инициализация приложения через [CApplication::preinit()].

  2. Инициализация обработчика ошибок.

  3. Регистрация компонентов ядра.

  4. Загрузка конфигурации приложения.

  5. Инициализация приложения [CApplication::init()]:
    • регистрация поведений приложения;
    • загрузка статических компонентов приложения.
  6. Вызов события [onBeginRequest|CApplication::onBeginRequest].

  7. Обработка запроса:
    • сбор информации о запросе;
    • создание контроллера;
    • запуск контроллера.
  8. Вызов события [onEndRequest|CApplication::onEndRequest].
    Лучшие практики MVC
    ===================

Несмотря на то что с концепцией MVC знаком практически каждый веб-разработчик, её
применение в реальных проектах часто вызывает затруднения. Главная идея
MVC — повторное использование кода и разделение проблем. В данном разделе
будут описаны общие принципы, которые помогут следовать MVC в вашем приложении.

Предположим, что веб-приложение состоит из нескольких подприложений, таких как:

  • front end: часть сайта, которую видят обычные пользователи;
  • back end: административная часть сайта, позволяющая управлять приложением.
    Доступ к ней обычно ограничен;
  • консоль: приложение, состоящее из набора консольных команд, запускаемых
    в окне терминала вручную или по расписанию;
  • API: предоставляет сторонним приложениям интерфейсы для интеграции с вашим
    приложением.

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

1.6 Модель

Модели представляют внутреннюю структуру данных приложения.
Они часто являются общими для нескольких подприложений.
Например, модель LoginForm может быть использована как в пользовательской, так
и в административной части приложения. Модель News может использоваться
консольными командами, API и front/back частями приложения. Поэтому модели

  • должны содержать свойства, представляющие конкретные данные;

  • должны включать в себя бизнес-логику (например, правила валидации), чтобы убедиться в том,
    что данные соответствуют предъявленным требованиям;

  • могут содержать код для работы с данными. К примеру, модель SearchForm,
    помимо хранения поисковых данных, может содержать метод search, который этот поиск
    осуществляет.

Иногда следование последнему правилу делает модель очень
толстой, то есть содержащей очень много кода в одном классе. Это может привести
к трудностям поддержки кода в том случае, если модель используется для выполнения
различных задач. К примеру, модель News может содержать метод getLatestNews,
который используется только пользовательской частью и метод getDeletedNews,
который используется только административной частью. Для небольших и средних
приложений это допустимо. Для крупных же приложений в целях упрощения
дальнейшей поддержки кода можно сделать следующее:

  • Создать модель NewsBase, содержащую только код, общий для подприложений
    (пользовательской и административной частей).

  • В каждом подприложении создать модель News, наследуемую от NewsBase и
    определить в ней специфичные для подприложения методы.

Таким образом, если применить это к рассмотренному выше примеру, необходимо
добавить модель News с методом getLatestNews в пользовательскую часть и
ещё одну модель News с методом getDeletedNews в административную часть.

В общем случае, модели не должны напрямую взаимодействовать с пользователем. То есть:

  • не должны использовать $_GET, $_POST или другие подобные переменные, напрямую
    получаемые из запроса пользователя, так как модели могут использоваться в
    совершенно других подприложениях (например, в модульных тестах или API), в
    которых эти переменные недоступны. Все переменные, относящиеся к запросу
    пользователя, должны обрабатываться в контроллере;

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

1.7 Представление

Представления отвечают за отображение моделей в
необходимом пользователю формате. В общем случае представления

  • должны, главным образом, содержать разметку, такую как HTML, и простой PHP код,
    используемый для обхода, форматирования и отображения данных;

  • не должны напрямую обращаться к базе данных. Этим должны заниматься модели;

  • не должны напрямую обращаться к $_GET, $_POST и другим переменным, получаемым
    из запроса пользователя. Эту задачу должен выполнять контроллер. Представления
    должны использоваться только для оформления данных, полученных от контроллера и модели;

  • могут напрямую обращаться к свойствам и методам контроллера или моделей.
    Однако это должно делаться только в целях отображения данных.

Представления можно использовать повторно несколькими способами:

  • Общий шаблон: в него можно вынести разметку, общую для всех страниц.
    Например, шапку и подвал.

  • Части шаблона: используются внутри других шаблонов и, как правило, не
    используются с общим шаблоном. К примеру, часть шаблона _form.php можно
    использовать для отображения формы ввода модели, которая будет использоваться
    как при её создании, так и при редактировании.

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

  • Хелперы (помощники): в шаблонах часто требуется выполнять небольшие задачи, такие как
    форматирование данных или генерация HTML-тегов. Вместо того чтобы вставлять
    код напрямую в шаблоны, можно поместить его в класс-хелпер и использовать в шаблонах
    этот класс. Пример такого подхода можно найти в классе [CHtml], который помогает генерировать
    часто используемый HTML код. Для того чтобы избежать явного подключения классов,
    хелперы можно разместить в отдельной директории, указанной в import.

1.8 Контроллер

Контроллеры — связующее звено, соединяющее
модели, представления и другие компоненты в рабочее приложение. Контроллер отвечает
за обработку запросов пользователя. Поэтому контроллер

  • может обращаться к $_GET, $_POST и другим переменным PHP, получаемым из
    запроса пользователя;

  • может создавать экземпляры моделей и управлять ими. К примеру, в типичном действии
    обновления модели контроллер может сначала создать экземпляр модели, затем
    заполнить его данными из $_POST и, в случае успешного сохранения модели, перенаправить
    браузер пользователя на страницу созданной модели. Стоит отметить, что само сохранение
    модели должно быть реализовано в классе модели, а не в контроллере;

  • не должен содержать SQL-запросы. Их лучше держать в моделях;

  • не должен содержать HTML и другую разметку. Её стоит вынести в представления.

В хорошо спроектированном MVC-приложении контроллеры обычно очень тонкие и
содержат только несколько десятков строк кода. В то же время, модели очень толстые
и содержат большую часть кода, связанную с обработкой данных, так как структура
данных и бизнес-логика, содержащиеся там, обычно довольно специфичны для конкретного
приложения. Логика контроллера, наоборот, довольно типична и может быть вынесена
в базовые классы.
Компонент
=========
Yii-приложения состоят из компонентов–объектов, созданных согласно спецификациям.
Компонент (component) — это экземпляр класса [CComponent] или производного от него.
Использование компонента, как правило, включает доступ к его свойствам, а также вызов и обработку его событий.
Базовый класс [CComponent] устанавливает правила, согласно которым определяются свойства и события.

1.9 Объявление и использование свойства компонента

Свойство компонента схоже с открытой переменной-членом класса (public member variable).
Мы можем читать или устанавливать его значение. Например:

[php]
$width=$component->textWidth; // получаем значение свойства textWidth
$component->enableCaching=true; // устанавливаем значение свойства enableCaching

Существует два разных способа определения свойства компонента. Первым способом является обычное объявление
открытой переменной-члена класса компонента так, как показано ниже:

[php]
class Document extends CComponent
{
    public $textWidth;
}

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

[php]
class Document extends CComponent
{
    private $_textWidth;
    protected $_completed=false;

    public function getTextWidth()
    {
        return $this->_textWidth;
    }

    public function setTextWidth($value)
    {
        $this->_textWidth=$value;
    }

    public function getTextHeight()
    {
        // вычисляет и возвращает высоту текста
    }

    public function setCompleted($value)
    {
        $this->_completed=$value;
    }
}

Компонент выше может быть использован следующим образом:

[php]
$document=new Document();

// мы можем как писать в, так и читать из textWidth
$document->textWidth=100;
echo $document->textWidth;

// значение textHeight мы можем только получать
echo $document->textHeight;

// значение completed мы можем только изменять
$document->completed=true;

При чтении свойства, которое не было объявлено публичным членом класса, Yii пытается
использовать методы-геттеры, т.е. для textWidth методом-геттером будет getTextWidth. Тоже самое
происходит и при изменении свойства, которое не было объявлено публичным членом класса.

Если существует метод-геттер, но метода-сеттера при этом объявлено не было, то свойство компонента
можно использовать только для чтения, в противном случае будет вызвано исключение. Обратное верно и для
свойств, доступных только для изменения.

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

Note|Примечание: Есть небольшая разница в определении свойства через методы и через простое
объявление переменной. В первом случае имя свойства не чувствительно к регистру,
во втором — чувствительно.

1.10 События компонента

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

Событие компонента объявляется путём создания метода с именем, начинающимся на on.
Так же как и имена свойств, заданных через методы чтения и записи, имена событий
не чувствительны к регистру. Следующий код объявляет событие onClicked:

[php]
public function onClicked($event)
{
    $this->raiseEvent('onClicked', $event);
}

где $event — это экземпляр класса [CEvent] или производного от него,
представляющего параметр события. К событию можно подключить обработчик как показано ниже:

[php]
$component->onClicked=$callback;

где $callback — это корректный callback-вызов PHP (см.
PHP-функцию call_user_func). Это может быть либо глобальная функция, либо метод класса.
В последнем случае вызову должен передаваться массив: array($object,'methodName').

Обработчик события должен быть определён следующим образом:

[php]
function methodName($event)
{
    …
}

где $event — это параметр, описывающий событие (передаётся методом raiseEvent()).
Параметр $event — это экземпляр класса [CEvent] или его производного.
Как минимум, он содержит информацию о том, кто вызвал событие.

Обработчик события может быть анонимной функцией,
требующей наличия версии PHP 5.3+. Например,

[php]
$component->onClicked=function($event) {
    …
}

Если теперь использовать метод onClicked(), то в нём будет вызвано событие onClicked.
Назначенный ему обработчик будет запущен автоматически.

Событию могут быть назначены несколько обработчиков.
При возникновении события обработчики будут вызваны в порядке их назначения.
Если в обработчике необходимо предотвратить вызов последующих обработчиков,
необходимо установить [$event->handled|CEvent::handled] в true.

1.11 Поведения компонента

Для компонентов реализован шаблон проектирования mixin,
что позволяет присоединить к ним одно или несколько поведений. Поведение
объект, чьи методы «наследуются» компонентом, к которому он присоединён. Под
«наследованием» здесь понимается наращивание функционала, а не наследование
в классическом смысле. К компоненту можно прикрепить несколько поведений и,
таким образом, получить аналог множественного наследования.

Поведения классов должны реализовывать интерфейс [IBehavior]. Большинство поведений могут быть созданы путём расширения
базового класса [CBehavior]. В случае если поведение необходимо прикрепить к модели, его можно
создать на основе класса [CModelBehavior] или класса [CActiveRecordBehavior], которые реализуют дополнительные,
специфические для моделей, возможности.

Чтобы использовать поведение, его необходимо прикрепить к компоненту путём вызова метода поведения
[attach()|IBehavior::attach]. После этого мы можем вызывать методы поведения через компонент:

[php]
// $name уникально идентифицирует поведения в компоненте
$component->attachBehavior($name,$behavior);
// test() является методом $behavior
$component->test();

К прикреплённому поведению можно обращаться как к обычному свойству компонента.
Например, если поведение с именем tree прикреплено к компоненту, мы можем получить
ссылку на объект поведения следующим образом:

[php]
$behavior=$component->tree;
// эквивалентно выражению:
// $behavior=$component->asa('tree');

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

[php]
$component->disableBehavior($name);
// выражение ниже приведет к вызову исключения
$component->test();
$component->enableBehavior($name);
// здесь все будет работать нормально
$component->test();

В случае когда два поведения, прикреплённые к одному компоненту, имеют методы с одинаковыми именами,
преимущество будет иметь метод поведения, прикреплённого раньше.

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

Свойства поведения также доступны из компонента, к которому оно присоединено.
Свойства включают в себя как открытые поля класса поведения, так и его методы чтения/записи (getters/setters).
Например, если поведение имеет свойство с именем xyz и привязано к компоненту
$a, то мы можем использовать выражение $a->xyz для доступа к этому свойству.
Контроллер
==========
Контроллер (controller) — это экземпляр класса [CController] или унаследованного
от него класса. Он создается объектом приложения в случае, когда пользователь его
запрашивает. При запуске контроллер выполняет соответствующее действие, что обычно
подразумевает создание соответствующих моделей и отображение необходимых представлений.
В самом простом случае действие — это метод класса контроллера, название
которого начинается на action.

У контроллера есть действие по умолчанию, которое выполняется в случае, когда
пользователь не указывает действие при запросе. По умолчанию это действие
называется index. Изменить его можно путём установки значения [CController::defaultAction].

Следующий код определяет контроллер site с действиями index (действие по
умолчанию) и contact:

[php]
class SiteController extends CController
{
    public function actionIndex()
    {
        // ...
    }

    public function actionContact()
    {
        // ...
    }
}

1.12 Маршрут

Контроллеры и действия опознаются по их идентификаторам.
Идентификатор контроллера — это запись формата path/to/xyz, соответствующая
файлу класса контроллера protected/controllers/path/to/XyzController.php, где xyz
следует заменить реальным названием класса (например, post соответствует
protected/controllers/PostController.php). Идентификатор действия — это название
метода без префикса action. Например, если класс контроллера содержит метод
actionEdit, то идентификатор соответствующего действия — edit.

Пользователь обращается к контроллеру и действию посредством маршрута (route).
Маршрут формируется путём объединения идентификаторов контроллера и действия,
отделенных косой чертой. Например, маршрут post/edit указывает на действие
edit контроллера PostController, и по умолчанию URL http://hostname/index.php?r=post/edit
приведёт к вызову именно этих контроллера и действия.

Note|Примечание: По умолчанию маршруты чувствительны к регистру.
Это возможно изменить путём установки свойства
[CUrlManager::caseSensitive] равным false в конфигурации приложения.
В режиме, не чувствительном к регистру, убедитесь, что названия директорий,
содержащих файлы классов контроллеров, указаны в нижнем регистре, а также,
что [controller map|CWebApplication::controllerMap] и [action map|CController::actions]
используют ключи в нижнем регистре.

Приложение может содержать модули. Маршрут к действию
контроллера внутри модуля задаётся в формате moduleID/controllerID/actionID.
Более подробно это описано в разделе о модулях.

1.13 Создание экземпляра контроллера

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

  • если установлено свойство [CWebApplication::catchAllRequest], контроллер будет создан
    на основе этого свойства, а контроллер, запрошенный пользователем, будет проигнорирован.
    Как правило, это используется для установки приложения в режим технического обслуживания
    и отображения статической страницы с соответствующим сообщением;

  • если идентификатор контроллера обнаружен в [CWebApplication::controllerMap], то для
    создания экземпляра контроллера будет использована соответствующая конфигурация контроллера;

  • если идентификатор контроллера соответствует формату 'path/to/xyz', то имя класса
    контроллера определяется как XyzController, а соответствующий класс как
    protected/controllers/path/to/XyzController.php.
    Например, идентификатор контроллера admin/user будет соответствовать классу
    контроллера — UserController и файлу protected/controllers/admin/UserController.php.
    Если файл не существует, будет сгенерировано исключение [CHttpException] с кодом ошибки 404.

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

1.14 Действие

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

Для создания класса действия необходимо выполнить следующее:

[php]
class UpdateAction extends CAction
{
    public function run()
    {
        // некоторая логика действия
    }
}

Чтобы контроллер знал об этом действии, необходимо переопределить метод
[actions()|CController::actions] в классе контроллера:

[php]
class PostController extends CController
{
    public function actions()
    {
        return array(
            'edit'=>'application.controllers.post.UpdateAction',
        );
    }
}

В приведённом коде мы используем псевдоним маршрута application.controllers.post.UpdateAction
для указания файла класса действия protected/controllers/post/UpdateAction.php.
Создавая действия, основанные на классах, можно организовать приложение в модульном стиле.
Например, следующая структура директорий может быть использована для организации кода контроллеров:
~~~
protected/
controllers/
PostController.php
UserController.php
post/
CreateAction.php
ReadAction.php
UpdateAction.php
user/
CreateAction.php
ListAction.php
ProfileAction.php
UpdateAction.php
~~~

1.14.1 Привязка параметров действий

Начиная с версии 1.1.4, в Yii появилась поддержка автоматической привязки
параметров к действиям контроллера. То есть можно задать именованные
параметры, в которые автоматически будут попадать соответствующие значения из $_GET.

Для того чтобы показать, как это работает, предположим, что нам нужно
реализовать действие create контроллера PostController. Действие принимает
два параметра:

  • category: ID категории, в которой будет создаваться запись (целое число);
  • language: строка, содержащая код языка, который будет использоваться в записи.

Скорее всего, для получения параметров из $_GET в контроллере нам придётся написать следующий скучный код:

[php]
class PostController extends CController
{
    public function actionCreate()
    {
        if(isset($_GET['category']))
            $category=(int)$_GET['category'];
        else
            throw new CHttpException(404,'неверный запрос');

        if(isset($_GET['language']))
            $language=$_GET['language'];
        else
            $language='en';

        // … действительно полезная часть кода …
    }
}

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

[php]
class PostController extends CController
{
    public function actionCreate($category, $language='en')
    {
        $category=(int)$category;

        // … действительно полезная часть кода …
    }
}

Мы добавляем два параметра методу actionCreate. Имя каждого должно в точности
совпадать с одним из ключей в $_GET. Параметру $language задано значение
по умолчанию en, которое используется, если в запросе соответствующий параметр
отсутствует. Так как $category не имеет значения по умолчанию, в случае
отсутствия соответствующего параметра в запросе будет автоматически выброшено
исключение [CHttpException] (с кодом ошибки 400).

Начиная с версии 1.1.5, Yii поддерживает указание массивов в качестве параметров действий.
Использовать их можно следующим образом:

[php]
class PostController extends CController
{
    public function actionCreate(array $categories)
    {
        // Yii приведёт $categories к массиву
    }
}

Мы добавляем ключевое слово array перед параметром $categories.
В результате, если параметр $_GET['categories'] является простой строкой, то он будет
приведён к массиву, содержащему исходную строку.

Note|Примечание: Если параметр объявлен без указания типа array, то он должен
быть скалярным (т.е. не массивом). В этом случае передача массива через
$_GET параметр приведёт к исключению HTTP.

Начиная с версии 1.1.7, автоматическая привязка параметров работает и с
действиями, оформленными в виде классов. Если метод run() в классе действия
описать с параметрами, то эти параметры наполняются соответствующими значениями
из HTTP-запроса:

[php]
class UpdateAction extends CAction
{
    public function run($id)
    {
        // $id будет заполнен значением из $_GET['id']
    }
}

1.15 Фильтры

Фильтр — это часть кода, которая может выполняться до или после
выполнения действия контроллера в зависимости от конфигурации. Например, фильтр
контроля доступа может проверять, аутентифицирован ли пользователь перед тем,
как будет выполнено запрошенное действие. Фильтр, контролирующий производительность,
может быть использован для определения времени, затраченного на выполнение действия.

Действие может иметь множество фильтров. Фильтры запускаются в том порядке, в котором
они указаны в списке фильтров, при этом фильтр может предотвратить выполнение
действия и следующих за ним фильтров.

Фильтр может быть определён как метод класса контроллера. Имя метода должно начинаться на filter.
Например, метод filterAccessControl определяет фильтр accessControl.
Метод фильтра должен выглядеть так:

[php]
public function filterAccessControl($filterChain)
{
    // для выполнения последующих фильтров и выполнения действия вызовите метод $filterChain->run()
}

где $filterChain — экземпляр класса [CFilterChain], представляющего собой список
фильтров, ассоциированных с запрошенным действием. В коде фильтра можно вызвать
$filterChain->run() для того, чтобы продолжить выполнение последующих фильтров и действия.

Фильтр также может быть экземпляром класса [CFilter] или его производного.
Следующий код определяет новый класс фильтра:

[php]
class PerformanceFilter extends CFilter
{
    protected function preFilter($filterChain)
    {
        // код, выполняемый до выполнения действия
        return true; // false — для случая, когда действие не должно быть выполнено
    }

    protected function postFilter($filterChain)
    {
        // код, выполняемый после выполнения действия
    }
}

Для того чтобы применить фильтр к действию, необходимо переопределить метод
CController::filters(), возвращающий массив конфигураций фильтров. Например:

[php]
class PostController extends CController
{
    …
    public function filters()
    {
        return array(
            'postOnly + edit, create',
            array(
                'application.filters.PerformanceFilter - edit, create',
                'unit'=>'second',
            ),
        );
    }
}

Данный код определяет два фильтра: postOnly и PerformanceFilter.
Фильтр postOnly задан как метод (соответствующий метод уже определен в
[CController]), в то время как PerformanceFilter — фильтр на базе класса.
Псевдоним application.filters.PerformanceFilter указывает на файл класса фильтра —
protected/filters/PerformanceFilter. Для конфигурации PerformanceFilter
используется массив, что позволяет задать начальные значения свойств фильтра.
В данном случае свойство unit фильтра PerformanceFilter будет
инициализировано значением 'second'.

Используя операторы '+' и '-' можно указать, к каким действиям должен и
не должен быть применён фильтр. В приведённом примере postOnly будет
применён к действиям edit и create, а PerformanceFilter — ко всем действиям,
кроме edit и create. Если операторы '+' и '-' не указаны, фильтр будет
применён ко всем действиям.
Соглашения
==========

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

Ниже представлены соглашения, рекомендуемые для программирования под Yii.
Для удобства примем, что WebRoot — это директория, в которую установлено приложение.

1.16 URL

По умолчанию Yii понимает адреса URL следующего формата:

http://hostname/index.php?r=ControllerID/ActionID

GET-переменная r представляет маршрут, из которого Yii извлекает информацию о контроллере и действии.
Если ActionID не указан, контроллер будет использовать действие по умолчанию, определённое в свойстве [CController::defaultAction].
Если же и ControllerID не указан (либо отсутствует переменная r), то будет использован
контроллер по умолчанию, определённый в свойстве [CWebApplication::defaultController].

Благодаря компоненту [CUrlManager] можно создавать и использовать SEO-дружественные адреса URL, такие как
http://hostname/ControllerID/ActionID.html. Эта возможность подробно описана в разделе Красивые адреса URL.

1.17 Код

Yii рекомендует именовать переменные, функции и классы, используя CamelCase, что подразумевает написание
каждого слова в имени с большой буквы и соединение их без пробелов.
Первое слово в имени переменных и функций должно быть написано в нижнем регистре, чтобы отличать их от имён
классов (например, $basePath, runController(), LinkPager).
Для полей класса с видимостью private рекомендуется
использовать знак подчеркивания в качестве префикса (например, $_actionList).

Поскольку пространства имён не поддерживаются версиями PHP до 5.3.0, рекомендуется, чтобы имена классов были
уникальными во избежание конфликта имён с классами сторонних разработчиков. По этой причине все имена классов
фреймворка имеют префикс “C”.

Особое правило для имён классов контроллеров — они должны быть дополнены словом Controller. При этом идентификатором
контроллера будет имя класса с первой буквой в нижнем регистре и без слова Controller.
Например, для класса PageController идентификатором будет page. Данное правило делает приложение более защищённым.
Оно также делает адреса URL более понятными (к примеру, /index.php?r=page/index вместо
/index.php?r=PageController/index).

1.18 Конфигурация

Конфигурация — это массив пар ключ-значение, где каждый ключ представляет собой имя свойства конфигурируемого объекта,
а значение — начальное значение соответствующего свойства.
К примеру, array('name'=>'My application', 'basePath'=>'./protected') инициализирует свойства name и basePath
соответствующими значениями.

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

1.19 Файл

Соглашения для именования и использования файлов зависят от их типов.

Файлы классов должны быть названы так же, как и открытые классы, содержащиеся в них.
Например, класс [CController] находится в файле CController.php.
Открытый класс — это класс, который может использоваться любыми другими классами.
Каждый файл классов должен содержать максимум один открытый класс. Приватные классы
(классы, которые могут быть использованы только одним открытым классом) должны
находиться в одном файле с открытым классом.

Файлы представлений должны иметь такие же имена, как и содержащиеся в них представления.
К примеру, представление index находится в файле index.php.
Файл представления — это PHP-скрипт, содержащий HTML и PHP-код, в основном предназначенный для отображения
пользовательского интерфейса.

Конфигурационные файлы могут именоваться произвольным образом. Файл конфигурации —
это PHP-скрипт, чьё единственное назначение — возвращать
ассоциативный массив, представляющий конфигурацию.

1.20 Директория

В Yii предопределён набор директорий для различных целей. Каждая из них может быть изменена при необходимости.

  • WebRoot/protected: это базовая директория приложения,
    содержащая все наиболее важные с точки зрения безопасности PHP-скрипты и файлы данных. Псевдоним по умолчанию для этого пути — application.
    Сама директория и её содержимое должны быть защищены от прямого доступа из веб. Директория может быть настроена через
    [CWebApplication::basePath].

  • WebRoot/protected/runtime: эта директория содержит приватные временные файлы, сгенерированные во время выполнения приложения.
    Эта директория должна быть доступна для записи веб-сервером. Она может быть настроена через [CApplication::runtimePath].

  • WebRoot/protected/extensions: эта директория содержит все сторонние расширения. Она может быть настроена через
    [CApplication::extensionPath]. Псевдоним по умолчанию для этого пути — ext.

  • WebRoot/protected/modules: эта директория содержит все модули приложения, каждый из которых находится в отдельной поддиректории. Директория может быть настроена через [CWebApplication::modulePath].

  • WebRoot/protected/controllers: эта директория содержит файлы всех классов контроллеров. Она может быть настроена через [CWebApplication::controllerPath].

  • WebRoot/protected/views: эта директория содержит файлы всех представлений, включая представления контроллеров, макеты и системные
    представления. Она может быть настроена через [CWebApplication::viewPath].

  • WebRoot/protected/views/ControllerID: эта директория содержит файлы представлений для отдельного класса контроллера.
    Здесь ControllerID является идентификатором контроллера. Директория может быть настроена через [CController::viewPath].

  • WebRoot/protected/views/layouts: эта директория содержит файлы макетов. Она может быть настроена через
    [CWebApplication::layoutPath].

  • WebRoot/protected/views/system: эта директория содержит файлы системных представлений (используются для отображения сообщений об
    ошибках и исключениях). Она может быть настроена через [CWebApplication::systemViewPath].

  • WebRoot/assets: эта директория содержит файлы ресурсов (приватные файлы, которые могут быть опубликованы для доступа к ним из веб).
    Директория должна быть доступна для записи процессами веб-сервера. Она может быть настроена через [CAssetManager::basePath].

  • WebRoot/themes: эта директория содержит различные темы оформления, доступные в приложении.
    Каждая поддиректория содержит отдельную тему с именем, совпадающим с названием поддиректории. Директория может быть настроена через [CThemeManager::basePath].

1.21 База данных

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

  • Таблицы и поля именуются в нижнем регистре.

  • Слова в названиях разделяются символом подчёркивания (например, product_order).

  • В именах таблиц используется либо единственное число, либо множественное, но не
    оба сразу. Мы рекомендуем использовать единственное число.

  • Имена таблиц могут содержать префикс. Например, tbl_. Это особенно полезно,
    когда таблицы нашего приложения находятся в БД, используемой одновременно другими
    приложениями.
    Входной скрипт
    ==============

Входной скрипт — это PHP-скрипт, выполняющий первоначальную обработку
пользовательского запроса. Это единственный PHP-скрипт, к которому может обращаться
конечный пользователь.

В большинстве случаев входной скрипт приложения Yii содержит простой код:

[php]
// в production режиме эту строку необходимо удалить
defined('YII_DEBUG') or define('YII_DEBUG',true);
// подключаем файл инициализации Yii
require_once('path/to/yii/framework/yii.php');
// создаем экземпляр приложения и запускаем его
$configFile='path/to/config/file.php';
Yii::createWebApplication($configFile)->run();

Сначала скрипт подключает файл инициализации фреймворка yii.php,
затем создаёт экземпляр приложения с установленными параметрами и запускает
его на исполнение.

1.22 Режим отладки

Приложение может выполняться в отладочном (debug) или рабочем (production) режиме
в зависимости от значения константы YII_DEBUG.

По умолчанию её значение установлено в false, что означает рабочий режим.
Для запуска в режиме отладки установите значение константы в true до подключения
файла yii.php. Работа приложения в режиме отладки не столь эффективна из-за
ведения множества внутренних логов. С другой стороны, данный режим очень полезен
на стадии разработки, т.к. предоставляет большое количество отладочной
информации при возникновении ошибок.
Модель
======

Модель (model) — это экземпляр класса [CModel] или класса, унаследованного от него.
Модель используется для хранения данных и применимых к ним бизнес-правил.

Модель представляет собой отдельный объект данных. Это может быть запись таблицы
базы данных или HTML-форма с полями для ввода данных.
Каждое поле объекта данных представляется атрибутом модели. Каждый атрибут имеет
текстовую метку и может быть проверен на корректность, используя набор правил.

Yii предоставляет два типа моделей: модель формы и Active Record. Оба типа
являются наследниками базового класса [CModel].

Модель формы — это экземпляр класса [CFormModel]. Она используется для хранения
данных, введённых пользователем. Как правило, мы получаем эти данные, обрабатываем,
а затем избавляемся от них. Например, на странице авторизации модель такого типа
может быть использована для представления информации об имени пользователя и пароле.
Подробное описание работы с формами приведено в разделе Работа с формами.

Active Record (AR) — это шаблон проектирования, используемый для абстрагирования
доступа к базе данных в объектно-ориентированной форме.
Каждый объект AR является экземпляром класса [CActiveRecord] или класса,
унаследованного от него, и представляет отдельную строку в таблице базы данных.
Поля этой строки соответствуют свойствам AR-объекта. Подробнее с AR-моделью можно
ознакомиться в разделе Active Record

В разделе Лучшие практики MVC вы найдёте рекомендации по правильному использованию
моделей.
Модуль
======

Модуль — это самодостаточная программная единица, состоящая из моделей, представлений, контроллеров и иных компонентов.
Во многом модуль схож с приложением.
Основное различие заключается в том, что модуль не может использоваться сам по себе — только в составе приложения.
Пользователи могут обращаться к контроллерам внутри модуля абсолютно так же, как и в случае работы с обычными контроллерами приложения.

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

1.23 Создание модуля

Модуль организован как директория, имя которой выступает в качестве уникального [идентификатора модуля|CWebModule::id]. Структура директории
модуля похожа на структуру базовой директории приложения. Ниже представлена
типовая структура директории модуля с именем forum:

forum/
   ForumModule.php            файл класса модуля
   components/                содержит пользовательские компоненты
      views/                  содержит файлы представлений для виджетов
   controllers/               содержит файлы классов контроллеров
      DefaultController.php   файл класса контроллера по умолчанию
   extensions/                содержит сторонние расширения
   models/                    содержит файлы классов моделей
   views/                     содержит файлы представлений контроллера и макетов
      layouts/                содержит файлы макетов
      default/                содержит файлы представлений для контроллера по умолчанию
         index.php            файл представления 'index'

В корневой директории модуля должен находиться класс модуля, наследующий [CWebModule]. Имя класса определяется, используя выражение
ucfirst($id).'Module', где $id соответствует идентификатору модуля (или названию директории модуля). Класс модуля выполняет роль
центрального хранилища информации, совместно используемой компонентами модуля. Например, мы можем использовать
[CWebModule::params] для хранения параметров модуля, а также [CWebModule::components] для совместного использования
компонентов приложения на уровне модуля.

Tip|Подсказка: Для создания базового каркаса модуля можно воспользоваться
генератором модулей, входящим в состав Gii.

1.24 Использование модуля

Для использования модуля необходимо поместить папку модуля в директорию modules
базовой директории приложения. Далее необходимо объявить
идентификатор модуля в свойстве приложения [modules|CWebApplication::modules]. Например, чтобы воспользоваться модулем forum,
приведённым выше, можно использовать следующую конфигурацию приложения:

[php]
return array(
    …
    'modules'=>array('forum',…),
    …
);

Кроме того, модулю можно задать начальные значения свойств. Порядок использования такой же, как и с компонентами
приложения
. Например, модуль forum может иметь в своём классе свойство с именем
postPerPage, которое может быть установлено в конфигурации приложения
следующим образом:

[php]
return array(
    …
    'modules'=>array(
        'forum'=>array(
            'postPerPage'=>20,
        ),
    ),
    …
);

К экземпляру модуля можно обращаться посредством свойства [module|CController::module] активного в настоящий момент контроллера. Через экземпляр
модуля можно получить доступ к совместно используемой информации на уровне модуля. Например, для того чтобы обратиться к упомянутому выше свойству
postPerPage, мы можем воспользоваться следующим выражением:

[php]
$postPerPage=Yii::app()->controller->module->postPerPage;
// или таким, если $this ссылается на экземпляр контроллера
// $postPerPage=$this->module->postPerPage;

Обратиться к действию контроллера в модуле можно, используя маршрут moduleID/controllerID/actionID.
Например, предположим, что всё тот же модуль forum имеет контроллер с именем PostController. Тогда мы можем использовать
маршрут forum/post/create для того, чтобы обратиться к действию create этого контроллера.
Адрес URL, соответствующий этому маршруту, будет таким: http://www.example.com/index.php?r=forum/post/create.

Tip|Подсказка: Если контроллер находится в подпапке папки controllers, мы также можем использовать формат
маршрута, приведенный выше. Например, предположим, что контроллер PostController находится в
папке forum/controllers/admin, тогда мы можем обратиться к действию create через forum/admin/post/create.

1.25 Вложенные модули

Модули могут быть вложенными друг в друга сколько угодно раз,
т.е. один модуль может содержать в себе другой, который
содержит в себе ещё один. Первый мы будем называть модуль-родитель,
второй — модуль-потомок.
Модули-потомки должны быть описаны в свойстве [modules|CWebModule::modules]
модуля-родителя точно так же, как мы описываем модули в файле конфигурации
приложения.

Для обращения к действию контроллера в дочернем модуле используется
маршрут parentModuleID/childModuleID/controllerID/actionID.
Модель-Представление-Контроллер (MVC)
======================================

Yii использует шаблон проектирования Модель-Представление-Контроллер (MVC, Model-View-Controller),
который широко применяется в веб-программировании.

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

Помимо этого, Yii использует фронт-контроллер, называемый приложением (application),
который инкапсулирует контекст обработки запроса. Приложение собирает информацию
о запросе и передает её для дальнейшей обработки соответствующему контроллеру.

Следующая диаграмма отображает структуру приложения Yii:

Статическая структура приложения Yii

1.26 Типичная последовательность работы приложения Yii

Следующая диаграмма описывает типичную последовательность процесса обработки пользовательского запроса приложением:

Типичная последовательность работы приложения Yii

  1. Пользователь осуществляет запрос посредством URL http://www.example.com/index.php?r=post/show&id=1,
    и веб-сервер обрабатывает его, запуская скрипт инициализации index.php.
  2. Скрипт инициализации создает экземпляр приложения и запускает его на выполнение.
  3. Приложение получает подробную информацию о запросе пользователя от компонента приложения request.
  4. Приложение определяет запрошенные контроллер
    и действие при помощи компонента urlManager.
    В данном примере контроллером будет post, относящийся к классу PostController, а действием — show, суть которого
    определяется контроллером.
  5. Приложение создаёт экземпляр запрашиваемого контроллера для дальнейшей обработки запроса пользователя. Контроллер определяет
    соответствие действия show методу actionShow в классе контроллера. Далее создаются и применяются фильтры
    (например, access control, benchmarking), связанные с данным действием, и, если фильтры позволяют, действие выполняется.
  6. Действие считывает из базы данных модель Post с ID равным 1.
  7. Действие подключает представление show, передавая в него модель Post.
  8. Представление получает и отображает атрибуты модели Post.
  9. Представление подключает некоторые виджеты.
  10. Сформированное представление вставляется в макет страницы.
  11. Действие завершает формирование представления и выводит результат пользователю.
    Псевдоним пути и пространство имён
    ==================================

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

RootAlias.path.to.target

где RootAlias — псевдоним существующей директории.

При помощи [YiiBase::getPathOfAlias()] мы можем преобразовать псевдоним
в соответствующий ему путь. К примеру, system.web.CController будет
преобразован в yii/framework/web/CController.

Также мы можем использовать [YiiBase::setPathOfAlias()] для определения новых
корневых псевдонимов.

1.27 Корневой псевдоним

Для удобства Yii предопределяет следующие системные псевдонимы:

Кроме того, если приложение использует модули, то
у каждого модуля имеется совпадающий с его ID корневой псевдоним, указывающий на
корень модуля. К примеру, если приложение использует модуль users, то для него будет
определён корневой псевдоним users.

1.28 Импорт классов

Используя псевдонимы, очень удобно импортировать описания классов.
К примеру, для подключения класса [CController] можно вызвать:

[php]
Yii::import('system.web.CController');

Использование метода [import|YiiBase::import] более эффективно, чем include и require, поскольку
описание импортируемого класса не будет включено до первого обращения (это реализовано через механизм
автозагрузки классов PHP). Импорт одного и того же пространства имён также происходит намного быстрее,
чем при использовании include_once и require_once. Стоит отметить, что при импорте директории субдиректории не
импортируются.

Tip|Подсказка: Если мы ссылаемся на класс фреймворка, то нет необходимости импортировать или явно включать его.
Все системные классы Yii уже импортированы заранее.

1.28.1 Использование таблицы классов

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

Для импорта набора классов необходимо выполнить следующий код до вызова [CWebApplication::run()]:

[php]
Yii::$classMap=array(
    'ClassName1' => 'path/to/ClassName1.php',
    'ClassName2' => 'path/to/ClassName2.php',
    ......
);

1.29 Импорт директорий

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

[php]
Yii::import('system.web.*');

Помимо [import|YiiBase::import], псевдонимы также используются во многих других местах, где есть ссылки на классы.
Например, псевдоним может быть передан методу [Yii::createComponent()] для создания экземпляра соответствующего
класса, даже если этот класс не был предварительно подключён.

1.30 Пространство имён

Пространства имён служат для логической группировки
имён классов, чтобы их можно было отличить от других, даже если их имена совпадают.
Не путайте псевдоним пути с пространством имён. Псевдоним пути — всего лишь
удобный способ именования файлов и директорий. К пространствам имён он не имеет никакого
отношения.

Tip|Подсказка: Так как версии PHP до 5.3.0 не поддерживают пространства имён, вы не можете создать
экземпляры классов с одинаковыми именами, но различными описаниями. По этой причине все названия
классов Yii-фреймворка имеют префикс ‘C’ (означающий ‘class’), чтобы их можно было отличить от
пользовательских классов. Для пользовательских классов рекомендуется использовать другие префиксы,
сохранив префикс ‘C’ зарезервированным для Yii-фреймворка.

1.31 Классы в пространствах имён

Класс в пространстве имён — любой класс, описанный в неглобальном пространстве имён.
К примеру, класс application\components\GoogleMap описан в пространстве имён
application\components. Использование пространств имён требует PHP версии 5.3.0 или выше.

Начиная с версии Yii 1.1.5, стало возможным использование класса из пространства имён
без его предварительного подключения. К примеру, мы можем создать новый экземпляр
application\components\GoogleMap без явного подключения соответствующего файла.
Это реализуется при помощи улучшенного загрузчика классов Yii.

Для того чтобы автоматически подгрузить класс из пространства имён, пространство имён должно быть
названо в том же стиле, что и псевдонимы пути. Например, класс application\components\GoogleMap
должен храниться в файле, которому соответствует псевдоним application.components.GoogleMap.

То есть для того, чтобы использовать пространство имён, начинающиеся, например,
с \mynamespace и классы которого располагаются в /var/www/common/mynamespace/,
единственное, что необходимо сделать — это объявить псевдоним пути:

[php]
Yii::setPathOfAlias('mynamespace', '/var/www/common/mynamespace/');

1.32 Контроллеры в пространствах имён

По умолчанию все контроллеры Yii берутся из глобального пространства имён.
Соответствующие классы расположены в protected/controllers. Вы можете изменить
данное поведение двумя способами: используя controllerMap и используя
controllerNamespace. Первый позволяет использовать контроллеры из разных
пространств имён. Второй легче настраивается, но задаёт одно пространство имён
для всех контроллеров.

1.32.1 Использование controllerMap

Лучше всего менять данное свойство через файл конфигурации (protected/config/main.php):

[php]
// добавляем пространство имён "mynamespace"
Yii::setPathOfAlias('mynamespace', '/var/www/common/mynamespace/');

return array(
    'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..',
    'name'=>'My Web Application',

    'controllerMap' => array(
        'test' => '\mynamespace\controllers\TestController',
    ),

Когда пользователь пытается загрузить любой из контроллеров, для которого есть
запись в controllerMap, Yii сразу же подгружает указанный в ней класс. В
случае test Yii будет подгружать класс \mynamespace\controllers\TestController,
располагающийся в /var/www/common/mynamespace/controllers/TestController.php.

Стоит отметить, что код контроллера должен быть в пространстве имён:

[php]
// задаём пространство имён
namespace mynamespace\controllers;

// так как класс находится в пространстве имён, обращаться к глобальному
// пространству следует явно при помощи "\":
class TestController extends \CController
{
    public function actionIndex()
    {
        echo 'Это TestController из \mynamespace\controllers';
    }
}

1.32.2 Использование controllerNamespace

Так как приложение является модулем, то вы можете использовать controllerNamespace
так, как это описано далее в подразделе «Модули в пространствах имён».

1.33 Модули в пространствах имён

Иногда удобно использовать пространство имён для целого модуля. К примеру,
если вам необходимо использовать пространство имён \mynamespace\modules\testmodule,
расположенное в /var/www/common/mynamespace/modules/testmodule, для модуля testmodule
сначала следует создать следующую структуру файлов:

/var/www/common/mynamespace/modules
  testmodule
    controllers
      DefaultController.php
    views
      default
        index.php
    TestmoduleModule.php

Здесь index.php ничем не отличается от обычного модуля, а TestmoduleModule.php
и DefaultController.php находятся в пространстве имён.

TestmoduleModule.php:

[php]
// задаём пространство имён:
namespace mynamespace\modules\testmodule;

// так как класс находится в пространстве имён, обращаться к глобальному
// пространству следует явно при помощи "\":
class TestmoduleModule extends \CWebModule
{
    // задаём пространство имён для контроллеров (это же можно сделать через
    // файл конфигурации)
    public $controllerNamespace = '\mynamespace\modules\testmodule\controllers';

    // обычный код модуля
}

DefaultController.php:

[php]
<?php
// задаём пространство имён:
namespace mynamespace\modules\testmodule\controllers;

// так как класс находится в пространстве имён, обращаться к глобальному
// пространству следует явно при помощи "\":
class DefaultController extends \Controller
{
    public function actionIndex()
    {
        $this->render('index');
    }
}

Теперь нам осталось только добавить модуль в наше приложение. Лучший способ сделать
это — использовать файл конфигурации protected/config/main.php:

[php]
// добавляем пространство имён "mynamespace"
Yii::setPathOfAlias('mynamespace', '/var/www/common/mynamespace/');

return array(
    'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..',
    'name'=>'My Web Application',

    'modules'=>array(
        'testmodule' => array(
            'class' => '\mynamespace\modules\testmodule\TestModuleModule',
        ),
    ),

2 Представление

Представление — это PHP-скрипт, состоящий преимущественно из элементов
пользовательского интерфейса. Он может включать выражения PHP, однако рекомендуется,
чтобы эти выражения не изменяли данные и оставались относительно простыми.
Следуя концепции разделения логики и представления, большая часть кода логики
должна быть помещена в контроллер или модель, а не в скрипт представления.

У представления есть имя, которое используется, чтобы идентифицировать файл
скрипта представления в процессе рендеринга. Имя представления должно совпадать
с названием файла представления. К примеру, для представления edit
соответствующий файл скрипта должен называться edit.php. Чтобы отобразить
представление, необходимо вызвать метод [CController::render()], указав имя
представления. При этом метод попытается найти соответствующий файл в директории
protected/views/ControllerID.

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

Кроме того, мы можем использовать следующий способ для передачи данных представлению:

[php]
$this->render('edit', array(
    'var1'=>$value1,
    'var2'=>$value2,
));

В приведённом коде метод [render()|CController::render] преобразует второй параметр —
массив — в переменные. Как результат, внутри представления будут доступны
локальные переменные $var1 и $var2.

2.1 Макет

Макет (layout) — это специальное представление для декорирования других представлений.
Макет обычно содержит части пользовательского интерфейса, общие для нескольких представлений.
Например, макет может содержать верхнюю и нижнюю части страницы, заключая между ними
содержание другого представления:

[php]
…здесь верхняя часть…
<?php echo $content; ?>
…здесь нижняя…

Здесь $content хранит результат рендеринга представления.

Макет применяется неявно при вызове метода [render()|CController::render].
По умолчанию в качестве макета используется представление
protected/views/layouts/main.php. Его можно изменить путём установки значений
[CWebApplication::layout] или [CController::layout]. Для рендеринга представления
без применения макета необходимо вызвать [renderPartial()|CController::renderPartial].

2.2 Виджет

Виджет (widget) — это экземпляр класса [CWidget] или унаследованного от него.
Это компонент, применяемый, в основном, с целью оформления. Виджеты обычно
встраиваются в представления для формирования некоторой сложной, но в то же время
самостоятельной части пользовательского интерфейса. К примеру, виджет календаря
может быть использован для рендеринга сложного интерфейса календаря.
Виджеты позволяют повторно использовать код пользовательского интерфейса.

Для подключения виджета необходимо выполнить в коде:

[php]
<?php $this->beginWidget('path.to.WidgetClass'); ?>
…некое содержимое, которое может быть использовано виджетом…
<?php $this->endWidget(); ?>

или

[php]
<?php $this->widget('path.to.WidgetClass'); ?>

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

Изменить поведение виджета можно путём установки начальных значений его свойств при вызове
[CBaseController::beginWidget] или [CBaseController::widget].
Например, при использовании виджета [CMaskedTextField] можно указать используемую маску,
передав массив начальных значений свойств как показано ниже, где ключи массива
являются именами свойств, а значения — начальными значениями соответствующих
им свойств виджета:

[php]
<?php
$this->widget('CMaskedTextField',array(
    'mask'=>'99/99/9999'
));
?>

Чтобы создать новый виджет, необходимо расширить класс [CWidget] и переопределить
его методы [init()|CWidget::init] и [run()|CWidget::run]:

[php]
class MyWidget extends CWidget
{
    public function init()
    {
        // этот метод будет вызван внутри CBaseController::beginWidget()
    }

    public function run()
    {
        // этот метод будет вызван внутри CBaseController::endWidget()
    }
}

Как и у контроллера, у виджета может быть собственное представление.
По умолчанию файлы представлений виджета находятся в поддиректории
views директории, содержащей файл класса виджета. Эти представления можно рендерить
при помощи вызова [CWidget::render()] точно так же, как и в случае с контроллером.
Единственное отличие состоит в том, что для представления виджета не используются макеты.
Также следует отметить, что $this в представлении указывает на экземпляр виджета, а не на экземпляр
контроллера.

Tip|Подсказка: свойство [CWidgetFactory::widgets] может быть использовано для
настройки умолчаний для отдельных виджетов во всём приложении. Подробнее об
этом можно прочитать в разделе
«Темы оформления, глобальная настройка виджетов».

2.3 Системные представления

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

Именование системных представлений подчиняется некоторым правилам.
Имена типа errorXXX относятся к представлениям, служащим для
отображения исключений [CHttpException] с кодом ошибки XXX.
Например, если исключение [CHttpException] сгенерировано с кодом ошибки 404,
будет использовано представление error404.

Yii предоставляет стандартный набор системных представлений, расположенных в
framework/views. Их можно изменить, создав файлы представлений с
теми же названиями в директории protected/views/system.
Процесс разработки
==================

Рассказав о фундаментальных концепциях Yii, мы опишем общий процесс создания веб-приложений с использованием
фреймворка. Процесс подразумевает, что анализ требований уже проведён, как и анализ устройства приложения.

  1. Создание структуры директорий. Утилита yiic, описанная в разделе
    «cоздание первого приложения», может быть использована для того, чтобы ускорить этот процесс.

  2. Конфигурирование приложения путём модификации файла конфигурации приложения.
    Этот этап также может потребовать написания некоторых компонентов приложения (например, компонента управления пользователями).

  3. Создание класса модели для каждого используемого типа данных.
    Для автоматической генерации всех необходимых моделей Active Record
    можно воспользоваться инструментом Gii, описанным в разделах
    «создание первого приложения»
    и «автоматическая генерация кода».

  4. Создание класса контроллера для каждого типа пользовательского запроса. Классификация
    пользовательских запросов зависит от текущих требований. В общем случае, если класс модели используется пользователем, должен существовать
    соответствующий класс контроллера. Утилита Gii также может автоматизировать этот процесс.

  5. Создание действий и представлений. Именно здесь и происходит
    основная работа.

  6. Конфигурирование необходимых фильтров для действий в классах контроллеров.

  7. Создание тем оформления при необходимости.

  8. Перевод сообщений в случае, когда требуется локализация приложения.

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

  10. Настройка производительности и развёртывание.

Для каждого из представленных этапов может потребоваться создание и применение тестов.
Кэширование данных
==================

Кэширование данных — это сохранение некоторой переменной PHP в кэше и последующее её извлечение
оттуда. Для этой цели базовый класс компонентов кэширования [CCache]
предоставляет два наиболее используемых метода: [set()|CCache::set]
и [get()|CCache::get].

Для кэширования переменной $value мы выбираем уникальный идентификатор (ID)
и вызываем метод [set()|CCache::set] для её сохранения в кэше:

[php]
Yii::app()->cache->set($id, $value);

Данные будут оставаться в кэше до тех пор, пока не будут удалены
согласно некоторой политике кэширования (например, если места для хранения кэшированых
данных не осталось, тогда самые старые данные удаляются).
Чтобы изменить это поведение, мы можем установить срок действия кэша при вызове
метода [set()|CCache::set]. В этом случае данные будут удалены из кэша по истечении как максимум
заданного периода времени:

[php]
// храним значение переменной в кэше не более 30 секунд
Yii::app()->cache->set($id, $value, 30);

Позже, когда нам требуется обратиться к этой переменной (при обработке текущего или другого веб-запроса),
мы вызываем метод [get()|CCache::get] с указанным идентификатором, чтобы получить её значение из кэша.
Если будет возвращено значение false, то это означает, что переменная
не доступна в кэше, и мы должны заново создать её.

[php]
$value=Yii::app()->cache->get($id);
if($value===false)
{
    // устанавливаем значение $value заново, т.к. оно не найдено в кэше,
    // и сохраняем его в кэше для дальнейшего использования:
    // Yii::app()->cache->set($id,$value);
}

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

Некоторые кэш-хранилища, такие как MemCache и APC, поддерживают извлечение
нескольких кэшированных значений в пакетном режиме, что может уменьшить
накладные расходы при извлечении данных из кэша. Метод [mget()|CCache::mget]
позволяет использовать эту возможность. В случае если кэш-хранилище не поддерживает
такую возможность, [mget()|CCache::mget] будет тем не менее имитировать её.

Для удаления значения из кэша необходимо вызвать метод
[delete()|CCache::delete], а для очистки всего кэша — метод
[flush()|CCache::flush]. Следует быть осторожным при вызове метода
[flush()|CCache::flush], т.к. он также удаляет кэшированные данные
других приложений.

Tip|Подсказка: класс [CCache] реализует интерфейс ArrayAccess, поэтому
компонент кэширования может использоваться как массив. Ниже приведены примеры:
~~~
[php]
$cache=Yii::app()->cache;
$cache[‘var1’]=$value1; // эквивалентно $cache->set(‘var1’,$value1);
$value2=$cache[‘var2’]; // эквивалентно $value2=$cache->get(‘var2’);
~~~

2.4 Зависимость кэша

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

Мы представляем зависимость как экземпляр класса [CCacheDependency] или
одного из его наследников. Мы передаём экземпляр зависимости вместе с
кэшируемыми данными, когда вызываем метод [set()|CCache::set].

[php]
// значение действительно не более 30 секунд
// кроме того, значение может стать недействительным раньше, если зависимый файл изменился
Yii::app()->cache->set($id, $value, 30, new CFileCacheDependency('FileName'));

Теперь, если мы попытаемся извлечь значение $value из кэша, вызвав метод
[get()|CCache::get], зависимость будет проверена и, если она изменилась, мы
получим значение false, означающее, что данные требуют обновления.

Ниже приведён список доступных зависимостей кэша:

  • изменилось.

  • каталоге или в подкаталогах изменился.

  • некоторого определённого SQL-выражения изменился.

  • определённого глобального состояния изменилось. Глобальное состояние —
    это переменная, являющаяся постоянной в многократных запросах и сессиях
    приложения. Её значение устанавливается с помощью метода [CApplication::setGlobalState()].

  • цепочки изменилась.

  • определённого PHP выражения изменился.

2.5 Кэширование запросов

Начиная с версии 1.1.7, Yii поддерживает кэширование запросов. Построенное на
кэшировании данных, кэширование запросов хранит результат запроса к базе
данных в кэше и, тем самым, экономит время, расходуемое на одни и те же запросы.

Info|Информация: Некоторые СУБД, такие как
MySQL,
поддерживают кэширование на стороне сервера базы данных.
Аналогичная возможность в Yii обеспечивает большую гибкость по сравнению с кэшированием на стороне сервера БД, и
потенциально она более эффективна.

2.5.1 Включение кэширования запросов

Для того чтобы включить кэширование запросов, убедитесь, что
[CDbConnection::queryCacheID] содержит ID подключённого компонента,
отвечающего за кэширование. По умолчанию это компонент cache.

2.5.2 Использование кэширования запросов с DAO

Для того чтобы использовать кэширование запросов, необходимо вызвать
метод [CDbConnection::cache()], как показано ниже:

[php]
$sql = 'SELECT * FROM tbl_post LIMIT 20';
$dependency = new CDbCacheDependency('SELECT MAX(update_time) FROM tbl_post');
$rows = Yii::app()->db->cache(1000, $dependency)->createCommand($sql)->queryAll();

При выполнении приведённого кода Yii сначала проверит, есть ли в кэше актуальный
результат, соответствующий SQL-запросу, который мы собираемся выполнить. При этом
проверяются следующие три условия:

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

Если все три условия выполнены, то результат берётся из кэша. Иначе выполняется
SQL-запрос, его результат записывается в кэш и возвращается.

2.5.3 Использование кеширования запросов с ActiveRecord

Кэширование запросов также можно использовать совместно с Active Record.
Для этого мы используем метод [CActiveRecord::cache()]:

[php]
$dependency = new CDbCacheDependency('SELECT MAX(update_time) FROM tbl_post');
$posts = Post::model()->cache(1000, $dependency)->findAll();
// реляционный запрос
$posts = Post::model()->cache(1000, $dependency)->with('author')->findAll();

Метод cache() является сокращением для вызова [CDbConnection::cache()].
При выполнении SQL-запроса, сгенерированного ActiveRecord, Yii попытается
использовать кэширование так же, как это было описано в предыдущем подразделе.

2.5.4 Кэширование нескольких запросов

По умолчанию, каждый раз, когда мы вызываем метод cache() (класса CDbConnection
или [CActiveRecord]), он кэширует только следующий за его вызовом SQL-запрос. Все остальные
запросы НЕ кэшируются, пока мы не вызовем cache() ещё раз. Например:

[php]
$sql = 'SELECT * FROM tbl_post LIMIT 20';
$dependency = new CDbCacheDependency('SELECT MAX(update_time) FROM tbl_post');

$rows = Yii::app()->db->cache(1000, $dependency)->createCommand($sql)->queryAll();
// запрос НЕ БУДЕТ закэширован
$rows = Yii::app()->db->createCommand($sql)->queryAll();

Передавая методу cache() дополнительный параметр $queryCount, мы можем
закэшировать несколько подряд выполняющихся запросов. В следующем примере мы кэшируем
два запроса:

[php]
// ...
$rows = Yii::app()->db->cache(1000, $dependency, 2)->createCommand($sql)->queryAll();
// запрос БУДЕТ закэширован
$rows = Yii::app()->db->createCommand($sql)->queryAll();

Как известно, при выполнении реляционного AR-запроса, на самом деле могут выполняться
несколько SQL-запросов (это можно узнать,
проверив журнал сообщений). Например, если
связь между Post и Comment типа HAS_MANY, то код, приведённый ниже,
выполнит два запроса:

  • сначала будут выбраны 20 записей;
  • после этого будут выбраны комментарии для этих записей.
[php]
$posts = Post::model()->with('comments')->findAll(array(
    'limit'=>20,
));

Если использовать кэширование запросов, как показано ниже, закэширован будет
только первый запрос к БД:

[php]
$posts = Post::model()->cache(1000, $dependency)->with('comments')->findAll(array(
    'limit'=>20,
));

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

[php]
$posts = Post::model()->cache(1000, $dependency, 2)->with('comments')->findAll(array(
    'limit'=>20,
));

2.5.5 Ограничения

Кэширование запросов не работает с результатами, содержащими указатели на ресурс.
Например, указатель возвращается в некоторых СУБД при использовании типа BLOB.

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

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

Для решения этой проблемы мы можем варьировать содержимое кэша в зависимости от
имени пользователя, но это было бы очень большой тратой драгоценного места
для кэширования практически одинакового содержимого, кроме имени
пользователя. Мы также можем разделить страницу на несколько фрагментов и
кэшировать их по отдельности, но это усложняет наше представление и делает код
слишком сложным. Наилучший способ — использование возможности
динамического содержимого, предоставляемой классом [CController].

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

Мы вызываем метод [CController::renderDynamic()] для вставки динамического
содержимого в нужное место.

[php]
…другое HTML-содержимое…
<?php if($this->beginCache($id)) { ?>
…фрагмент кэшируемого содержимого…
    <?php $this->renderDynamic($callback); ?>
…фрагмент кэшируемого содержимого…
<?php $this->endCache(); } ?>
…другое HTML-содержимое…

В коде выше $callback — это корректный обратный PHP-вызов. Это
может быть строка с именем метода текущего контроллера или глобальной функции.
Также это может быть массив, ссылающийся на метод класса. Любые дополнительные
параметры в методе [renderDynamic()|CController::renderDynamic()] должны быть
переданы обратному вызову. Обратный вызов должен не отображать динамическое
содержимое, а возвращать его.
Кэширование фрагментов
======================

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

Для использования кэширования фрагментов мы вызываем методы
[CController::beginCache()|CBaseController::beginCache()] и
[CController::endCache()|CBaseController::endCache()] в скрипте
представления контроллера. Эти два метода являются метками начала и конца
содержимого страницы, которое должно быть кэшировано. Как и при кэшировании
данных
, нам нужен идентификатор для определения
кэшируемого фрагмента.

[php]
…другое HTML-содержимое…
<?php if($this->beginCache($id)) { ?>
…кэшируемое содержимое…
<?php $this->endCache(); } ?>
…другое HTML-содержимое…

В приведённом коде, если метод [beginCache()|CBaseController::beginCache()] возвращает
false, то кэшированное содержимое будет автоматически вставлено в данное место;
в противном случае содержимое внутри выражения if будет выполнено и сохранено
в кэше, когда будет вызван метод [endCache()|CBaseController::endCache()].

2.6 Параметры кэширования

Вызывая метод [beginCache()|CBaseController::beginCache()], мы можем передать
в качестве второго аргумента массив, содержащий параметры кэширования для
управления кэшированием фрагмента. Фактически, методы
[beginCache()|CBaseController::beginCache()] и
[endCache()|CBaseController::endCache()] являются удобной обёрткой
виджета [COutputCache]. Поэтому параметры кэширования могут быть
начальными значениями любых свойств виджета [COutputCache].

2.6.1 Длительность (срок хранения)

Наверное, наиболее часто используемым параметром является
[duration|COutputCache::duration],
который определяет, насколько долго содержимое кэша будет оставаться
действительным (валидным). Это похоже на параметр срока действия метода
[CCache::set()]. Следующий код помещает фрагмент в кэш не более, чем на час:

[php]
…другое HTML-содержимое…
<?php if($this->beginCache($id, array('duration'=>3600))) { ?>
…кэшируемое содержимое…
<?php $this->endCache(); } ?>
…другое HTML-содержимое…

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

Начиная с версии 1.1.8, если выставить duration в 0, то соответствующее
значение будет удалено из кэша. Если же применить отрицательное значение duration,
кэш будет отключён, но существующее значение в нём останется.
До 1.1.8, как при выставлении 0, так и при использовании отрицательного
значения кэш отключался без очистки значения.

2.6.2 Зависимости

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

Для определения зависимости мы устанавливаем параметр
[dependency|COutputCache::dependency], который может быть либо объектом,
реализующим интерфейс [ICacheDependency], либо массивом настроек, который может
быть использован для генерации объекта зависимости. Следующий код
определяет содержимое фрагмента, зависящее от изменения значения
столбца lastModified:

[php]
…другое HTML-содержимое…
<?php if($this->beginCache($id, array('dependency'=>array(
        'class'=>'system.caching.dependencies.CDbCacheDependency',
        'sql'=>'SELECT MAX(lastModified) FROM Post')))) { ?>
…кэшируемое содержимое…
<?php $this->endCache(); } ?>
…другое HTML-содержимое…

2.6.3 Вариации (изменения)

Кэшируемое содержимое может быть изменено в соответствии с некоторыми
параметрами. Например, личный профиль может по-разному выглядеть для разных
пользователей. Для кэширования содержимого профиля мы бы хотели, чтобы
кэшированная копия была различной в соответствии с идентификатором
пользователя. По-существу, это значит, что мы должны использовать разные
идентификаторы при вызове метода [beginCache()|CBaseController::beginCache()].

Чтобы не утруждать разработчиков варьированием идентификаторов в соответствии
с некоторыми условиями, класс [COutputCache] уже включает в
себя такую возможность. Ниже приведён список встроенных вариаций:

  • кэшированное содержимое будет изменяться в зависимости от маршрута.
    Поэтому каждая комбинация запрашиваемого контроллера и действия будет иметь разное кэшированное содержимое.

  • true, кэшированное содержимое будет изменяться в соответствии с
    идентификатором сессии. Поэтому каждая пользовательская сессия может видеть
    различное содержимое и извлекать его из кэша.

  • массив имён, мы можем изменять кэшированное содержимое в соответствии с
    заданными GET параметрами. Например, если страница отображает содержимое
    сообщения в зависимости от GET-параметра id, мы можем определить
    varyByParam|COutputCache::varyByParam в виде массива array('id') и затем
    кэшировать содержимое каждого сообщения. Без такой вариации мы могли бы
    кэшировать только одно сообщение.

  • выражение PHP, мы можем изменять кэшированное содержимое в
    зависимости от результата данного выражения.

2.6.4 Типы запросов

Иногда нам требуется, чтобы кэширование фрагмента было включено только для некоторых
типов запросов. Например, страницу с формой мы хотим кэшировать только тогда,
когда обращение к ней произошло впервые (посредством GET запроса). Любое последующее
отображение формы (посредством POST запроса) не должно быть кэшировано,
потому что может содержать данные, введённые пользователем.
Для этого мы задаём параметр [requestTypes|COutputCache::requestTypes]:

[php]
…другое HTML содержмое…
<?php if($this->beginCache($id, array('requestTypes'=>array('GET')))) { ?>
…кэшируемое содержимое…
<?php $this->endCache(); } ?>
…другое HTML содержмое…

2.7 Вложенное кэширование

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

[php]
…другое HTML содержмое…
<?php if($this->beginCache($id1)) { ?>
…внешнее кэшируемое содержимое…
    <?php if($this->beginCache($id2)) { ?>
    …внутреннее кэшируемое содержимое…
    <?php $this->endCache(); } ?>
…внешнее кэшируемое содержимое…
<?php $this->endCache(); } ?>
…другое HTML содержмое…

Параметры кэширования могут быть различными для вложенных кэшей. Например,
внутренний и внешний кэши в вышеприведённом примере могут иметь разные
сроки хранения. Даже когда данные внешнего кэша уже не являются актуальными,
внутренний кеш может содержать актуальный фрагмент. Тем не менее, обратное не верно.
Если внешний кэш актуален, данные будут отдаваться из него даже если внутренний
кэш содержит устаревшие данные. Следует проявлять осторожность при выставлении
срока хранения и задания зависимостей для вложенных кэшей. В противном случае
вы можете получить устаревшие данные.
Кэширование
===========

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

Использование кэша в Yii подразумевает, главным образом, конфигурирование и обращение к
компоненту кэширования. Ниже приведена конфигурация приложения, определяющая компонент кэширования,
использующий memcache с двумя кэш-серверами.

[php]
array(
    …
    'components'=>array(
        …
        'cache'=>array(
            'class'=>'system.caching.CMemCache',
            'servers'=>array(
                array('host'=>'server1', 'port'=>11211, 'weight'=>60),
                array('host'=>'server2', 'port'=>11211, 'weight'=>40),
            ),
        ),
    ),
);

В ходе работы приложения обратиться к компоненту кэширования
можно следующим образом: Yii::app()->cache.

Yii предоставляет несколько компонентов кэширования, которые могут сохранять
кэшированные данные в различных хранилищах. Например, компонент CMemCache инкапсулирует расширение
memcache для PHP и использует оперативную память в качестве хранилища; компонент
CApcCache инкапсулирует расширение APC для PHP; компонент
CDbCache сохраняет кэшируемые данные в базе данных. Ниже приведён список
доступных компонентов кэширования:

  • расширение memcache для PHP.

  • расширение APC для PHP.

  • расширение XCache для PHP.

  • расширение EAccelerator для PHP.

  • [CRedisCache]: использует redis.

  • По умолчанию создаёт и использует базу данных SQLite3 во временном каталоге.
    Мы можем явно задать используемую базу данных, установив значение свойства
    [connectionID|CDbCache::connectionID].

  • Zend Data Cache.

  • [CWinCache]: использует WinCache
    (смотрите также).

  • подходит для больших единиц данных (например, страниц).

  • Необходим для упрощения кода, проверяющего доступность кэша. Мы можем воспользоваться данным
    компонентом во время разработки или в случае, если сервер не поддерживает кэширование.
    Когда “настоящее” кэширование будет включено, мы сможем переключиться на использование
    соответствующего компонента. В обоих случаях мы можем использовать код
    Yii::app()->cache->get($key), чтобы попытаться извлечь кэшированные данные,
    но не беспокоясь о том, что Yii::app()->cache может быть равен null.

Tip|Подсказка: Все перечисленные компоненты кэширования наследуют базовый класс
[CCache], поэтому можно переключаться между различными типами кэширования без
изменения кода, использующего кэш.

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

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

Note|Примечание: По определению, кэш — энергозависимая среда.
Это означает, что не гарантируется наличие в кэше данных даже в том случае, если они ещё не устарели.
Поэтому не используйте кэш как постоянное хранилище данных (например, не
используйте кэш для хранения сессионных данных).
Кэширование страниц
===================

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

2.8 Кэширование вывода

Кэширование страницы может быть рассмотрено как частный случай кэширования
фрагмента
. Из-за того что содержимое страницы
часто генерируется применением макета к представлению, кэширование не будет
работать, если мы просто вызовем в макете методы
[beginCache()|CBaseController::beginCache] и
[endCache()|CBaseController::endCache]. Причина этого в том, что
макет применяется при вызове метода [CController::render()] после
формирования содержимого представления.

Для кэширования всей страницы мы должны пропустить этап формирования
содержимого страницы. Для выполнения этой задачи мы можем использовать класс
[COutputCache] как фильтр действия.
В коде ниже показано, как можно сконфигурировать фильтр кэша:

[php]
public function filters()
{
    return array(
        array(
            'COutputCache',
            'duration'=>100,
            'varyByParam'=>array('id'),
        ),
    );
}

Вышеприведённая конфигурация создаёт фильтр, применяемый ко всем
действиям контроллера. Мы можем ограничить его применение одним или
несколькими действиями, используя оператор +.
Подробнее с работой фильтров можно ознакомиться в теме
фильтры.

Tip|Подсказка: Мы можем использовать класс [COutputCache] в качестве фильтра,
поскольку он наследует класс [CFilterWidget], т.е. оба этих класса одновременно
являются и виджетами, и фильтрами. Фактически, способ работы виджета очень похож
на работу фильтра: виджет (фильтр) выполняется до того, как любое вложенное
содержимое (действие) будет сформировано (выполнено), а выполнение виджета
(фильтра) заканчивается после того, как вложенное содержимое (действие)
будет сформировано (выполнено).

2.9 HTTP-кэширование

В дополнение к кешированию всего вывода действия контроллера в Yii с версии
1.1.11 есть [CHttpCacheFilter]. Данный фильтр отсылает рассматриваемые далее
HTTP-заголовки, которые говорят клиенту (чаще всего браузеру), что содержимое
страницы не менялось с последнего запроса. В этом случае серверу нет необходимости
повторно формировать и отсылать страницу. [CHttpCacheFilter] настраивается
также, как и [COutputCache]:

[php]
public function filters()
{
    return array(
        array(
            'CHttpCacheFilter + index',
            'lastModified'=>Yii::app()->db->createCommand("SELECT MAX(`update_time`) FROM {{post}}")->queryScalar(),
        ),
    );
}

Приведённый выше код добавит заголовок Last-Modified с значением, равным
последней дате изменения записи. Также вы можете использовать
[CHttpCacheFilter::lastModifiedExpression] для того, чтобы задать значение
Last-Modified при помощи выражения PHP.

Tip|Подсказка: Как [CHttpCacheFilter::lastModifiedExpression], так и
[CHttpCacheFilter::lastModified] могут принимать как Unix timestamp в виде
целого числа, так и дату строкой в понятном человеку формате. Если дату может
разобрать strtotime(), то
никаких дополнительных преобразований не требуется.

Похожим образом, через [CHttpCacheFilter::etagSeed] и
[CHttpCacheFilter::etagSeedExpression], может быть добавлен заголовок
«Entity Tag» (ETag). Заданные значения сериализуются (то есть можно использовать
как простое значение, так и массив) и затем используются для генерации хеша
quoted base64 SHA1, который подставляется в ETag. Данный способ отличается от
того, что используется Apache и другими веб-серверами.
Тем не менее, реализация в Yii полностью соответствует RFC и лучше подходит
для использования в фреймворке.

Note|Примечание: Для соответствия разделу 13.3.4 RFC 2616,
[CHttpCacheFilter] отсылает как ETag так и Last-Modified когда это возможно.
Соответственно, оба заголовка будут использоваться для инвалидации кэша.

Так как ETag является хешем, то позволяет реализовать более сложные и точные
стратегии кеширования, чем в случае использования Last-Modified. К примеру, ETag
может быть инвалидирован в случае изменения темы сайта.

Tip|Подсказка: Использование «дорогих» в плане производительности выражений
в [CHttpCacheFilter::etagSeedExpression] может свести на нет ожидаемую выгоду
от использования [CHttpCacheFilter] и даже привести к замедлению приложения потому
как используемое выражение выполняется при каждом запросе. Всегда старайтесь найти
наиболее простое выражение, показывающее факт изменения содержимого страницы.

2.9.1 Влияние на SEO

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

На этой странице кратко излагаются новые возможности, внесённые в каждом релизе Yii.

2.10 Версия 1.1.14

  • Добавлен [CPasswordHelper]
  • Добавлен [CRedisCache]

2.11 Версия 1.1.11

2.12 Версия 1.1.8

2.13 Версия 1.1.7

2.14 Версия 1.1.6

2.15 Версия 1.1.5

2.16 Версия 1.1.4

2.17 Версия 1.1.3

2.18 Версия 1.1.2

2.19 Версия 1.1.1

  • Добавлен виджет CActiveForm, упрощающий написание кода формы и поддерживающий
    прозрачную валидацию как на стороне клиента, так и на стороне сервера.

  • Произведён рефакторинг кода, генерируемого yiic. Приложение-каркас теперь
    генерируется с поддержкой нескольких главных разметок, использован виджет меню,
    добавлена возможность сортировать данные в административном интерфейсе, для
    отображения форм используется CActiveForm.

  • Добавлена поддержка глобальных консольных команд.

2.20 Версия 1.1.0

Хотя DAO Yii справляется практически с любыми задачами, касающимися работы с БД, почти наверняка
90% времени уйдёт на написание SQL-запросов, реализующих общие операции CRUD (создание, чтение, обновление и удаление).
Кроме того, код, перемешанный с SQL-выражениями, поддерживать проблематично.
Для решения этих проблем мы можем воспользоваться Active Record.

Active Record реализует популярный подход объектно-реляционного проецирования (ORM).
Каждый класс AR отражает таблицу (или представление) базы данных, экземпляр AR — строку в этой таблице,
а общие операции CRUD реализованы как методы AR. В результате мы можем использовать более
объектно-ориентированный подход доступа к данным. Например, используя следующий код, можно вставить новую строку в
таблицу tbl_post:

[php]
$post=new Post;
$post->title='тестовая запись';
$post->content='содержимое записи';
$post->save();

Ниже мы покажем, как настроить и использовать AR для реализации CRUD-операций,
а в следующем разделе — как использовать AR для работы со связанными таблицами.
Для примеров в этом разделе мы будем использовать следующую таблицу. Обратите внимание,
что при использовании БД MySQL в SQL-выражении ниже AUTOINCREMENT следует заменить
на AUTO_INCREMENT.

[sql]
CREATE TABLE tbl_post (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(128) NOT NULL,
    content TEXT NOT NULL,
    create_time INTEGER NOT NULL
);

Note|Примечание: AR не предоставляет решения всех задач, касающихся работы с базами данных.
Лучше всего использовать AR для моделирования таблиц в конструкциях PHP и для
несложных SQL-запросов. В сложных случаях следует использовать Yii DAO.

2.21 Соединение с базой данных

Для работы AR требуется подключение к базе данных. По умолчанию предполагается, что
компонент приложения db предоставляет необходимый экземпляр класса CDbConnection,
который отвечает за подключение к базе. Ниже приведён пример конфигурации приложения:

[php]
return array(
    'components'=>array(
        'db'=>array(
            'class'=>'system.db.CDbConnection',
            'connectionString'=>'sqlite:path/to/dbfile',
            // включить кэширование схем для улучшения производительности
            // 'schemaCachingDuration'=>3600,
        ),
    ),
);

Tip|Подсказка: Поскольку для получения информации о полях таблицы AR использует метаданные,
требуется некоторое время для их чтения и анализа. Если не предполагается, что схема базы данных будет меняться, то
следует включить кэширование схемы, установив для атрибута [CDbConnection::schemaCachingDuration]
любое значение больше нуля.

В настоящий момент AR поддерживает следующие СУБД:

Если вы хотите использовать другой компонент вместо db или предполагаете,
используя AR, работать с несколькими БД, то следует переопределить метод
[CActiveRecord::getDbConnection()]. Класс [CActiveRecord] является базовым классом
для всех классов AR.

Tip|Подсказка: Есть несколько способов для работы AR с несколькими БД. Если
схемы используемых баз различаются, то можно создать разные базовые классы AR с
различной реализацией метода [getDbConnection()|CActiveRecord::getDbConnection].
В противном случае проще будет динамически менять статическую переменную [CActiveRecord::db].

2.22 Определение AR-класса

Для доступа к таблице БД, прежде всего, требуется определить класс AR путём наследования
класса [CActiveRecord]. Каждый класс AR представляет одну таблицу базы данных, а экземпляр класса —
строку в этой таблице. Ниже приведён минимальный код, требуемый для определения класса AR,
представляющего таблицу tbl_post.

[php]
class Post extends CActiveRecord
{
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }

    public function tableName()
    {
        return 'tbl_post';
    }
}

Tip|Подсказка: Поскольку классы AR часто появляются во многих местах кода,
мы можем вместо включения классов по одному, подключить всю директорию с AR-классами.
Например, если AR-классы находятся в директории protected/models,
мы можем сконфигурировать приложение следующим образом:
~~~
[php]
return array(
‘import’=>array(
’application.models.*’,
),
);
~~~

По умолчанию имя AR-класса совпадает с названием таблицы в базе данных.
Если они различаются, потребуется переопределить метод [tableName()|CActiveRecord::tableName].
Метод [model()|CActiveRecord::model] объявляется для каждого AR-класса.

Info|Информация: В случае использования префиксов таблиц
метод [tableName()|CActiveRecord::tableName] AR-класса
может быть переопределён следующим образом:
~~~
[php]
public function tableName()
{
return ‘{{post}}’;
}
~~~
Вместо того чтобы возвращать полное имя таблицы, мы возвращаем
имя таблицы без префикса и заключаем его в двойные фигурные скобки.

Значения полей в строке таблицы доступны как атрибуты соответствующего экземпляра AR-класса.
Например, код ниже устанавливает значение для атрибута title:

[php]
$post=new Post;
$post->title='тестовая запись';

Хотя мы никогда не объявляем заранее свойство title класса Post,
мы, тем не менее, можем обратиться к нему как в коде выше. Это возможно потому, что
title является полем таблицы tbl_post, и [CActiveRecord] делает его доступным в качестве свойства
благодаря магическому методу PHP __get(). Если аналогичным образом обратиться к несуществующему полю,
будет выброшено исключение.

Info|Информация: В данном руководстве мы именуем столбцы и таблицы в нижнем регистре, так
как различные СУБД работают с регистрозависимыми именами по-разному.
Например, PostgreSQL считает имена столбцов регистронезависимыми по умолчанию,
и мы должны заключать их в кавычки в условиях запроса, если имена содержат заглавные буквы.
Использование нижнего регистра помогает избежать подобных проблем.

AR опирается на правильно определённые первичные ключи таблиц БД. Если в таблице
нет первичного ключа, то требуется указать в соответствующем классе AR столбцы,
которые будут использоваться как первичный ключ. Сделать это можно путём
перекрытия метода primaryKey():

[php]
public function primaryKey()
{
    return 'id';
    // Для составного первичного ключа следует использовать массив:
    // return array('pk1', 'pk2');
}

2.23 Создание записи

Для добавления новой строки в таблицу БД нам необходимо создать новый экземпляр
соответствующего класса, присвоить значения атрибутам, ассоциированным с полями таблицы,
и вызвать метод [save()|CActiveRecord::save] для завершения добавления.

[php]
$post=new Post;
$post->title='тестовая запись';
$post->content='содержимое тестовой записи';
$post->create_time=time();
$post->save();

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

Если поле задано в схеме таблицы с некоторым статическим значением по умолчанию
(например, строка или число), то после создания экземпляра соответствующее свойство
экземпляра AR будет автоматически содержать это значение. Один из способов
поменять это значение — прописать его в AR-классе.

[php]
class Post extends CActiveRecord
{
    public $title='пожалуйста, введите заголовок';
    …
}

$post=new Post;
echo $post->title;  // отобразится: пожалуйста, введите заголовок

До сохранения записи (добавления или обновления) атрибуту
может быть присвоено значение типа [CDbExpression]. Например, для сохранения текущей даты,
возвращаемой функцией MySQL NOW(), можно использовать следующий код:

[php]
$post=new Post;
$post->create_time=new CDbExpression('NOW()');
// $post->create_time='NOW()'; этот вариант работать не будет
// т.к. значение 'NOW()' будет воспринято как строка
$post->save();

Tip|Подсказка: Несмотря на то что AR позволяет производить различные операции без
написания громоздкого SQL, часто необходимо знать, какой SQL выполняется на самом деле.
Для этого необходимо включить журналирование.
Например, чтобы выводить выполненные SQL-запросы в конце каждой страницы, мы можем включить
CWebLogRoute в настройках приложения.
Можно установить значение параметра [CDbConnection::enableParamLogging] в true для отображения
значений параметров запросов.

2.24 Чтение записи

Для чтения данных из таблицы базы данных можно использовать методы find:

[php]
// найти первую строку, удовлетворяющую условию
$post=Post::model()->find($condition,$params);
// найти строку с указанным значением первичного ключа
$post=Post::model()->findByPk($postID,$condition,$params);
// найти строку с указанными значениями атрибутов
$post=Post::model()->findByAttributes($attributes,$condition,$params);
// найти первую строку, используя некоторое выражение SQL
$post=Post::model()->findBySql($sql,$params);

Выше мы вызываем метод find через Post::model(). Запомните, что статический метод model()
обязателен для каждого AR-класса. Этот метод возвращает экземпляр AR, используемый для
доступа к методам уровня класса (что-то схожее со статическими методами класса) в контексте объекта.

Если метод find находит строку, соответствующую условиям запроса, он возвращает экземпляр
класса Post, свойства которого содержат значения соответствующих полей строки таблицы. Далее мы
можем использовать загруженные значения аналогично обычным свойствам объектов, например, echo $post->title;.

В случае если в базе нет данных, соответствующих условиям запроса, метод find вернет значение null.

Параметры $condition и $params используются для уточнения запроса. В данном случае $condition
может быть строкой, соответствующей оператору WHERE в SQL-выражении, а $params — массивом параметров, значения
которых должны быть привязаны к маркерам, указанным в $condition. Например:

[php]
// найдём строку, для которой postID равен 10
$post=Post::model()->find('postID=:postID', array(':postID'=>10));

Note|Примечание: В примере выше нам может понадобиться заключить в кавычки
обращение к столбцу postID для некоторых СУБД. Например, если мы используем
СУБД PostgreSQL, нам следует писать условие как "postID"=:postID, потому
что PostgreSQL по умолчанию считает имя столбца регистронезависимым.

Кроме того, можно использовать $condition для указания более сложных условий запроса. Вместо строки параметр
$condition может быть экземпляром класса [CDbCriteria], который позволяет указать иные условия
помимо выражения WHERE. Например:

[php]
$criteria=new CDbCriteria;
$criteria->select='title';  // выбираем только поле 'title'
$criteria->condition='postID=:postID';
$criteria->params=array(':postID'=>10);
$post=Post::model()->find($criteria); // $params не требуется

Обратите внимание, если в качестве условия запроса используется [CDbCriteria], то параметр
$params уже не нужен, поскольку его можно указать непосредственно в [CDbCriteria], как показано выше.

Помимо использования [CDbCriteria], есть другой способ указать условие — передать методу
массив ключей и значений, соответствующих именам и значениям свойств критерия. Пример выше можно переписать
следующим образом:

[php]
$post=Post::model()->find(array(
    'select'=>'title',
    'condition'=>'postID=:postID',
    'params'=>array(':postID'=>10),
));

Info|Информация: В случае когда условие заключается в соответствии значениям некоторых полей, можно воспользоваться
методом [findByAttributes()|CActiveRecord::findByAttributes], где параметр $attributes представляет собой
массив значений, проиндексированных по имени поля. В некоторых фреймворках эта задача решается путём использования методов
типа findByNameAndTitle. Хотя такой способ и выглядит привлекательно, часто он вызывает путаницу и проблемы, связанные с чувствительностью
имён полей к регистру.

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

[php]
// найдём все строки, удовлетворяющие условию
$posts=Post::model()->findAll($condition,$params);
// найдём все строки с указанными значениями первичного ключа
$posts=Post::model()->findAllByPk($postIDs,$condition,$params);
// найдём все строки с указанными значениями атрибутов
$posts=Post::model()->findAllByAttributes($attributes,$condition,$params);
// найдём все строки, используя SQL-выражение
$posts=Post::model()->findAllBySql($sql,$params);

В отличие от find, методы findAll, в случае если нет ни одной строки, удовлетворяющей запросу,
возвращают не null, а пустой массив.

Помимо методов find и findAll, описанных выше, для удобства также доступны следующие методы:

[php]
// определим количество строк, удовлетворяющих условию
$n=Post::model()->count($condition,$params);
// определим количество строк, используя указанное SQL-выражение
$n=Post::model()->countBySql($sql,$params);
// определим, есть ли хотя бы одна строка, удовлетворяющая условию
$exists=Post::model()->exists($condition,$params);

2.25 Обновление записи

Заполнив экземпляр AR значениями полей, мы можем изменить их и сохранить обратно в БД.

[php]
$post=Post::model()->findByPk(10);
$post->title='new post title';
$post->save(); // сохраняем изменения

Как можно было заметить, мы используем метод [save()|CActiveRecord::save] как для добавления, так и для обновления записей.
Если экземпляр AR создан с использованием оператора new, то вызов метода [save()|CActiveRecord::save]
приведёт к добавлению новой строки в базу данных. Если же экземпляр AR создан как результат вызова методов
find и findAll, вызов метода [save()|CActiveRecord::save] обновит данные существующей строки в таблице.
На самом деле, можно использовать свойство [CActiveRecord::isNewRecord] для указания, является экземпляр AR новым или нет.

Кроме того, можно обновить одну или несколько строк в таблице без их предварительной загрузки. Для этого в
AR существуют следующие методы уровня класса:

[php]
// обновим строки, отвечающие заданному условию
Post::model()->updateAll($attributes,$condition,$params);
// обновим строки, удовлетворяющие заданному условию и значению первичного ключа (или нескольким значениям ключей)
Post::model()->updateByPk($pk,$attributes,$condition,$params);
// обновим поля-счётчики в строках, удовлетворяющих заданным условиям
Post::model()->updateCounters($counters,$condition,$params);

Здесь $attributes — это массив значений полей, проиндексированных по имени поля,
$counters — массив инкрементных значений, проиндексированных по имени поля,
$condition и $params аналогичны описанным выше.

2.26 Удаление записи

Мы можем удалить строку, если экземпляр AR был заполнен значениями этой строки.

[php]
$post=Post::model()->findByPk(10); // предполагаем, что запись с ID=10 существует
$post->delete(); // удаляем строку из таблицы

Обратите внимание, что после удаления экземпляр AR не меняется, но соответствующей записи в
таблице уже нет.

Следующие методы используются для удаления строк без их предварительной загрузки:

[php]
// удалим строки, соответствующие указанному условию
Post::model()->deleteAll($condition,$params);
// удалим строки, соответствующие указанному условию и первичному ключу (или нескольким ключам)
Post::model()->deleteByPk($pk,$condition,$params);

2.27 Проверка данных

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

AR осуществляет проверку данных автоматически в момент вызова метода [save()|CActiveRecord::save].
Проверка основана на правилах, заданных в методе AR-класса [rules()|CModel::rules]. Детально ознакомиться с
тем, как задаются правила проверки, можно в разделе Определение правил проверки.
Ниже приведён типичный порядок обработки в момент сохранения записи:

[php]
if($post->save())
{
    // данные корректны и успешно добавлены/обновлены
}
else
{
    // данные некорректны, сообщения об ошибках могут быть получены путём вызова метода getErrors()
}

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

[php]
$post->title=$_POST['title'];
$post->content=$_POST['content'];
$post->save();

Если полей будет много, мы получим простыню из подобных присваиваний. Этого можно избежать, если
использовать свойство [attributes|CActiveRecord::attributes], как показано ниже. Подробности можно найти в
разделах Безопасное присваивание значений атрибутам и
Создание действия.

[php]
// предполагаем, что $_POST['Post'] является массивом значений полей, проиндексированных по имени поля
$post->attributes=$_POST['Post'];
$post->save();

2.28 Сравнение записей

Экземпляры AR идентифицируются уникальным образом по значениям первичного ключа, аналогично строкам таблицы, поэтому
для сравнения двух экземпляров нам нужно просто сравнить значения их первичных ключей, предполагая, что оба экземпляра одного AR-класса.
Однако можно проделать это ещё проще, вызвав метод [CActiveRecord::equals()].

Info|Информация: В отличие от реализации AR в других фреймворках, Yii поддерживает в AR составные первичные ключи.
Составной первичный ключ состоит из двух и более полей таблицы. Соответственно,
первичный ключ в Yii представлен как массив, а свойство [primaryKey|CActiveRecord::primaryKey]
содержит значение первичного ключа для экземпляра AR.

2.29 Тонкая настройка

Класс [CActiveRecord] предоставляет несколько методов, которые могут быть переопределены в дочерних классах
для тонкой настройки работы AR.

  • [beforeValidate|CModel::beforeValidate] и [afterValidate|CModel::afterValidate]:
    методы вызываются до и после осуществления проверки;

  • [beforeSave|CActiveRecord::beforeSave] и [afterSave|CActiveRecord::afterSave]:
    методы вызываются до и после сохранения экземпляра AR;

  • [beforeDelete|CActiveRecord::beforeDelete] и [afterDelete|CActiveRecord::afterDelete]:
    методы вызываются до и после удаления экземпляра AR;

  • созданного с использованием оператора new;

  • будет выполнен поисковый запрос (например, find(), findAll()).

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

2.30 Использование транзакций с AR

Каждый экземпляр AR содержит свойство [dbConnection|CActiveRecord::dbConnection], которое является экземпляром класса CDbConnection.
Поэтому при необходимости можно использовать транзакции,
предоставляемые Yii DAO:

[php]
$model=Post::model();
$transaction=$model->dbConnection->beginTransaction();
try
{
    // поиск и сохранение — шаги, между которыми могут быть выполнены другие запросы,
    // поэтому мы используем транзакцию, чтобы удостовериться в целостности данных
    $post=$model->findByPk(10);
    $post->title='new post title';
    if($post->save())
        $transaction->commit();
    else
        $transaction->rollback();
}
catch(Exception $e)
{
    $transaction->rollback();
    throw $e;
}

2.31 Именованные группы условий

Info|Информация: Идея групп условий позаимствована у Ruby on Rails.

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

Именованные группы чаще всего описываются в методе [CActiveRecord::scopes()]
в виде пар имя-условие. Приведённый ниже код описывает две именованные группы
условий для модели Post: published и recently:

[php]
class Post extends CActiveRecord
{
    …
    public function scopes()
    {
        return array(
            'published'=>array(
                'condition'=>'status=1',
            ),
            'recently'=>array(
                'order'=>'create_time DESC',
                'limit'=>5,
            ),
        );
    }
}

Каждая группа описывается массивом, который используется для инициализации
экземпляра [CDbCriteria]. К примеру, recently устанавливает значение order
равным create_time DESC, а limit равным 5. Вместе эти условия означают, что
будут выбраны пять последних публикаций.

Именованные группы условий обычно используются в качестве модификаторов для
метода find. Можно использовать несколько групп одновременно для получения более точного
результата. Например, чтобы найти последние опубликованные записи, можно использовать
следующий код:

[php]
$posts=Post::model()->published()->recently()->findAll();

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

Note|Примечание: Именованные группы могут быть использованы только совместно с методами уровня
класса. Таким образом, метод должен вызываться при помощи ClassName::model().

2.31.1 Именованные группы условий с параметрами

Именованные группы условий могут быть параметризованы.
Например, если нам необходимо изменять число выбираемых публикаций для группы recently, то
вместо описания группы в методе [CActiveRecord::scopes], мы должны создать новый метод с именем, совпадающим с названием группы условий:

[php]
public function recently($limit=5)
{
    $this->getDbCriteria()->mergeWith(array(
        'order'=>'create_time DESC',
        'limit'=>$limit,
    ));
    return $this;
}

Теперь, чтобы получить 3 последних опубликованных записи, можно
использовать следующий код:

[php]
$posts=Post::model()->published()->recently(3)->findAll();

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

2.31.2 Группа условий по умолчанию

Класс модели может содержать группу условий по умолчанию, которая будет
применяться ко всем запросам (включая реляционные). К примеру, на сайте реализована
поддержка нескольких языков, и содержимое отображается на языке, выбранном пользователем.
Так как запросов, связанных с получением данных, скорее всего, будет достаточно много,
для решения этой задачи мы можем определить группу условий по умолчанию.
Для этого необходимо переопределить метод [CActiveRecord::defaultScope] следующим образом:

[php]
class Content extends CActiveRecord
{
    public function defaultScope()
    {
        return array(
            'condition'=>"language='".Yii::app()->language."'",
        );
    }
}

Теперь к приведённому ниже вызову метода findAll будут автоматически применены наши условия:

[php]
$contents=Content::model()->findAll();

Note|Примечание: Как группа условий по умолчанию, так и именованная группа условий
применяются только к запросам типа SELECT и игнорируются при запросах вида
INSERT, UPDATE или DELETE. Кроме того, нельзя использовать AR-модель для выполнения запросов
внутри методов, объявляющих группы условий (как именованные, так и группы условий по умолчанию).
Реляционная Active Record
=========================

Мы уже рассмотрели использование Active Record (AR) для выбора данных из одной таблицы базы данных.
В этом разделе мы расскажем, как использовать AR для соединения нескольких связанных таблиц и получения
набора связанных данных.

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

Для наглядности примеров в этом разделе мы будем использовать схему базы данных,
представленную на следующей диаграмме сущность-связь (ER).

Диаграмма ER

Info|Информация: Поддержка ограничений внешних ключей различается в разных СУБД.
SQLite 3.6.19 и более ранние версии не поддерживает ограничений, но вы, тем не менее, можете их объявить
при создании таблиц. Движок MySQL MyISAM не поддерживает внешние ключи.

2.32 Установка связей между AR-классами

Перед тем как использовать AR для выполнения реляционных запросов, нам необходимо установить связи между AR-классами.

Связь между двумя AR-классами напрямую зависит от связей между соответствующими таблицами базы данных. С точки
зрения БД, связь между таблицами A и В может быть трёх типов: один-ко-многим (например, tbl_user и tbl_post), один-к-одному
(например, tbl_user и tbl_profile) и многие-ко-многим (например, tbl_category и tbl_post). В AR существует четыре типа связей:

  • BELONGS_TO: если связь между А и В один-ко-многим, значит В принадлежит А (например, Post принадлежит User);

  • HAS_MANY: если связь между таблицами А и В один-ко-многим, значит у А есть много В (например, у User есть много Post);

  • HAS_ONE: это частный случай HAS_MANY, где А может иметь максимум одно В (например, у User есть только один Profile);

  • MANY_MANY: эта связь соответствует типу связи многие-ко-многим в БД. Поскольку многие СУБД не поддерживают непосредственно
    этот тип связи, требуется ассоциативная таблица для преобразования связи многие-ко-многим в связи один-ко-многим.
    В нашей схеме базы данных этой цели служит таблица tbl_post_category. В терминологии AR связь MANY_MANY можно описать как
    комбинацию BELONGS_TO и HAS_MANY. Например, Post принадлежит многим Category, а у Category есть много Post.

Существует пятый, специальный тип связи, который предназначен для статистических запросов над связанными записями
(запросы агрегирования) — называется он STAT. Более подробно с ним можно ознакомиться в разделе
Статистический запрос.

Установка связей производится внутри метода [relations()|CActiveRecord::relations] класса [CActiveRecord].
Этот метод возвращает массив с конфигурацией связей. Каждый элемент массива представляет одну связь в следующем формате:

[php]
'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', …дополнительные параметры)

где VarName — имя связи, RelationType указывает на один из четырёх типов связей,
ClassName — имя AR-класса, связанного с данным классом, а
ForeignKey обозначает один или несколько внешних ключей, используемых для связи.
Кроме того, можно указать ряд дополнительных параметров, о которых будет рассказано позже.

В приведённом ниже коде показано, как установить связь между классами User и Post.

[php]
class Post extends CActiveRecord
{
    …
    public function relations()
    {
        return array(
            'author'=>array(self::BELONGS_TO, 'User', 'author_id'),
            'categories'=>array(self::MANY_MANY, 'Category',
                'tbl_post_category(post_id, category_id)'),
        );
    }
}

class User extends CActiveRecord
{
    …
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'author_id'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'),
        );
    }
}

Info|Информация: Внешний ключ может быть составным, то есть состоять из двух и более столбцов таблицы. В этом случае
имена столбцов следует разделить запятыми и передать их либо в качестве строки, либо в виде массива array('key1','key2').
Задать свою связь первичного ключа с внешним можно в виде массива array('fk'=>'pk'). Для составных
ключей это будет array('fk_c1'=>'pk_c1','fk_c2'=>'pk_c2'). Для типа связи MANY_MANY имя ассоциативной таблицы также должно быть
указано во внешнем ключе. Например, связи categories модели Post соответствует внешний ключ tbl_post_category(post_id, category_id).
Для каждой добавленной связи неявно создаётся свойство класса. После выполнения реляционного запроса
соответствующее свойство будет содержать связанный экземпляр класса AR (или массив экземпляров для связей типа один-ко-многим и
многие-ко-многим). Например, если $author является экземпляром AR-класса User, то можно использовать $author->posts
для доступа к связанным экземплярам Post.

2.33 Выполнение реляционного запроса

Самый простой способ выполнить реляционный запрос — использовать реляционное свойство AR-класса.
Если обращение к этому свойству производится впервые, то будет выполнен реляционный запрос, который соединит связанные таблицы
и оставит только данные, соответствующие первичному ключу текущего экземпляра AR.
Результат запроса будет сохранён в свойстве как экземпляр (или массив экземпляров) связанного класса.
Этот подход также известен как «отложенная загрузка» (lazy loading), при которой непосредственный запрос выполняется
только в момент первого обращения к связанным объектам. Ниже приведён пример использования этого подхода:

[php]
// получаем запись с ID=10
$post=Post::model()->findByPk(10);
// Получаем автора записи. Здесь будет выполнен реляционный запрос.
$author=$post->author;

Info|Информация: Если связанные данные не найдены, то соответствующее
свойство примет значение null для связей BELONGS_TO и HAS_ONE или будет являться пустым массивом
для HAS_MANY и MANY_MANY.
Стоит отметить, что связи HAS_MANY и MANY_MANY возвращают
массивы объектов, и обращаться к их свойствам необходимо в цикле, иначе
можно получить ошибку «Trying to get property of non-object».

Способ отложенной загрузки удобен, но не всегда эффективен. Например, если нам потребуется
получить информацию об авторах N записей, то использование отложенной загрузки
потребует выполнения N дополнительных запросов к базе данных.
В данной ситуации разумно использовать метод «жадной загрузки» (eager loading).

Этот метод заключается в загрузке всех связанных данных вместе с
основным экземпляром AR. Реализуется этот подход путем использования метода
[with()|CActiveRecord::with] вместе с методом [find|CActiveRecord::find] или
[findAll|CActiveRecord::findAll].
Например:
~~~
[php]
$posts=Post::model()->with(‘author’)->findAll();
~~~

Приведённый код вернёт массив экземпляров Post. В отличие от отложенной загрузки, свойство author каждой записи будет
заполнено связанным экземпляром User ещё до обращения к этому свойству. Таким образом, вместо выполнения отдельного запроса
для каждой записи, жадная загрузка получит все записи вместе с их авторами в одном запросе!

В методе [with()|CActiveRecord::with] можно указать несколько связей, и жадная загрузка вернёт их за один раз.
Например, следующий код вернёт записи вместе с их авторами и категориями:

[php]
$posts=Post::model()->with('author','categories')->findAll();

Кроме того, можно осуществлять вложенную жадную загрузку. Для этого вместо простого списка имён связей, мы передаем методу
[with()|CActiveRecord::with] имена связей, упорядоченных иерархически, как в следующем примере:

[php]
$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->findAll();

Пример выше вернёт нам все записи вместе с их авторами и категориями, а также профиль каждого автора и все его записи.

Жадная загрузка может быть выполнена путём указания свойства [CDbCriteria::with]:

[php]
$criteria=new CDbCriteria;
$criteria->with=array(
    'author.profile',
    'author.posts',
    'categories',
);
$posts=Post::model()->findAll($criteria);

или

[php]
$posts=Post::model()->findAll(array(
    'with'=>array(
        'author.profile',
        'author.posts',
        'categories',
    )
));

2.34 Реляционный запрос без получения связанных моделей

Иногда требуется выполнить запрос с использованием связи, но при этом
не требуются данные из связанной модели. Допустим, есть пользователи (User),
которые публикуют множество записей (Post). Запись может быть опубликована,
а может быть черновиком. Этот факт определяется значением поля published модели Post.
Пусть нам необходимо получить всех пользователей, которые опубликовали хотя бы одну
запись, при этом сами записи нам не интересны. Сделать это можно следующим образом:

[php]
$users=User::model()->with(array(
    'posts'=>array(
        // записи нам не нужны
        'select'=>false,
        // но нужно выбрать только пользователей с опубликованными записями
        'joinType'=>'INNER JOIN',
        'condition'=>'posts.published=1',
    ),
))->findAll();

2.35 Параметры реляционного запроса

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

  • select: список выбираемых полей для связанного AR-класса. По умолчанию значение параметра равно ’*’,
    что соответствует всем полям таблицы. Для используемых столбцов должны быть разрешены конфликты имён.

  • condition: соответствует SQL оператору WHERE, по умолчанию значение параметра пустое.
    Для используемых столбцов должны быть разрешены конфликты имён.

  • params: параметры для связывания в генерируемом SQL-выражении. Параметры передаются как массив пар имя-значение.

  • on: соответствует SQL оператору ON. Условие, указанное в этом параметре,
    будет добавлено к основному условию соединения при помощи SQL оператора AND. Для используемых столбцов должны быть разрешены конфликты имён.
    Данный параметр неприменим для связей типа MANY_MANY.

  • order: соответствует SQL оператору ORDER BY, по умолчанию значение параметра пустое.
    Для используемых столбцов должны быть разрешены конфликты имён.

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

  • joinType: тип соединения таблиц. По умолчанию значение параметра равно LEFT OUTER JOIN;

  • alias: псевдоним таблицы, ассоциированной со связью. По умолчанию значение параметра
    равняется null, что означает, что псевдоним соответствует имени связи.

  • together: параметр, устанавливающий необходимость принудительного соединения таблицы, ассоциированной с этой связью,
    с другими таблицами. Этот параметр имеет смысл только для связей типов HAS_MANY и MANY_MANY. Если параметр не установлен или
    равен false, тогда каждая связь HAS_MANY или MANY_MANY будет использовать отдельный SQL-запрос для связанных данных,
    что может улучшить скорость выполнения запроса, т.к. уменьшается количество выбираемых данных.
    Если параметр равен true, то зависимая таблица при выполнении запроса всегда будет
    соединяться с основной, то есть будет выполнен один SQL-запрос даже в том случае, если
    к основной таблице применяется постраничная разбивка. Если данный параметр не
    задан, зависимая таблица будет соединена с основной только в случае, когда
    к основной таблице не применяется постраничная разбивка. Более подробное описание
    можно найти в разделе «производительность реляционного запроса».

  • join: дополнительный оператор JOIN. По умолчанию пуст. Этот параметр
    доступен с версии 1.1.3.

  • group: соответствует SQL оператору GROUP BY, по умолчанию значение параметра пустое.
    Для используемых столбцов должны быть разрешены конфликты имён.

  • having: соответствует SQL оператору HAVING, по умолчанию значение параметра пустое.
    Для используемых столбцов должны быть разрешены конфликты имён.

  • index: имя столбца таблицы, значения которого должны быть использованы в
    качестве ключей массива, хранящего связанные объекты. Без установки этого
    параметра массив связанных объектов использует целочисленный индекс,
    начинающийся с нуля. Параметр может быть установлен только для связей типа
    HAS_MANY и MANY_MANY.

  • scopes: группы условий, которые необходимо применить. В случае одной группы
    может задаваться в виде строки 'scopes'=>'scopeName'. Если же групп несколько, то
    их необходимо перечислить в массиве 'scopes'=>array('scopeName1','scopeName2'). Этот параметр
    доступен с версии 1.1.9.

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

  • limit: параметр для ограничения количества строк в выборке. Параметр неприменим для связей BELONGS_TO;

  • offset: параметр для указания начальной строки выборки. Параметр неприменим для связей BELONGS_TO.

  • through: имя связи модели, которое при получении данных будет
    использоваться как мост. Параметр может быть установлен только для связей
    HAS_ONE и HAS_MANY. Этот параметр доступен с версии 1.1.7, в которой можно применять его к HAS_ONE и HAS_MANY.
    Начиная с версии 1.1.14 он может использоваться с BELONGS_TO.

Ниже мы изменим определение связи posts в модели User, добавив несколько вышеприведенных параметров:

[php]
class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
                            'order'=>'posts.create_time DESC',
                            'with'=>'categories'),
            'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'),
        );
    }
}

Теперь при обращении к $author->posts, мы получим записи автора, отсортированные
в обратном порядке по времени их создания. Для каждой записи будут загружены
её категории.

2.36 Устранение конфликта имён столбцов

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

В реляционном запросе псевдоним главной таблицы всегда равен t,
а псевдоним связанной таблицы по умолчанию равен имени связи.
В приведённом ниже коде псевдонимы таблиц для моделей Post и Comment будут соответственно
t и comments:

[php]
$posts=Post::model()->with('comments')->findAll();

Допустим, что и в Post, и в Comment есть столбец create_time, в котором
хранится время создания записи или комментария, и нам необходимо получить записи
вместе с комментариями к ним, отсортированные сначала по времени создания
записи, а затем по времени написания комментария. Для этого нам понадобится
устранить конфликт столбцов create_time следующим образом:

[php]
$posts=Post::model()->with('comments')->findAll(array(
    'order'=>'t.create_time, comments.create_time'
));

Tip|Подсказка: Псевдоним таблицы связи по умолчанию равен названию самой связи. Имейте ввиду,
что при использовании одной связи внутри другой будет использовано название последней из них.
При этом название родительской связи не будет использовано в качестве префикса. Например, псевдонимом
связи ‘author.group’ является ‘group’, а не ‘author.group’.

[php]
$posts=Post::model()->with('author', 'author.group')->findAll(array(
  'order'=>'group.name, author.name, t.title'
));

Вы можете избежать конфликта псевдонимов таблиц задав свойство связи [alias|CActiveRelation::alias].

[php]
$comments=Comment::model()->with(
  'author',
  'post',
  'post.author'=>array('alias'=>'p_author'))->findAll(array(
  'order'=>'author.name, p_author.name, post.title'
));

2.37 Динамические параметры реляционного запроса

Мы можем использовать динамические параметры как для метода
[with()|CActiveRecord::with], так и для параметра with. Динамические параметры переопределяют существующие
параметры в соответствии с описанием метода [relations()|CActiveRecord::relations]. К примеру, если для модели User, приведённой выше,
мы хотим воспользоваться жадной загрузкой для получения записей автора в порядке возрастания (параметр order в определении связи
задает убывающий порядок), можно сделать это следующим образом:

[php]
User::model()->with(array(
    'posts'=>array('order'=>'posts.create_time ASC'),
    'profile',
))->findAll();

Динамические параметры в реляционных запросах можно использовать вместе с
отложенной загрузкой. Для этого необходимо вызвать метод с тем же именем, что и
имя связи, и передать параметры в качестве аргументов. К примеру, следующий код
вернёт публикации пользователя, у которых status равен 1:

[php]
$user=User::model()->findByPk(1);
$posts=$user->posts(array('condition'=>'status=1'));

2.38 Производительность реляционного запроса

Как было сказано выше, жадная загрузка используется, главным образом, когда
требуется получить множество связанных объектов. В этом случае при соединении
всех таблиц генерируется сложный SQL-запрос. Такой запрос во многих случаях
является предпочтительным, т.к. упрощает фильтрацию по значению столбца связанной таблицы.
Тем не менее, в некоторых случаях такие запросы не являются эффективными.

Рассмотрим пример, в котором нам необходимо найти последние добавленные записи вместе с их комментариями.
Предположив, что у каждой записи 10 комментариев, при использовании одного большого SQL-запроса
мы получим множество лишних данных, так как каждая запись будет повторно выбираться с каждым
её комментарием. Теперь попробуем по-другому: сначала выберем последние записи, а затем комментарии к ним.
В этом случае нам необходимо выполнить два SQL-запроса. Плюс в том, что полученные данные
не будут содержать дублирующую информацию.

Какой же подход более эффективен? Однозначного ответа на этот вопрос нет.
Выполнение одного большого SQL-запроса может оказаться более эффективным, так как СУБД
не приходится лишний раз разбирать и выполнять дополнительные запросы.
С другой стороны, используя один SQL-запрос, мы получаем лишние данные, а значит нам требуется больше времени на их передачу и обработку.
По умолчанию Yii использует жадную загрузку, то есть генерирует один SQL-запрос
за исключением случая, когда к главной модели применяется LIMIT. Если выставить
опцию together в описании связи в true, то мы получим единственный SQL-запрос
даже если используется LIMIT. Если использовать false, то выборка из
некоторых таблиц будет производиться отдельными запросами.
Например, для того чтобы использовать отдельные SQL-запросы для выборки
последних записей и комментариев к ним, связь comments модели Post следует
описать следующим образом:

[php]
public function relations()
{
    return array(
        'comments' => array(self::HAS_MANY, 'Comment', 'post_id', 'together'=>false),
    );
}

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

[php]
$posts = Post::model()->with(array('comments'=>array('together'=>false)))->findAll();

2.39 Статистический запрос

Помимо реляционных запросов, описанных выше, Yii также поддерживает так называемые статистические запросы (или запросы агрегирования).
Этот тип запросов используется для получения агрегированных данных, относящихся к связанным объектам (количество комментариев
к каждой записи, средний рейтинг для каждого наименования продукции и т.д.).
Статистические запросы могут быть использованы только для связей типа HAS_MANY (например, у записи есть много
комментариев) или MANY_MANY (например, запись принадлежит многим категориям, а категориия может относиться ко множеству записей).

Выполнение статистического запроса аналогично выполнению реляционного запроса. Первым делом необходимо
объявить статистический запрос в методе [relations()|CActiveRecord::relations] класса [CActiveRecord].

[php]
class Post extends CActiveRecord
{
    public function relations()
    {
        return array(
            'commentCount'=>array(self::STAT, 'Comment', 'post_id'),
            'categoryCount'=>array(self::STAT, 'Category', 'post_category(post_id, category_id)'),
        );
    }
}

Выше мы объявили два статистических запроса: commentCount подсчитывает количество комментариев к записи, а categoryCount
считает количество категорий, к которым относится запись. Обратите внимание, что связь между Post и Comment — типа HAS_MANY, а
связь между Post и Category — типа MANY_MANY (с использованием преобразующей таблицы post_category). Как можно видеть,
способ объявления похож на объявление связей, описанных выше. Единственное различие состоит в том, что в данном случае тип связи
равен STAT.

За счёт объявленных связей мы можем получить количество комментариев к записи, используя выражение $post->commentCount.
В момент первого обращения к данному свойству для получения соответствующего результата неявным образом выполняется SQL-запрос.
Как мы уже говорили, это называется подходом отложенной загрузки. Можно также использовать жадный вариант загрузки, если необходимо
получить количество комментариев к нескольким записям:

[php]
$posts=Post::model()->with('commentCount', 'categoryCount')->findAll();

Выражение выше выполняет три SQL-запроса для получения всех записей вместе с количеством комментариев к ним и числом категорий.
В случае отложенной загрузки нам бы понадобилось выполнить 2*N+1 SQL-запросов для N записей.

По умолчанию статистический запрос считает количество с использованием выражения COUNT.
Его можно уточнить путём указания дополнительных параметров в момент объявления в методе [relations()|CActiveRecord::relations].
Доступные параметры перечислены ниже:

  • select: статистическое выражение, по умолчанию равно COUNT(*), что соответствует количеству связанных объектов;

  • defaultValue: значение, которое присваивается в случае, если результат статистического запроса пуст.
    Например, если запись не имеет ни одного комментария, то свойству commentCount будет присвоено это значение. По умолчанию значение
    данного параметра равно 0;

  • condition: соответствует SQL оператору WHERE, по умолчанию значение параметра пустое;

  • params: параметры для связывания в генерируемом SQL-выражении. Параметры передаются
    как массив пар имя-значение;

  • order: соответствует SQL оператору ORDER BY, по умолчанию значение параметра пустое;

  • group: соответствует SQL оператору GROUP BY, по умолчанию значение параметра пустое;

  • having: соответствует SQL оператору HAVING, по умолчанию значение параметра пустое.

2.40 Реляционные запросы с именованными группами условий

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

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

[php]
$posts=Post::model()->published()->recently()->with('comments')->findAll();

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

В следующем примере показано, как применить группы условий к связанным моделям:

[php]
$posts=Post::model()->with('comments:recently:approved')->findAll();
// или, начиная с версии 1.1.7
$posts=Post::model()->with(array(
    'comments'=>array(
        'scopes'=>array('recently','approved')
    ),
))->findAll();
// или, начиная с версии 1.1.7
$posts=Post::model()->findAll(array(
    'with'=>array(
        'comments'=>array(
            'scopes'=>array('recently','approved')
        ),
    ),
));

Этот запрос вернёт все записи вместе с одобренными комментариями. Здесь comments
соответствует имени связи. recently и approved — именованные группы, описанные
в модели Comment. Имя связи и группы условий разделяются двоеточием.

Вам может понадобится использовать вместо «жадной» выборки «отложенную» для
связи с группой условий. Синтаксис для этого такой:

[php]
// имя связи comments повторяется два раза
$approvedComments = $post->comments('comments:approved');

Именованные группы могут быть использованы при описании связей модели в
методе [CActiveRecord::relations()] в параметре with. В следующем примере
при обращении к $user->posts вместе с публикациями будут получены все
одобренные комментарии.

[php]
class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
                'with'=>'comments:approved'),
        );
    }
}
// или, начиная с версии 1.1.7
class User extends CActiveRecord
{
    public function relations()
    {
        return array(
            'posts'=>array(self::HAS_MANY, 'Post', 'author_id',
                'with'=>array(
                    'comments'=>array(
                        'scopes'=>'approved'
                    ),
                ),
            ),
        );
    }
}

В версии 1.1.7 появилась возможность передавать параметры именованным группам условий
связи. К примеру, если в Post есть именованная группа условий rated,
принимающая минимальный рейтинг записи, использовать её в User можно следующим образом:

Note|Примечание: до версии 1.1.7 именованные группы условий, применяемые к реляционным моделям,
должны быть описаны в CActiveRecord::scopes. Поэтому они не могут быть параметризованы.

[php]
$users=User::model()->findAll(array(
    'with'=>array(
        'posts'=>array(
            'scopes'=>array(
                'rated'=>5,
            ),
        ),
    ),
));

class Post extends CActiveRecord
{
    ......

    public function rated($rating)
    {
        $this->getDbCriteria()->mergeWith(array(
            'condition'=>'rating=:rating',
            'params'=>array(':rating'=>$rating),
        ));
        return $this;
    }

    ......
}

2.41 Реляционные запросы с through

При использовании through определение связи должно выглядеть следующим образом:

[php]
'comments'=>array(self::HAS_MANY,'Comment',array('key1'=>'key2'),'through'=>'posts'),

В коде выше, а именно в array('key1'=>'key2'):

  • key1 — ключ, определённый в связи, на которую указывает through (в нашем случае posts).
  • key2 — ключ, определённый в модели, на которую указывает связь (в нашем случае Comment).

through может использоваться с HAS_ONE, BELONGS_TO и HAS_MANY.

2.41.1 HAS_MANY through

HAS_MANY through ER

Пример использования HAS_MANY с through — получение пользователей, состоящих
в определённой группе, если они записаны в группу через роли.

Более сложным примером является получение всех комментариев для всех пользователей
определённой группы. В этом случае необходимо использовать несколько связей
с through в одной модели:

[php]
class Group extends CActiveRecord
{
   ...
   public function relations()
   {
       return array(
           'roles'=>array(self::HAS_MANY,'Role','group_id'),
           'users'=>array(self::HAS_MANY,'User',array('user_id'=>'id'),'through'=>'roles'),
           'comments'=>array(self::HAS_MANY,'Comment',array('id'=>'user_id'),'through'=>'users'),
       );
   }
}

2.41.1.1 Примеры

[php]
// получаем все группы с соответствующими им пользователями
$groups=Group::model()->with('users')->findAll();

// получаем все группы с соответствующими им пользователями и ролями
$groups=Group::model()->with('roles','users')->findAll();

// получаем всех пользователей и роли для группы с ID, равным 1
$group=Group::model()->findByPk(1);
$users=$group->users;
$roles=$group->roles;

// получаем все комментарии для группы с ID, равным 1
$group=Group::model()->findByPk(1);
$comments=$group->comments;

2.41.2 HAS_ONE through

HAS_ONE through ER

Пример использования HAS_ONE с through — получение адреса пользователя в
случае, если пользователь связан с адресом через профиль. Все задействованные
сущности (пользователь, профиль и адрес) имеют соответствующие им модели:

[php]
class User extends CActiveRecord
{
   ...
   public function relations()
   {
       return array(
           'profile'=>array(self::HAS_ONE,'Profile','user_id'),
           'address'=>array(self::HAS_ONE,'Address',array('id'=>'profile_id'),'through'=>'profile'),
       );
   }
}

2.41.2.1 Примеры

[php]
// получаем адрес пользователя с ID, равным 1
$user=User::model()->findByPk(1);
$address=$user->address;

2.41.3 through с собой

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

through self ER

Связи для данного случая определяются следующим образом:

[php]
class User extends CActiveRecord
{
   ...
   public function relations()
   {
       return array(
           'mentorships'=>array(self::HAS_MANY,'Mentorship','teacher_id','joinType'=>'INNER JOIN'),
           'students'=>array(self::HAS_MANY,'User',array('student_id'=>'id'),'through'=>'mentorships','joinType'=>'INNER JOIN'),
       );
   }
}

2.41.3.1 Примеры

[php]
// получаем всех студентов учителя с ID, равным 1
$teacher=User::model()->findByPk(1);
$students=$teacher->students;

3 Объекты доступа к данным (DAO)

Объекты доступа к данным (DAO) предоставляют общий API для доступа к данным, хранящимся в
различных СУБД. Это позволяет без проблем поменять используемую СУБД на любую другую
без необходимости изменения кода, использующего DAO для доступа к данным.

Yii DAO является надстройкой над PHP Data Objects (PDO) -
расширением, которое предоставляет унифицированный доступ к данным многих популярных
СУБД, таких как MySQL, PostgreSQL. Поэтому для использования Yii DAO необходимо, чтобы
были установлены расширение PDO и соответствующий используемой базе данных драйвер PDO (например, PDO_MYSQL).

Yii DAO состоит из четырёх основных классов:

Далее мы проиллюстрируем использование Yii DAO на различных примерах.

3.1 Соединение с базой данных

Для установления соединения с базой необходимо создать экземпляр класса CDbConnection и
активировать его. Дополнительная информация, необходимая для подключения к БД (хост, порт, имя пользователя, пароль и т.д.), указывается
в DSN (Data Source Name). В случае возникновения ошибки в процессе соединения с БД будет выброшено исключение
(например, неверный DSN или неправильные имя пользователя/пароль).

[php]
$connection=new CDbConnection($dsn,$username,$password);
// устанавливаем соединение
// можно использовать конструкцию try…catch для перехвата возможных исключений
$connection->active=true;
…
$connection->active=false;  // close connection

Формат DSN зависит от используемого драйвера PDO. Как правило, DSN состоит из имени драйвера PDO,
за которым следует двоеточие, далее указываются параметры подключения, соответствующие синтаксису подключения
используемого драйвера. Подробнее с этим можно ознакомиться в документации PDO.
Ниже представлены несколько основных форматов DSN:

  • SQLite: sqlite:/path/to/dbfile
  • MySQL/MariaDB: mysql:host=localhost;dbname=testdb
  • PostgreSQL: pgsql:host=localhost;port=5432;dbname=testdb
  • SQL Server: mssql:host=localhost;dbname=testdb
  • Oracle: oci:dbname=//localhost:1521/testdb

Поскольку CDbConnection наследует класс [CApplicationComponent], мы можем использовать его
в качестве компонента приложения. Для этого нужно настроить
компонент db в конфигурации приложения
следующим образом:

[php]
array(
    …
    'components'=>array(
        …
        'db'=>array(
            'class'=>'CDbConnection',
            'connectionString'=>'mysql:host=localhost;dbname=testdb',
            'username'=>'root',
            'password'=>'password',
            'emulatePrepare'=>true,  // необходимо для некоторых версий инсталляций MySQL
        ),
    ),
)

Теперь мы можем получить доступ к соединению с БД через Yii::app()->db. Чтобы соединение не активировалось
автоматически, необходимо установить значение [CDbConnection::autoConnect] в false.
Этот способ даёт нам возможность использовать одно и то же подключение к БД в любом месте кода.

3.2 Выполнение SQL-запросов

Когда соединение с БД установлено, мы можем выполнять SQL-запросы, используя CDbCommand.
Для этого необходимо создать экземпляр класса CDbCommand путём вызова [CDbConnection::createCommand()], указав SQL-выражение:

[php]
$connection=Yii::app()->db; // так можно делать, если в конфигурации настроен компонент соединения "db"
// В противном случае можно создать соединение явно:
// $connection=new CDbConnection($dsn,$username,$password);
$command=$connection->createCommand($sql);
// при необходимости SQL-выражение можно изменить:
// $command->text=$newSQL;

Существуют два способа выполнения SQL-запросов с использованием CDbCommand:

  • В случае успешного выполнения возвращает количество затронутых строк.

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

Если в процессе выполнения SQL-запроса возникнет ошибка, то будет выброшено исключение.

[php]
$rowCount=$command->execute();   // выполнение запроса типа `INSERT`, `UPDATE` или `DELETE`
$dataReader=$command->query();   // выполнение запроса типа `SELECT`
$rows=$command->queryAll();      // возвращает все строки результата запроса
$row=$command->queryRow();       // возвращает первую строку результата запроса
$column=$command->queryColumn(); // возвращает первый столбец результата запроса
$value=$command->queryScalar();  // возвращает значение первого поля первой строки результата запроса

3.3 Обработка результатов запроса

После того как [CDbCommand::query()] создаст экземпляр класса CDbDataReader, мы можем
получить результат запроса построчно путём повторения вызовов метода [CDbDataReader::read()].
Для получения данных строка за строкой можно также использовать CDbDataReader в конструкциях foreach.

[php]
$dataReader=$command->query();
// многократно вызываем read() до возврата методом значения false
while(($row=$dataReader->read())!==false) { … }
// используем foreach для построчного обхода данных
foreach($dataReader as $row) { … }
// получаем все строки разом в виде массива
$rows=$dataReader->readAll();

Note|Примечание: Все методы queryXXX(), в отличие от query()|CDbCommand::query, возвращают
данные напрямую. Например, метод [queryRow()|CDbCommand::queryRow] возвращает массив, соответствующий первой
строке результата запроса.

3.4 Использование транзакций

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

  • Начало транзакции.
  • Выполнение запросов по очереди. В этот момент все изменения в базе недоступны извне.
  • Подтверждение транзакции. Если транзакция проведена успешно, то изменения становятся общедоступны.
  • При возникновении ошибки в ходе выполнения запросов происходит откат транзакции в начальное состояние.

Эту последовательность действий можно реализовать следующим образом:

[php]
$transaction=$connection->beginTransaction();
try
{
    $connection->createCommand($sql1)->execute();
    $connection->createCommand($sql2)->execute();
    //… прочие SQL-запросы
    $transaction->commit();
}
catch(Exception $e) // в случае возникновения ошибки при выполнении одного из запросов выбрасывается исключение
{
    $transaction->rollback();
}

3.5 Привязка параметров

Для предотвращения SQL-инъекций и
повышения производительности при выполнении однотипных SQL-запросов мы
можем «подготавливать» SQL-выражения, используя маркеры параметров (placeholders), которые в процессе привязки будут
заменяться на реальные значения.

Маркеры параметров могут быть именованными (уникальные маркеры) или неименованными (вопросительные знаки). Для замены маркеров на
реальные значения нужно вызвать методы [CDbCommand::bindParam()] или [CDbCommand::bindValue()].
Экранировать или заключать в кавычки значения параметров не нужно, используемый драйвер базы данных всё сделает сам.
Привязку параметров необходимо осуществить до выполнения SQL-запроса.

[php]
// выражение SQL с двумя именованными маркерами «:username» и «:email»
$sql="INSERT INTO tbl_user(username, email) VALUES(:username,:email)";
$command=$connection->createCommand($sql);
// заменяем маркер «:username» на соответствующее значение имени пользователя
$command->bindParam(":username",$username,PDO::PARAM_STR);
// заменяем маркер «:email» на соответствующее значение электронной почты
$command->bindParam(":email",$email,PDO::PARAM_STR);
$command->execute();
// вставляем следующую строку с новыми параметрами
$command->bindParam(":username",$username2,PDO::PARAM_STR);
$command->bindParam(":email",$email2,PDO::PARAM_STR);
$command->execute();

Методы [bindParam()|CDbCommand::bindParam] и [bindValue()|CDbCommand::bindValue] очень похожи.
Единственное различие состоит в том, что первый привязывает параметр к ссылке на переменную PHP,
а второй — к значению. Для параметров, представляющих большой объем данных, с точки зрения производительности
предпочтительнее использовать метод [bindParam()|CDbCommand::bindParam].

Подробнее о привязке параметров можно узнать в
соответствующей документации PHP.

3.6 Привязка полей

При получении результатов запроса мы также можем привязать поля таблицы к переменным PHP.
Это позволяет автоматически присваивать значения переменным при чтении очередной строки:

[php]
$sql="SELECT username, email FROM tbl_user";
$dataReader=$connection->createCommand($sql)->query();
// привязываем первое поле (username) к переменной $username
$dataReader->bindColumn(1,$username);
// привязываем второе поле (email) к переменной $email
$dataReader->bindColumn(2,$email);
while($dataReader->read()!==false)
{
    // переменные $username и $email получают значения полей username и email текущей строки
}

3.7 Использование префиксов таблиц

Yii предоставляет встроенную поддержку префиксов таблиц.
Префикс таблиц — это строка, предваряющая имена таблиц в текущей подключённой БД.
В основном, префиксы используются на виртуальном (shared) хостинге, где к одной БД
подключаются несколько приложений, использующих различные префиксы таблиц в целях избежания конфликтов имён.
Например, одно приложение использует префикс tbl_, а другое — yii_.

Для использования префикса таблиц установите свойство [CDbConnection::tablePrefix].
Затем в SQL-выражениях используйте {{TableName}} для указания имён таблиц, где TableName — имя
таблицы без префикса. Например, если БД содержит таблицу tbl_user, где tbl_ — это префикс таблиц,
то мы можем использовать следующий код для получения списка пользователей:

[php]
$sql='SELECT * FROM {{user}}';
$users=$connection->createCommand($sql)->queryAll();

4 Миграции

Note|Примечание: Миграции доступны с версии 1.1.6.

Как и исходный код, структура базы данных изменяется в процессе разработки
и поддержки приложения. К примеру, во время разработки может понадобиться
добавить новую таблицу или уже после размещения приложения на сервере добавить
индекс или столбец. При этом важно отслеживать изменения в структуре базы
данных (называемые миграциями) также, как мы делаем это для нашего исходного кода.
Если исходный код и база данных не соответствуют друг другу, скорее всего,
всё приложение не будет работать. Именно поэтому в Yii есть поддержка миграций,
позволяющая отслеживать изменения в базе данных, применять миграции или откатывать
уже применённые.

Ниже приведён пошаговый процесс использования миграций при разработке:

  1. Иван создаёт новую миграцию (например, создающую новую таблицу).
  2. Иван заливает её в систему контроля версий (SVN, GIT или другую).
  3. Андрей обновляется из системы контроля версий и получает новую миграцию.
  4. Андрей применяет миграцию к своей локальной базе данных.

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

Note|Примечание: При работе с командой migrate рекомендуется использовать
yiic приложения (то есть после cd path/to/protected), а не yiic из
директории framework. Убедитесь, что директория protected\migrations существует и
доступна для записи. Также проверьте настройки соединения с базой данных в protected/config/console.php.

4.1 Создание миграций

Для создания новой миграции (например, создающей таблицу для новостей), мы должны
ввести в консоли:

yiic migrate create <name>

Обязательный параметр name должен содержать очень краткое описание миграции
(например, create_news_table). Как будет показано далее,
этот параметр используется как часть имени класса миграции, поэтому использовать
можно только буквы, цифры и знаки подчёркивания.

yiic migrate create create_news_table

Приведённая команда создаст в директории protected/migrations файл
m101129_185401_create_news_table.php, содержащий следующее:

[php]
class m101129_185401_create_news_table extends CDbMigration
{
    public function up()
    {
    }


    public function down()
    {
        echo "m101129_185401_create_news_table does not support migration down.\n";
        return false;
    }

    /*
    // если требуется выполнить изменения внутри транзакции, используйте safeUp/safeDown
    // вместо up/down
    public function safeUp()
    {
    }

    public function safeDown()
    {
    }
    */
}

Стоит отметить, что имя класса совпадает с именем файла и строится как
m<timestamp>_<name>, где <timestamp> — это время создания миграции в UTC
(в формате yymmdd_hhmmss), а <name> — то, что передано в параметре
name команды.

Метод up() должен содержать код, выполняющий миграцию, а метод down() может
содержать код, отменяющий сделанное в up().

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

Info|Информация: Начиная с версии 1.1.7, если метод up() или метод down()
возвращают false, все последующие миграции не будут применены. В 1.1.6 для
этого было необходимо выкинуть исключение.

Рассмотрим в качестве примера миграцию, создающую таблицу с новостями.

[php]
class m101129_185401_create_news_table extends CDbMigration
{
    public function up()
    {
        $this->createTable('tbl_news', array(
            'id' => 'pk',
            'title' => 'string NOT NULL',
            'content' => 'text',
        ));
    }

    public function down()
    {
        $this->dropTable('tbl_news');
    }
}

Базовый класс [CDbMigration] предоставляет набор методов для работы с данными
и структурой базы данных. К примеру, при помощи [CDbMigration::createTable]
можно создать новую таблицу, а [CDbMigration::insert] добавит строку с данными.
Все эти методы используют подключение к базе данных, возвращаемое
[CDbMigration::getDbConnection()], что по умолчанию эквивалентно Yii::app()->db.

Info|Информация: Как вы могли заметить, методы [CDbMigration] очень похожи на
методы CDbCommand. И это на самом деле так. Единственно отличие состоит в том, что
методы [CDbMigration] подсчитывают затрачиваемое на их выполнение время и
выводят сообщения о параметрах методов.

4.2 Транзакционные миграции

Info|Информация: Данная возможность поддерживается, начиная с версии 1.1.7.

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

Можно начать транзакцию явно и заключить в неё весь код, меняющий базу данных:

[php]
class m101129_185401_create_news_table extends CDbMigration
{
    public function up()
    {
        $transaction=$this->getDbConnection()->beginTransaction();
        try
        {
            $this->createTable('tbl_news', array(
                'id' => 'pk',
                'title' => 'string NOT NULL',
                'content' => 'text',
            ));
            $transaction->commit();
        }
        catch(Exception $e)
        {
            echo "Exception: ".$e->getMessage()."\n";
            $transaction->rollback();
            return false;
        }
    }

    // …похожий код для down()
}

А можно сделать это проще, реализовав метод safeUp() вместо up() и safeDown()
вместо down():

[php]
class m101129_185401_create_news_table extends CDbMigration
{
    public function safeUp()
    {
        $this->createTable('tbl_news', array(
            'id' => 'pk',
            'title' => 'string NOT NULL',
            'content' => 'text',
        ));
    }

    public function safeDown()
    {
        $this->dropTable('tbl_news');
    }
}

Yii при применении миграции начнёт транзакцию, а затем выполнит код в
safeUp() или safeDown(). Если при этом возникнет какая-либо ошибка,
произойдёт откат транзакции, то есть база вернётся в начальное состояние.

Note|Примечание: Не все СУБД полностью поддерживают транзакции и не для всех
выражений. В том случае, если поддержки транзакций нет, реализовывать надо
up() и down(). В случае использования MySQL или MariaDB некоторые выражения SQL могут вызвать
неявное применение транзакции. Смотрите документацию MySQL
и MariaDB.

4.3 Применение миграций

Для того чтобы применить все новые миграции (то есть привести локальную БД в
актуальное состояние), следует запустить следующую команду:

yiic migrate

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

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

Иногда требуется применить лишь одну или несколько новых миграций. Для этого можно
использовать следующую команду:

yiic migrate up 3

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

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

yiic migrate to 101129_185401

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

4.4 Откат миграций

Для отката одной или нескольких последних применённых миграций можно
воспользоваться следующей командой:

yiic migrate down [step]

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

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

4.5 Повторное применение миграций

Повторное примение миграции производится путём последовательного отката и применения.
Осуществить это можно следующей командой:

yiic migrate redo [step]

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

4.6 Просмотр информации о миграциях

Кроме применения и отката миграций, инструмент миграций может отображать историю
миграций, а также новые, ещё не применённые миграции.

yiic migrate history [limit]
yiic migrate new [limit]

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

Первая команда показывает уже применённые миграции, вторая — миграции, которые
ещё не были применены.

4.7 Изменение истории миграций

Иногда требуется изменить историю миграций так, чтобы текущая версия была
заменена на указанную без применения или отката миграций. Часто это требуется
при созданнии новой миграции. Для этого можно использовать следующую команду:

yiic migrate mark 101129_185401

Эта команда очень похожа на yiic migrate to, но она лишь изменяет таблицу истории
миграций до указанной версии без применения или отката самих миграций.

4.8 Настройка команды миграций

Есть несколько способов настроить команду миграций.

4.8.1 Используя параметры командной строки

Команда миграций может быть настроена четыремя опциями:

  • interactive: использовать ли интерактивный режим. По умолчанию true, то есть
    при пременении миграции будет выводиться подтверждение. Если параметр выставлен
    в false, то миграции можно применить в фоновом режиме.

  • migrationPath: указывает директорию, в которой хранятся все файлы миграций.
    Путь должен указываться в формате псевдонима, и соответствующая ему директория должна
    существовать. Если параметр не указан, будет использована поддиректория
    migrations, находящаяся внутри директории с приложением;

  • migrationTable: указывает имя таблицы в базе данных, которая хранит историю
    миграций. Значение по умолчанию равно tbl_migration. Структура таблицы следующая:
    version varchar(255) primary key, apply_time integer;

  • connectionID: указывает идентификатор компонента базы данных.
    По умолчанию это ‘db’;

  • templateFile: указывает путь к файлу, который используется как шаблон
    для генерации классов миграций. Путь должен указываться как псевдоним
    (то есть как application.migrations.template). Если путь не задан, будет
    использоваться внутренний шаблон. В шаблоне токен {ClassName} будет заменён
    именем класса миграции.

Для указания опций используется следующий формат:

yiic migrate up --option1=value1 --option2=value2 ...

К примеру, если необходимо мигрировать модуль forum, файлы миграций которого
расположены в директории модуля migrations, можно воспользоваться следующей командой:

yiic migrate up --migrationPath=ext.forum.migrations

Стоит отметить, что при передаче через командную строку флагов, таких как interactive, необходимо использовать
значения 1 или 0:

yiic migrate --interactive=0

4.8.2 Глобальная конфигурация команды

В то время как опции командной строки позволяют нам на лету конфигурировать команду
миграций, иногда требуется применить настройки раз и навсегда. К примеру, нам
может понадобиться использовать другую таблицу для хранения истории миграций или
использовать свой шаблон миграции. Это можно сделать, изменив настройки консольного
приложения следующим образом:

[php]
return array(
    ......
    'commandMap'=>array(
        'migrate'=>array(
            'class'=>'system.cli.commands.MigrateCommand',
            'migrationPath'=>'application.migrations',
            'migrationTable'=>'tbl_migration',
            'connectionID'=>'db',
            'templateFile'=>'application.migrations.template',
        ),
        ......
    ),
    ......
);

Теперь при запуске команды migrate указанные выше настройки будут применены
без ввода каких-либо дополнительных параметров.
Работа с БД
===========

Yii предоставляет разработчику мощный инструмент для работы с базами данных —
объекты доступа к данным (DAO).

Yii DAO — это надстройка над расширением PHP Data Objects (PDO),
позволяющая работать с различными СУБД через единый интерфейс.
Приложения, разработанные с использованием DAO, могут легко
переключаться с одной СУБД на другую без необходимости внесения правок в программный код,
отвечающий за доступ к данным.

Конструктор запросов Yii предоставляет объектно-ориентированный способ
построения SQL-запросов, что позволяет понизить риск SQL-инъекций.

Yii Active Record (AR) реализует переработанный
подход объектно-реляционного проецирования (ORM) и ещё больше упрощает работу с
базами данных. Представляя таблицу базы данных как класс, а строки таблицы как
экземпляры класса, Yii AR избавляет от необходимости написания SQL-выражений,
связанных с операциями CRUD (создание, чтение, обновление и удаление).

Несмотря на то, что встроенные в Yii возможности для работы с БД подходят
практически для всех задач, касающихся работы с БД, также возможно использование
и других библиотек для работы с базами данных. Yii изначально был спроектирован таким
образом, чтобы разработчик имел возможность работы со сторонними библиотеками.
Конструктор запросов
====================

Конструктор запросов Yii предоставляет объектно-ориентированный способ
написания SQL-запросов. Он позволяет разработчику использовать методы и свойства
класса для того, чтобы указать отдельные части SQL-запроса. Затем конструктор
собирает отдельные части в единый SQL-запрос, который может быть выполнен вызовом
методов DAO, как описано в «Объекты доступа к данным (DAO)».
Следующий код показывает типичное использование конструктора запросов для создания
SQL-запроса SELECT:

[php]
$user = Yii::app()->db->createCommand()
    ->select('id, username, profile')
    ->from('tbl_user u')
    ->join('tbl_profile p', 'u.id=p.user_id')
    ->where('id=:id', array(':id'=>$id))
    ->queryRow();

Конструктор запросов лучше всего использовать в том случае, когда необходимо
собрать SQL-запрос, следуя некоторой условной логике приложения. Основными достоинствами
конструктора запросов являются:

  • Возможность собрать сложный SQL-запрос программно.

  • Автоматическое экранирование имён таблиц и полей для избежания конфликтов
    с ключевыми словами SQL и специальными символами.

  • Экранирование значений параметров и, где это возможно, использование привязки
    параметров, помогающей избежать SQL-инъекций.

  • Слой абстракции, упрощающий переход на другие СУБД.

Использовать конструктор запросов не обязательно. Если ваши запросы простые, легче
и быстрее использовать именно SQL.

Note|Примечание: Конструктор запросов не может быть использован для
изменения существующего запроса, заданного при помощи SQL.
К примеру, не будет работать следующий код:

[php]
$command = Yii::app()->db->createCommand('SELECT * FROM tbl_user');
// следующая строка НЕ добавит WHERE к SQL
$command->where('id=:id', array(':id'=>$id));

Не стоит использовать для одного запроса и SQL, и конструктор запросов.

4.9 Подготовка конструктора запросов

Конструктор запросов реализован в классе CDbCommand — главном классе для
работы с базой данных, описанном в разделе «Объекты доступа к данным (DAO)».

Для того чтобы начать его использовать, необходимо создать новый экземпляр
CDbCommand:

[php]
$command = Yii::app()->db->createCommand();

Здесь мы используем Yii::app()->db для получения соединения с базой данных и
затем вызываем [CDbConnection::createCommand()] для создания экземпляра команды.

Следует отметить, что теперь мы не передаём методу createCommand() готовое SQL-выражение, как это делалось
в случае с DAO.
Вместо этого мы соберём отдельные части запроса при помощи методов конструктора, которые описаны далее.

4.10 Запросы на получение данных

Запросы на получение данных соответствуют SQL-запросам SELECT. В конструкторе
есть ряд методов для сборки отдельных частей SELECT запроса. Так как все
эти методы возвращают экземпляр CDbCommand, мы можем использовать их
цепочкой, как показано в примере в начале этого раздела.

Рассмотрим использование перечисленных методов. Для простоты предположим, что
запросы делаются к MySQL. Для других СУБД способ экранирования названий таблиц, полей и значений,
используемый в примерах, может отличаться.

4.10.1 select()

[php]
function select($columns='*')

Метод select()|CDbCommand::select() задаёт часть запроса после SELECT. Параметр
$columns определяет выбираемые поля и может быть либо списком имён выбираемых
полей, разделённых запятой, либо массивом имён полей. Имена могут содержать
префиксы таблиц и псевдонимы полей. Метод автоматически экранирует имена, если
в них нет скобок (что означает использование выражения).

Несколько примеров:

[php]
// SELECT *
select()
// SELECT `id`, `username`
select('id, username')
// SELECT `tbl_user`.`id`, `username` AS `name`
select('tbl_user.id, username as name')
// SELECT `id`, `username`
select(array('id', 'username'))
// SELECT `id`, count(*) as num
select(array('id', 'count(*) as num'))

4.10.2 selectDistinct()

[php]
function selectDistinct($columns)

Метод selectDistinct()|CDbCommand::selectDistinct делает то же, что и метод
select()|CDbCommand::select(), но добавляет к выражению DISTINCT. К примеру,
selectDistinct('id, username') сгенерирует следующий SQL:

SELECT DISTINCT `id`, `username`

4.10.3 from()

[php]
function from($tables)

Метод from()|CDbCommand::from() задаёт часть запроса после FROM. Параметр
$tables определяет, из каких таблиц производится выборка, и может быть либо
списком имён таблиц, разделённых запятыми, либо массивом имён таблиц. Имена могут
содержать префиксы схемы (такие, как public.tbl_user) и псевдонимы таблиц
(такие, как tbl_user u). Метод автоматически экранирует имена, если в них нет
скобок (что означает использование подзапроса или выражения).

Примеры:

[php]
// FROM `tbl_user`
from('tbl_user')
// FROM `tbl_user` `u`, `public`.`tbl_profile` `p`
from('tbl_user u, public.tbl_profile p')
// FROM `tbl_user`, `tbl_profile`
from(array('tbl_user', 'tbl_profile'))
// FROM `tbl_user`, (select * from tbl_profile) p
from(array('tbl_user', '(select * from tbl_profile) p'))

4.10.4 where()

[php]
function where($conditions, $params=array())

Метод where()|CDbCommand::where() задаёт часть запроса после WHERE. Параметр
$conditions определяет условия запроса, а $params — параметры, которые
подставляются в запрос. Значение параметра $conditions может быть как строкой
(например, id=1), так и массивом следующего вида:

[php]
array(operator, operand1, operand2, ...)

где operator может быть одним из следующих:

  • and: операнды соединяются при помощи AND. К примеру, array('and', 'id=1', 'id=2')
    сгенерирует id=1 AND id=2. Если операнд является массивом, то он будет преобразован
    в строку с использованием описанных здесь правил. К примеру,
    array('and', 'type=1', array('or', 'id=1', 'id=2')) сгенерирует
    type=1 AND (id=1 OR id=2). Данный метод ничего НЕ экранирует.

  • or: то же, что и and, но для OR.

  • in: первый операнд должнен быть столбцом или выражением, второй — массивом,
    содержащим список значений, в которые должно входить значение поля или выражения.
    К примеру, array('in', 'id', array(1,2,3)) сгенерирует id IN (1,2,3).
    Метод экранирует имя столбца и значения в списке.

  • not in: то же, что и in, но вместо IN используется NOT IN.

  • like: первый операнд должен быть именем поля или выражением, второй — строкой
    или массивом, содержащим список значений, на которые должно быть похоже значение
    поля или выражения. К примеру, array('like', 'name', '%tester%') сгенерирует
    name LIKE '%tester%'. Когда список значений является массивом, генерируется несколько
    LIKE, соединённых при помощи AND. Например,
    array('like', 'name', array('%test%', '%sample%')) сгенерирует
    name LIKE '%test%' AND name LIKE '%sample%'. Метод экранирует имена полей и
    значения в списке.

  • not like: то же, что и like, но вместо LIKE генерируется NOT LIKE.

  • or like: то же, что и like но для соединения LIKE используется OR.

  • or not like: то же, что и not like но для соединения NOT LIKE используется OR.

Несколько примеров использования where:

[php]
// WHERE id=1 or id=2
where('id=1 or id=2')
// WHERE id=:id1 or id=:id2
where('id=:id1 or id=:id2', array(':id1'=>1, ':id2'=>2))
// WHERE id=1 OR id=2
where(array('or', 'id=1', 'id=2'))
// WHERE id=1 AND (type=2 OR type=3)
where(array('and', 'id=1', array('or', 'type=2', 'type=3')))
// WHERE `id` IN (1, 2)
where(array('in', 'id', array(1, 2))
// WHERE `id` NOT IN (1, 2)
where(array('not in', 'id', array(1,2)))
// WHERE `name` LIKE '%Qiang%'
where(array('like', 'name', '%Qiang%'))
// WHERE `name` LIKE '%Qiang' AND `name` LIKE '%Xue'
where(array('like', 'name', array('%Qiang', '%Xue')))
// WHERE `name` LIKE '%Qiang' OR `name` LIKE '%Xue'
where(array('or like', 'name', array('%Qiang', '%Xue')))
// WHERE `name` NOT LIKE '%Qiang%'
where(array('not like', 'name', '%Qiang%'))
// WHERE `name` NOT LIKE '%Qiang%' OR `name` NOT LIKE '%Xue%'
where(array('or not like', 'name', array('%Qiang%', '%Xue%')))

Стоит отметить, что в случае, когда оператор содержит like, необходимо явно
задавать спецсимволы (вроде % и _). Если паттерн вводится пользователем, то
необходимо использовать приведённый ниже код для экранирования спецсимволов и
предотвращения интерпретации их как спецсимволов:

[php]
$keyword=$_GET['q'];
// экранирует символы % и _
$keyword=strtr($keyword, array('%'=>'\%', '_'=>'\_'));
$command->where(array('like', 'title', '%'.$keyword.'%'));

4.10.5 order()

[php]
function order($columns)

Метод order()|CDbCommand::order() задаёт часть запроса после ORDER BY. Параметр
$columns определяет, по каким полям будет производиться сортировка. Поля могут
быть указаны как в виде строки, содержащей список полей и направлений
(ASC или DESC), разделённых запятыми, так и массив полей и направлений.
Имена полей могут содержать префиксы таблиц. Метод автоматически экранирует
имена полей, если они не содержат скобок (что означает использование выражения).

Несколько примеров:

[php]
// ORDER BY `name`, `id` DESC
order('name, id desc')
// ORDER BY `tbl_profile`.`name`, `id` DESC
order(array('tbl_profile.name', 'id desc'))

4.10.6 limit() и offset()

[php]
function limit($limit, $offset=null)
function offset($offset)

Методы limit()|CDbCommand::limit() и offset()|CDbCommand::offset() задают
части запроса, следующие после LIMIT и OFFSET. Стоит отметить, что не все
СУБД поддерживают именно синтаксис LIMIT и OFFSET. Если он не поддерживается,
то конструктор запросов переписывает весь SQL-запрос для достижения схожего
эффекта.

Несколько примеров:

[php]
// LIMIT 10
limit(10)
// LIMIT 10 OFFSET 20
limit(10, 20)
// OFFSET 20
offset(20)

4.10.7 join() и его варианты

[php]
function join($table, $conditions, $params=array())
function leftJoin($table, $conditions, $params=array())
function rightJoin($table, $conditions, $params=array())
function crossJoin($table)
function naturalJoin($table)

Метод join()|CDbCommand::join() и его варианты задают порядок и параметры
соединения таблиц с использованием INNER JOIN, LEFT OUTER JOIN,
RIGHT OUTER JOIN, CROSS JOIN и NATURAL JOIN. Параметр $table определяет
таблицу, с которой производится соединение. Имя таблицы может содержать
префикс схемы или псевдоним. Метод экранирует имя таблицы, если оно не
содержит скобок, что означает использование подзапроса или выражения. Параметр
$conditions задаёт условие соединения. Синтаксис такой же, как и у
where()|CDbCommand::where(). Через $params указываются параметры, подставляемые
в запрос.

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

Несколько примеров:

[php]
// JOIN `tbl_profile` ON user_id=id
join('tbl_profile', 'user_id=id')
// LEFT JOIN `pub`.`tbl_profile` `p` ON p.user_id=id AND type=1
leftJoin('pub.tbl_profile p', 'p.user_id=id AND type=:type', array(':type'=>1))

4.10.8 group()

[php]
function group($columns)

Метод group()|CDbCommand::group() задаёт часть запроса после GROUP BY.
Параметр $columns определяет поля, по которым будет осуществляться группировка,
и может быть либо строкой разделённых запятыми полей, либо массивом полей.
Имена полей могут содержать префиксы. Метод автоматически экранирует имена полей,
если они не содержат скобок (что означает использование выражений).

Несколько примеров:

[php]
// GROUP BY `name`, `id`
group('name, id')
// GROUP BY `tbl_profile`.`name`, `id`
group(array('tbl_profile.name', 'id'))

4.10.9 having()

[php]
function having($conditions, $params=array())

Метод having()|CDbCommand::having() задаёт часть запроса после HAVING. Используется
точно так же, как и where()|CDbCommand::where().

Несколько примеров:

[php]
// HAVING id=1 or id=2
having('id=1 or id=2')
// HAVING id=1 OR id=2
having(array('or', 'id=1', 'id=2'))

4.10.10 union()

[php]
function union($sql)

Метод union()|CDbCommand::union() задаёт часть запроса после UNION. Он добавляет
$sql к сгенерированному запросу, используя UNION. Несколько вызовов union()
добавят несколько частей запроса.

Несколько примеров:

[php]
// UNION (select * from tbl_profile)
union('select * from tbl_profile')

4.10.11 Выполнение запросов

После вызова приведённых выше методов для построения запроса, выполнить его можно,
используя методы DAO, как описано в разделе «Объекты доступа к данным (DAO)».
Например, мы можем использовать метод [CDbCommand::queryRow()] для получения строки или [CDbCommand::queryAll()]
для получения набора строк.

Пример:

[php]
$users = Yii::app()->db->createCommand()
    ->select('*')
    ->from('tbl_user')
    ->queryAll();

4.10.12 Получение SQL

Кроме выполнения запросов, которые мы создали при помощи конструктора, можно
также получить их SQL. Сделать это можно при помощи [CDbCommand::getText()].

[php]
$sql = Yii::app()->db->createCommand()
    ->select('*')
    ->from('tbl_user')
    ->text;

Если у запроса есть параметры, получить их можно при помощи свойства
[CDbCommand::params].

4.10.13 Альтернативный синтаксис построения запросов

Иногда использование цепочек вызовов может быть неоптимальным решением. Конструктор
запросов Yii позволяет создать запрос путём задания полей объекта. Для каждого
метода конструктора запросов есть соответствующее поле с таким же именем.
Присвоение значения полю эквивалентно вызову соответствующего метода. К примеру,
приведённые ниже строки эквивалентны, если $command — объект CDbCommand:

[php]
$command->select(array('id', 'username'));
$command->select = array('id', 'username');

Более того, метод [CDbConnection::createCommand()] может принимать массив в
качестве аргумента. Пары имя-значение из массива будут использованы для инициализации
полей созданного экземпляра CDbCommand. Таким образом, для построения запроса можно
использовать следующий код:

[php]
$row = Yii::app()->db->createCommand(array(
    'select' => array('id', 'username'),
    'from' => 'tbl_user',
    'where' => 'id=:id',
    'params' => array(':id'=>1),
))->queryRow();

4.10.14 Построение нескольких запросов

Для построения нескольких запросов экземпляр CDbCommand может быть
использован несколько раз. Перед тем как построить новый запрос, необходимо
вызвать метод [CDbCommand::reset()] для очистки предыдушего запроса. Пример:

[php]
$command = Yii::app()->db->createCommand();
$users = $command->select('*')->from('tbl_users')->queryAll();
$command->reset();  // очищаем предыдущий запрос
$posts = $command->select('*')->from('tbl_posts')->queryAll();

4.11 Построение запросов для изменения данных

К запросам для изменения данных относятся SQL-запросы для вставки, обновления и
удаления данных из базы. В конструкторе запросов есть соответствующие методы
insert, update и delete. В отличие от запросов получения данных, описанных
выше, данные методы строят полный SQL-запрос и тут же выполняют его.

4.11.1 insert()

[php]
function insert($table, $columns)

Метод insert()|CDbCommand::insert строит и выполняет SQL-запрос INSERT. Параметр
$table указывает, в какую таблицу производится вставка, а $columns является
массивом пар имя-значение полей для вставки. Метод экранирует имя таблицы и использует
параметры для вставляемых значений.

Пример:

[php]
// строим и выполняем следующий SQL:
// INSERT INTO `tbl_user` (`name`, `email`) VALUES (:name, :email)
$command->insert('tbl_user', array(
    'name'=>'Tester',
    'email'=>'tester@example.com',
));

4.11.2 update()

[php]
function update($table, $columns, $conditions='', $params=array())

Метод update()|CDbCommand::update строит и выполняет SQL-запрос UPDATE. Параметр
$table указывает обновляемую таблицу; $columns является массивом пар имя-значение,
задающим значения обновляемых полей; $conditions и $params эквивалентны аналогичным
параметрам в where()|CDbCommand::where() и определяют часть запроса UPDATE после
WHERE. Метод экранирует имя таблицы и использует параметры для обновляемых значений.

Пример:

[php]
// строим и выполняем следующий SQL:
// UPDATE `tbl_user` SET `name`=:name WHERE id=:id
$command->update('tbl_user', array(
    'name'=>'Tester',
), 'id=:id', array(':id'=>1));

4.11.3 delete

[php]
function delete($table, $conditions='', $params=array())

Метод delete()|CDbCommand::delete строит и выполняет SQL-запрос DELETE. Параметр
$table указывает таблицу, из которой удаляются записи; $conditions и $params
эквивалентны аналогичным параметрам в where()|CDbCommand::where(), которые определяют
часть запроса DELETE после WHERE. Метод экранирует имя таблицы.

Пример:

[php]
// строим и выполняем следующий SQL:
// DELETE FROM `tbl_user` WHERE id=:id
$command->delete('tbl_user', 'id=:id', array(':id'=>1));

4.12 Построение запросов изменения схемы

Кроме обычных запросов для получения данных и работы с ними, конструктор
может собирать и выполнять SQL-запросы для изменения схемы базы данных.
Поддерживаются следующие запросы:

Info|Информация: Несмотря на то что в разных СУБД запросы для измения схемы
различаются, конструктор запросов предоставляет единый интерфейс для их создания.
Это упрощает задачу мигрирования с одной СУБД на другую.

4.12.1 Абстрактные типы данных

Конструктор запросов вводит ряд абстрактных типов данных, которые можно
использовать для описания полей таблицы. В отличие от реальных типов данных,
которые отличаются в разных СУБД, абстрактные типы не зависят от СУБД.
При использовании их для описания типов полей конструктор запросов конвертирует
абстрактные типы в соответствующие им реальные.

Конструктор запросов поддерживает следующие абстрактные типы:

  • pk: обычный первичный ключ. Для MySQL конвертируется в int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY;
  • string: строка. Для MySQL конвертируется в varchar(255);
  • text: текстовый тип (длинная строка). Для MySQL конвертируется в text;
  • integer: целое. Для MySQL конвертируется в int(11);
  • float: число с плавающей точкой. Для MySQL конвертируется в float;
  • decimal: десятичное число. Для MySQL конвертируется в decimal;
  • datetime: дата и время. Для MySQL конвертируется в datetime;
  • timestamp: метка времени. Для MySQL конвертируется в timestamp;
  • time: время. Для MySQL конвертируется в time;
  • date: дата. Для MySQL конвертируется в date;
  • binary: бинарный. Для MySQL конвертируется в blob;
  • boolean: булевый. Для MySQL конвертируется в tinyint(1);
  • money: деньги/валюта. Для MySQL конвертируется в decimal(19,4). Доступен с версии 1.1.8.

4.12.2 createTable()

[php]
function createTable($table, $columns, $options=null)

Метод createTable()|CDbCommand::createTable строит и выполняет SQL-запрос для
создания таблицы. Параметр $table задаёт имя создаваемой таблицы. Параметр
$columns определяет поля новой таблицы. Они должны быть указаны в виде пар
имя-определение (т.е. 'username'=>'string'). Параметр $options задаёт
дополнительный фрагмент SQL, который будет добавлен к генерируемому SQL.
Конструктор запроса экранирует имя таблицы и имена полей.

Для указания определения поля можно использовать один из абстрактных типов данных, описанных выше.
Конструктор конвертирует абстрактный тип данных в соответствующий реальный тип данных в соответствии с используемой СУБД.
Например, string в случае MySQL преобразуется в varchar(255).

Определение поля также может содержать неабстрактный тип данных и спецификаций.
Они будут подставлены в результирующий SQL-запрос без каких-либо изменений. К примеру,
point не является абстрактным типом данных, и при использовании в определении
поля он будет включён в итоговый SQL без изменений. string NOT NULL будет
конвертирован в varchar(255) NOT NULL (т.е. конвертируются только абстрактный
тип string).

Пример создания таблицы:

[php]
// CREATE TABLE `tbl_user` (
//     `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
//     `username` varchar(255) NOT NULL,
//     `location` point
// ) ENGINE=InnoDB
createTable('tbl_user', array(
    'id' => 'pk',
    'username' => 'string NOT NULL',
    'location' => 'point',
), 'ENGINE=InnoDB')

4.12.3 renameTable()

[php]
function renameTable($table, $newName)

Метод renameTable()|CDbCommand::renameTable строит и выполняет SQL-запрос для
переименования таблицы. Параметр $table задаёт имя изменяемой таблицы.
Параметр $newName определяет новое имя таблицы. Конструктор запроса
экранирует имена таблицы.

Пример переименования таблицы:

[php]
// RENAME TABLE `tbl_users` TO `tbl_user`
renameTable('tbl_users', 'tbl_user')

4.12.4 dropTable()

[php]
function dropTable($table)

Метод dropTable()|CDbCommand::dropTable строит и выполняет SQL-запрос для удаления
таблицы. Параметр $table определяет имя удаляемой таблицы. Конструктор запроса
экранирует имя таблицы.

Пример удаления таблицы:

[php]
// DROP TABLE `tbl_user`
dropTable('tbl_user')

4.12.5 truncateTable()

[php]
function truncateTable($table)

Метод truncateTable()|CDbCommand::truncateTable строит и выполняет SQL-запрос для
очистки всех данных таблицы. Параметр $table определяет имя очищаемой таблицы.
Конструктор запроса экранирует имя таблицы.

Пример очистки таблицы:

[php]
// TRUNCATE TABLE `tbl_user`
truncateTable('tbl_user')

4.12.6 addColumn()

[php]
function addColumn($table, $column, $type)

Метод addColumn()|CDbCommand::addColumn строит и выполняет SQL-запрос для
добавления нового поля таблицы. Параметр $table задаёт имя таблицы, к которой
будет добавлено новое поле. Параметр $column — имя нового поля. $type
задаёт тип поля. Определение поля может содержать абстрактный тип данных,
как уже было описано в подразделе «createTable». Конструктор запроса
экранирует имя таблицы и имя поля.

Пример добавления поля:

[php]
// ALTER TABLE `tbl_user` ADD `email` varchar(255) NOT NULL
addColumn('tbl_user', 'email', 'string NOT NULL')

4.12.7 dropColumn()

[php]
function dropColumn($table, $column)

Метод dropColumn()|CDbCommand::dropColumn строит и выполняет SQL-запрос для
удаления поля таблицы. Параметр $table задаёт имя таблицы, из которой удаляется
поле. Параметр $column определяет имя удаляемого поля. Конструктор запроса
экранирует имя таблицы и имя поля.

Пример удаления поля таблицы:

[php]
// ALTER TABLE `tbl_user` DROP COLUMN `location`
dropColumn('tbl_user', 'location')

4.12.8 renameColumn()

[php]
function renameColumn($table, $name, $newName)

Метод renameColumn()|CDbCommand::renameColumn строит и выполняет SQL-запрос для
переименования поля таблицы. Параметр $table задаёт имя таблицы, поле которой
будет переименовано. Параметр $name определяет имя изменяемого поля.
$newName задаёт новое имя поля. Конструктор запроса экранирует имя таблицы и имена полей.

Пример переименования поля таблицы:

[php]
// ALTER TABLE `tbl_users` CHANGE `name` `username` varchar(255) NOT NULL
renameColumn('tbl_user', 'name', 'username')

4.12.9 alterColumn()

[php]
function alterColumn($table, $column, $type)

Метод alterColumn()|CDbCommand::alterColumn строит и выполняет SQL-запрос для
изменения поля таблицы. Параметр $table задаёт имя таблицы, поле которой
будет изменено. Параметр $column определяет имя изменяемого поля. $type
задаёт новое определение поля, которое может содержать абстрактный тип данных,
как было описано в подразделе «createTable». Конструктор запросов экранирует имя
таблицы и имя поля.

Пример изменения поля таблицы:

[php]
// ALTER TABLE `tbl_user` CHANGE `username` `username` varchar(255) NOT NULL
alterColumn('tbl_user', 'username', 'string NOT NULL')

4.12.10 addForeignKey()

[php]
function addForeignKey($name, $table, $columns,
    $refTable, $refColumns, $delete=null, $update=null)

Метод addForeignKey()|CDbCommand::addForeignKey строит и выполняет SQL-запрос для
добавления внешнего ключа в таблицу. Параметр $name задаёт имя внешнего ключа.
Параметры $table и $columns определяют имя таблицы и имя поля внешнего
ключа. Если указаны несколько полей, то они должны быть разделены запятыми.
Параметры $refTable и $refColumns определяют имя таблицы и имя поля, на которое
ссылается внешний ключ. Параметры $delete и $update задают SQL-опции
ON DELETE и ON UPDATE соответственно. Большинство СУБД поддерживают
следующие опции: RESTRICT, CASCADE, NO ACTION, SET DEFAULT и SET NULL.
Конструктор запросов экранирует имя таблицы, имя индекса и имена полей.

Пример добавления внешнего ключа:

[php]
// ALTER TABLE `tbl_profile` ADD CONSTRAINT `fk_profile_user_id`
// FOREIGN KEY (`user_id`) REFERENCES `tbl_user` (`id`)
// ON DELETE CASCADE ON UPDATE CASCADE
addForeignKey('fk_profile_user_id', 'tbl_profile', 'user_id',
    'tbl_user', 'id', 'CASCADE', 'CASCADE')

4.12.11 dropForeignKey()

[php]
function dropForeignKey($name, $table)

Метод dropForeignKey()|CDbCommand::dropForeignKey строит и выполняет SQL-запрос
для удаления внешнего ключа. Параметр $name задаёт имя внешнего ключа,
который требуется удалить. Параметр $table — имя таблицы, из которой
удаляется ключ. Конструктор запроса экранирует имя таблицы и имя ключа.

Пример удаления внешнего ключа:

[php]
// ALTER TABLE `tbl_profile` DROP FOREIGN KEY `fk_profile_user_id`
dropForeignKey('fk_profile_user_id', 'tbl_profile')

4.12.12 createIndex()

[php]
function createIndex($name, $table, $column, $unique=false)

Метод createIndex()|CDbCommand::createIndex строит и выполняет SQL-запрос
для создания индекса. Параметр $name задаёт имя индекса, который будет создан.
Параметр $table — имя таблицы, в которой создаётся индекс. Параметр $column
— имя индексируемого поля. Параметр $unique определяет, будет ли
индекс уникальным. Если индекс состоит из нескольких полей, то они разделяются
запятыми. Конструктор запросов экранирует имя таблицы, имя индекса и имена полей.

Пример создания индекса:

[php]
// CREATE INDEX `idx_username` ON `tbl_user` (`username`)
createIndex('idx_username', 'tbl_user', 'username')

4.12.13 dropIndex()

[php]
function dropIndex($name, $table)

Метод dropIndex()|CDbCommand::dropIndex строит и выполняет SQL-запрос для
удаления индекса. Параметр $name задаёт имя удаляемого индекса.
Параметр $table — имя таблицы, из которой удаляется индекс. Конструктор
запроса экранирует имя таблицы и имя индекса.

Пример удаления индекса:

[php]
// DROP INDEX `idx_username` ON `tbl_user`
dropIndex('idx_username', 'tbl_user')

5 Создание расширений

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

  • расширение должно быть самодостаточным, т.е. зависимость от внешних ресурсов должна быть минимальна. Очень неудобно, когда для работы
    расширения требуется устанавливать дополнительные пакеты, классы и иные файлы ресурсов;
  • все файлы расширения должны быть собраны в одной директории, имя которой должно совпадать с названием расширения;
  • классы расширения должны начинаться с префикса, чтобы избежать конфликтов имён с классами других расширений;
  • расширение должно включать подробную документацию по его установке и API, чтобы сократить время, затрачиваемое другими разработчиками на его подключение и изучение;
  • расширение должно использовать надлежащую лицензию. Если вы хотите, чтобы ваше расширение могло быть использовано как открытыми, так и закрытыми проектами,
    вы можете воспользоваться лицензиями BSD, MIT и др., но не GPL, т.к. последняя требует, чтобы код, в котором используется ваше расширение, также был открыт.

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

5.1 Компонент приложения

Компонент приложения должен реализовывать интерфейс [IApplicationComponent]
или расширять класс [CApplicationComponent]. Основной метод, который необходимо реализовать, — [IApplicationComponent::init].
В этом методе происходит инициализация компонента. Метод вызывается после того, как компонент создан и установлены начальные значения,
указанные в конфигурации приложения.

По умолчанию компонент приложения создаётся и инициализируется только в момент первого обращения к нему в ходе обработки запроса.
Если необходимо принудительно создавать компонент сразу после создания экземпляра приложения, то нужно добавить идентификатор
этого компонента в свойство [CApplication::preload].

5.2 Поведение

Для того чтобы создать поведение, необходимо реализовать интерфейс [IBehavior].
Для удобства в Yii имеется класс [CBehavior], реализующий этот интерфейс и
предоставляющий некоторые общие методы. Наследуемые классы могут реализовать
дополнительные методы, которые будут доступны в компонентах, к которым прикреплено
поведение.

При разработке поведений для [CModel] и [CActiveRecord] можно наследовать классы
[CModelBehavior] и [CActiveRecordBehavior] соответственно. Эти базовые классы
предоставляют дополнительные возможности, специально созданные для [CModel] и [CActiveRecord].
К примеру, класс [CActiveRecordBehavior] реализует набор методов для обработки
событий жизненного цикла ActiveRecord. Наследуемый класс, таким образом, может
перекрыть эти методы и выполнить код, участвующий в жизненном цикле AR.

Следующий код демонстрирует пример поведения ActiveRecord. Если это поведение
прикреплено к объекту AR и вызван метод save(), атрибутам create_time и
update_time будет автоматически присвоено текущее время.

[php]
class TimestampBehavior extends CActiveRecordBehavior
{
    public function beforeSave($event)
    {
        if($this->owner->isNewRecord)
            $this->owner->create_time=time();
        else
            $this->owner->update_time=time();
    }
}

5.3 Виджет

Виджет должен расширять класс [CWidget] или производные от него.

Наиболее простой способ создать виджет — расширить существующий виджет и переопределить его методы или заменить значения по умолчанию.
Например, если вы хотите заменить CSS стиль для [CTabView], то, используя виджет, нужно установить свойство [CTabView::cssFile]. Можно также расширить класс
[CTabView], чтобы при использовании виджета не требовалась постоянная конфигурация, следующим образом:

[php]
class MyTabView extends CTabView
{
    public function init()
    {
        if($this->cssFile===null)
        {
            $file=dirname(__FILE__).DIRECTORY_SEPARATOR.'tabview.css';
            $this->cssFile=Yii::app()->getAssetManager()->publish($file);
        }
        parent::init();
    }
}

Выше мы переопределяем метод [CWidget::init] и, если свойство [CTabView::cssFile] не установлено,
присваиваем ему значение URL нового CSS стиля по умолчанию. Файл нового CSS стиля необходимо поместить
в одну папку с файлом класса MyTabView, чтобы их можно было упаковать как расширение. Поскольку CSS стиль не доступен из веб, его
необходимо опубликовать как ресурс.

Чтобы написать виджет с нуля, нужно, как правило, реализовать два метода: [CWidget::init] и [CWidget::run].
Первый метод вызывается, когда мы используем конструкцию $this->beginWidget для вставки виджета в представление,
а второй — когда используется конструкция $this->endWidget.
Если необходимо получить и обработать содержимое, заключённое между вызовами этих двух функций,
то можно запустить буферизацию вывода в
[CWidget::init] и получать сохранённый вывод для дальнейшей обработки в методе [CWidget::run].

Когда виджет используется на странице, он обычно подключает CSS стили, JavaScript файлы и прочие файлы ресурсов.
Файлы такого рода называются ресурсы, поскольку они хранятся вместе с файлом класса виджета и, как правило, не доступны веб-пользователям.
Для того чтобы они стали доступны пользователям, их необходимо опубликовать, используя [CWebApplication::assetManager], как показано во фрагменте кода выше.
Помимо этого, если необходимо подключить файлы CSS стилей или JavaScript на текущей странице, их необходимо зарегистрировать посредством [CClientScript]:

[php]
class MyWidget extends CWidget
{
    protected function registerClientScript()
    {
        // …подключаем здесь файлы CSS или JavaScript…
        $cs=Yii::app()->clientScript;
        $cs->registerCssFile($cssFile);
        $cs->registerScriptFile($jsFile);
    }
}

Виджет также может иметь собственные файлы представлений. В этом случае необходимо создать директорию views
внутри директории, содержащей файл класса виджета, и поместить в неё все файлы представлений.
Для подключения представлений виджета в его классе используется конструкция $this->render('ViewName'), аналогичная соответствующему методу контроллера.

5.4 Действие

Действие должно расширять класс [CAction] или производные от него. [IAction::run] — основной метод, который необходимо
реализовать для действия.

5.5 Фильтр

Фильтр должен расширять класс [CFilter] или производные от него. Основными методами, которые необходимо реализовать,
являются [CFilter::preFilter] и [CFilter::postFilter]. Первый вызывается до выполнения действия, второй — после.

[php]
class MyFilter extends CFilter
{
    protected function preFilter($filterChain)
    {
        // применяется до выполнения действия
        return true; // значение false возвращается, если действие не должно выполняться
    }

    protected function postFilter($filterChain)
    {
        // применяется после завершения выполнения действия
    }
}

Параметр $filterChain — экземпляр класса [CFilterChain], содержащий информацию о действии, к которому применяются фильтры в настоящий момент.

5.6 Контроллер

Контроллер, распространяемый как расширение, должен наследовать класс [CExtController], а не класс [CController].
Основной причиной этого является то, что для класса [CController] предполагается, что файлы представлений располагаются
в application.views.ControllerID, а для [CExtController] считается, что файлы представлений находятся в директории views,
расположенной в той же директории, что и файл класса этого контроллера. Очевидно, что расширение-контроллер удобнее распространять, когда все
файлы расширения собраны в одном месте.

5.7 Валидатор

Валидатор должен расширять класс [CValidator] и реализовывать его метод [CValidator::validateAttribute].

[php]
class MyValidator extends CValidator
{
    protected function validateAttribute($model,$attribute)
    {
        $value=$model->$attribute;
        if($value has error)
            $model->addError($attribute,$errorMessage);
    }
}

5.8 Команда консоли

Консольная команда должна расширять класс [CConsoleCommand]
и реализовывать его метод [CConsoleCommand::run]. При желании можно переопределить
метод [CConsoleCommand::getHelp], который отвечает за информационную справку по команде.

[php]
class MyCommand extends CConsoleCommand
{
    public function run($args)
    {
        // $args — массив аргументов, переданных с командой
    }

    public function getHelp()
    {
        return 'Usage: how to use this command';
    }
}

5.9 Модуль

Информация о порядке создания и использования модулей представлена в разделе Модуль.

Если сформулировать требования в общем виде, то модуль должен быть самодостаточным, файлы ресурсов (CSS, JavaScript, изображения), используемые модулем, должны распространяться вместе с модулем, а сам модуль должен публиковать ресурсы, чтобы они были доступны для веб-пользователей.

5.10 Компонент общего вида

Разработка компонента общего вида аналогична написанию класса. Компонент, как и модуль,
должен быть самодостаточен и удобен для использования другими разработчиками.
Использование сторонних библиотек
=================================

Yii изначально спроектирован таким образом, чтобы использование сторонних библиотек
с целью расширения функциональности Yii, происходило легко и непринужденно.
Очень часто при использовании в работе сторонних библиотек разработчики сталкиваются с проблемами
именования классов и подключения файлов. Поскольку все классы Yii имеют префикс C, то вероятность
возникновения конфликтов имён существенно ниже. А благодаря тому, что для подключения файлов Yii
использует автозагрузку SPL, работа с библиотеками,
использующими для подключения файлов классов этот механизм автозагрузки или же относительный путь
подключения в РНР (PHP include path), становится существенно приятнее.

Ниже приведён пример, иллюстрирующий использование в Yii приложении компонента
Zend_Search_Lucene
из Zend Framework.

Первым делом, распаковываем релиз с Zend Framework в директорию protected/vendors, где protected
это базовая директория приложения.
Убедитесь в том, что файл protected/vendors/Zend/Search/Lucene.php существует.

Далее, в самом начале файла, содержащего класс контроллера, добавляем строку:

[php]
Yii::import('application.vendors.*');
require_once('Zend/Search/Lucene.php');

Код, приведённый выше, подключает файл Lucene.php. Поскольку используется относительный путь,
то необходимо изменить относительный путь подключения в РНР (PHP include path) таким образом, чтобы приложение могло
найти файл. Делается это путём вызова метода Yii::import перед require_once.

После того как проделано всё описанное, можно использовать класс Lucene в действиях контроллера следующим образом:

[php]
$lucene=new Zend_Search_Lucene($pathOfIndex);
$hits=$lucene->find(strtolower($keyword));

5.11 Подключение библиотек, использующих пространства имён

Для того чтобы подключить библиотеку, использующую пространство имён согласно соглашению
PSR-0
(например, Zend Framework 2 или Symfony2), необходимо зарегистрировать её корень как псевдоним пути.

В качестве примера будем использовать Imagine.
Если мы скопируем директорию Imagine в protected/vendors, то использовать библиотеку можно будет следующим
образом:

[php]
Yii::setPathOfAlias('Imagine',Yii::getPathOfAlias('application.vendors.Imagine'));

// Далее стандартный код из руководства Imagine:
// $imagine = new Imagine\Gd\Imagine();
// и т.д.

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

5.12 Использование стронних загрузчиков классов

Некоторые сторонние библиотеки, такие как PHPUnit, используют собственные загрузчики классов, не совместимые с Yii.
Так как загрузчик классов Yii, если класс не удаётся найти, пробует загрузку из include path PHP, то регистрация
сторонних загрузчиков может дать PHP Warning:

include(PHPUnit_Framework_TestCase.php) [function.include]: failed to open stream: No such file or directory

Для избежания данной проблемы стоит регистрировать сторонние загрузчики до загрузчика Yii:

[php]
require_once('PHPUnit/Autoload.php'); // register 3rd-party autoloader
require_once('/path/to/framework/yii.php'); // register Yii autoloader
...

Если сторонний загрузчик реализован в виде отдельной функции или метода, можно использовать Yii::registerAutoloader()
для его регистрации. В этом случае он будет зарегистриован до загрузчика Yii автоматически.

[php]
require_once('/path/to/framework/yii.php'); // регистрируем загрузчик Yii
...
Yii::registerAutoloader(array('SomeLibrary','autoload')); // регистрируем сторонний загрузчик
...

Ещё один способ избежать проблем со сторонними загрузчиками — запретить PHP include path выставив
YiiBase::$enableIncludePath в false до запуска приложения:

[php]
require_once('/path/to/framework/yii.php');
$configFile='/path/to/config/main.php';
Yii::$enableIncludePath = false; // запрещаем PHP include path
Yii::createWebApplication($configFile)->run();

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

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

[php]
require_once('путь/к/yii.php');
Yii::createWebApplication('путь/к/config.php');

Приведённый код очень похож на тот, который используется в index.php обычного
приложения. Отличие состоит в том, что после создания экземпляра приложения не вызывается
метод run().

Теперь при разработке сторонней системы можно использовать большинство
возможностей Yii. Например, для получения доступа к экземпляру приложения
можно использовать Yii::app(). Также можно использовать DAO, ActiveRecord
модели, валидацию и т.д.
Расширение Yii
==============

Расширение функциональности Yii — стандартная практика в процессе разработки. Например,
при написании нового контроллера вам необходимо расширить Yii путём наследования его класса
[CController]; при написании виджета — класса [CWidget] или класса уже существующего виджета.
Если созданный код предназначен для повторного его использования сторонними разработчиками,
то мы называем его расширением (extension).

Как правило, каждое расширение решает только одну задачу. Используя терминологию, принятую в Yii,
расширения можно классифицировать следующим образом:

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

Порядок использования расширений, как правило, включает три шага:

  1. Скачать расширение из репозитория расширений Yii;
  2. Распаковать расширение в поддиректорию extensions/xyz
    базовой директории приложения,
    где xyz — имя расширения;
  3. Подключить, настроить и использовать расширение.

Каждое расширение уникально идентифицируется по имени. Если расширение
называется xyz, то, используя псевдоним пути ext.xyz, мы всегда можем
определить директорию, в которой хранятся файлы этого расширения.

Разные расширения предъявляют различные требования к подключению, настройке и порядку их
использования. Ниже мы приведём несколько общих вариантов использования
расширений согласно классификации, представленной в
обзоре.

5.14 Расширения Zii

Перед тем как рассказать об использовании сторонних расширений, стоит упомянуть
библиотеку расширений Zii — набор расширений, разрабатываемый командой Yii и
включаемый в каждую новую версию.

При использовании расширения Zii необходимо обращаться к соответствующим классам,
используя псевдоним пути вида zii.path.to.ClassName. Здесь zii — предопределённый
в Yii маршрут, соответствующий корневой директории библиотеки Zii. К примеру, чтобы
воспользоваться [CGridView], необходимо использовать в шаблоне представления следующий код:

[php]
$this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider'=>$dataProvider,
));

5.15 Компонент приложения

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

[php]
return array(
    // 'preload'=>array('xyz',…),
    'components'=>array(
        'xyz'=>array(
            'class'=>'ext.xyz.XyzClass',
            'property1'=>'value1',
            'property2'=>'value2',
        ),
        // прочие настройки компонентов
    ),
);

Теперь можно обращаться к компоненту в любом месте приложения через
Yii::app()->xyz. Компонент будет создан в момент первого
обращения к нему (т.е. будет произведена отложенная загрузка), если мы не укажем его в свойстве preload.

5.16 Поведение

Поведение может быть
использовано в любом компоненте. Делается это в два шага: присоединение
к компоненту и вызов метода поведения из компонента. Например:

[php]
// $name уникально идентифицирует поведение внутри компонента
$component->attachBehavior($name,$behavior);
// test() является методом $behavior
$component->test();

Чаще всего поведение присоединяется к компоненту в ходе его конфигурирования, а
не посредством вызова метода attachBehavior. К примеру, чтобы присоединить поведение к
компоненту приложения,
мы можем использовать следующую
конфигурацию приложения:

[php]
return array(
    'components'=>array(
        'db'=>array(
            'class'=>'CDbConnection',
            'behaviors'=>array(
                'xyz'=>array(
                    'class'=>'ext.xyz.XyzBehavior',
                    'property1'=>'value1',
                    'property2'=>'value2',
                ),
            ),
        ),
        //…
    ),
);

Приведённый выше код присоединяет поведение xyz к компоненту приложения db.
Это возможно, так как [CApplicationComponent] определяет свойство behaviors.
При инициализации компонент присоединит перечисленные в нём поведения.

Для классов [CController], [CFormModel] и [CActiveRecord], которые необходимо расширять,
присоединение поведений происходит при помощи переопределения метода behaviors().
При инициализации классы автоматически присоединят поведения, объявленные в этом методе.
Например:

[php]
public function behaviors()
{
    return array(
        'xyz'=>array(
            'class'=>'ext.xyz.XyzBehavior',
            'property1'=>'value1',
            'property2'=>'value2',
        ),
    );
}

5.17 Виджет

Виджеты в основном используются в представлениях. Виджетом класса XyzClass,
входящим в состав расширения xyz, можно воспользоваться в представлении следующим образом:

[php]
// виджет без внутреннего содержимого
<?php $this->widget('ext.xyz.XyzClass', array(
    'property1'=>'value1',
    'property2'=>'value2')); ?>

// виджет, который может иметь внутреннее содержимое
<?php $this->beginWidget('ext.xyz.XyzClass', array(
    'property1'=>'value1',
    'property2'=>'value2')); ?>

…содержимое виджета…

<?php $this->endWidget(); ?>

5.18 Действие

Действия используются в
контроллерах для обработки запросов
пользователя. Действие класса XyzClass, входящее в расширение xyz, можно использовать
путём переопределения метода [CController::actions] класса нашего контроллера:

[php]
class TestController extends CController
{
    public function actions()
    {
        return array(
            'xyz'=>array(
                'class'=>'ext.xyz.XyzClass',
                'property1'=>'value1',
                'property2'=>'value2',
            ),
            // прочие действия
        );
    }
}

Теперь к действию можно обратиться по маршруту
test/xyz.

5.19 Фильтры

Фильтры также используются в
контроллерах. В основном они используются в
действиях для осуществления пред- и
пост-обработки пользовательского запроса. Фильтр класса XyzClass, входящий в расширение
xyz, можно использовать путём переопределения метода [CController::filters]
в нашем классе контроллера:

[php]
class TestController extends CController
{
    public function filters()
    {
        return array(
            array(
                'ext.xyz.XyzClass',
                'property1'=>'value1',
                'property2'=>'value2',
            ),
            // прочие фильтры
        );
    }
}

Выше мы можем использовать операторы ‘+’ и ‘-’ в первом элементе массива для
применения фильтра только к определенным действиям. Подробнее ознакомиться с этим можно
в документации к [CController].

5.20 Контроллер

Контроллер предоставляет набор действий,
которые могут быть запрошены пользователем. Для использования расширения
контроллера необходимо настроить свойство [CWebApplication::controllerMap] в
конфигурации приложения:

[php]
return array(
    'controllerMap'=>array(
        'xyz'=>array(
            'class'=>'ext.xyz.XyzClass',
            'property1'=>'value1',
            'property2'=>'value2',
        ),
        // прочие контроллеры
    ),
);

Теперь к действию a контроллера можно обратиться через
маршрут xyz/a.

5.21 Валидатор

Валидатор используется в классе модели, наследующем
[CFormModel] или [CActiveRecord]. Класс валидатора XyzClass расширения
xyz используется путём переопределения метода [CModel::rules] в нашем классе
модели:

[php]
class MyModel extends CActiveRecord // или CFormModel
{
    public function rules()
    {
        return array(
            array(
                'attr1, attr2',
                'ext.xyz.XyzClass',
                'property1'=>'value1',
                'property2'=>'value2',
            ),
            // прочие правила проверки
        );
    }
}

5.22 Команда консоли

Расширение консольной команды, как правило,
используется для добавления новой команды в утилиту yiic. Консольную команду
XyzClass расширения xyz можно использовать, настроив конфигурацию
консольного приложения:

[php]
return array(
    'commandMap'=>array(
        'xyz'=>array(
            'class'=>'ext.xyz.XyzClass',
            'property1'=>'value1',
            'property2'=>'value2',
        ),
        // прочие команды
    ),
);

Теперь в утилите yiic доступна ещё одна команда xyz.

Note|Примечание: Консольное приложение, как правило, использует иной файл
конфигурации, нежели веб-приложение. Если приложение было создано командой
консоли yiic webapp, то конфигурационный файл для консоли protected/yiic
находится в protected/config/console.php, а конфигурация веб-приложения — в
protected/config/main.php.

5.23 Модуль

Информация о порядке использования и создания модулей представлена в разделе
Модуль.

5.24 Компонент общего вида

Чтобы использовать компонент общего вида,
нужно для начала подключить его класс:

[php]
Yii::import('ext.xyz.XyzClass');

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

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

[php]
public function actionLogin()
{
    $model=new LoginForm;
    if(isset($_POST['LoginForm']))
    {
        // получаем данные от пользователя
        $model->attributes=$_POST['LoginForm'];
        // проверяем полученные данные и, если результат проверки положительный,
        // перенаправляем пользователя на предыдущую страницу
        if($model->validate())
            $this->redirect(Yii::app()->user->returnUrl);
    }
    // рендерим представление
    $this->render('login',array('model'=>$model));
}

Вначале мы создаём экземпляр модели LoginForm, затем, если данные формы были
отправлены, заполняем $model данными $_POST['LoginForm']. Далее проверяем
полученные данные и, если ошибок нет, перенаправляем пользователя на страницу,
для доступа к которой требовалось авторизоваться, т.е. ту страницу, которая
отправила пользователя на страницу авторизации. Если же результат проверки
отрицательный или действие выполняется впервые, то отображаем пользователю
представление login, которое рассмотрим в следующем разделе.

Tip|Подсказка: В действии login мы используем Yii::app()->user->returnUrl,
чтобы получить URL страницы, которая затребовала авторизацию. Компонент Yii::app()->user
является объектом класса [CWebUser] (или его производного), который позволяет получить информацию, хранящуюся в сессии
пользователя (например, имя пользователя, статус и пр.). Подробно ознакомиться с этой темой можно
в разделе Аутентификация и авторизация.

Обратим особое внимание на следующее выражение в действии login:

[php]
$model->attributes=$_POST['LoginForm'];

Как мы уже говорили в подразделе Безопасное присваивание значений атрибутам,
это выражение заполняет модель данными, которые ввёл пользователь. Свойство attributes определяется
классом [CModel], который ожидает получить массив пар имя-значение, чтобы затем присвоить каждому атрибуту модели
соответствующее значение. Следовательно, если $_POST['LoginForm'] содержит такой массив, то выражение,
которое мы привели выше, будет эквивалентно следующему коду (считаем, что массив содержит все нужные нам атрибуты):

[php]
$model->username=$_POST['LoginForm']['username'];
$model->password=$_POST['LoginForm']['password'];
$model->rememberMe=$_POST['LoginForm']['rememberMe'];

Note|Примечание: Для того чтобы $_POST['LoginForm'] возвращал массив вместо строки,
необходимо следовать правилам именования полей ввода в представлении. Так, поле, соответствующее
атрибуту a в классе модели C, должно называться C[a]. В нашем примере в качестве имени поля формы, соответствующего
атрибуту username, мы будем использовать LoginForm[username].

Теперь нам осталось только создать представление login, которое будет содержать
HTML форму с требуемыми полями.
Использование конструктора форм
===============================

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

5.25 Общая идея

Конструктор форм использует объект [CForm] для описания параметров, необходимых для
создания HTML формы, таких как модели и поля, используемые в форме, а также параметры
построения самой формы. Разработчику достаточно создать объект [CForm], задать его параметры
и вызвать метод для построения формы.

Параметры формы организованы в виде иерархии элементов формы. Корнем является
объект [CForm]. Корневой объект формы включает в себя две коллекции, содержащие
другие элементы: [CForm::buttons] и [CForm::elements]. Первая содержит кнопки
(такие как «Сохранить» или «Очистить»), вторая — поля ввода, статический текст и
вложенные формы — объекты [CForm], находящиеся в коллекции [CForm::elements]
другой формы. Вложенная форма может иметь свою модель данных и коллекции
[CForm::buttons] и [CForm::elements].

Когда пользователи отправляют форму, данные, введённые в поля ввода всей иерархии формы, включая
вложенные формы, передаются на сервер. [CForm] включает в себя методы, позволяющие
автоматически присвоить данные полям соответствующей модели и провести валидацию.

5.26 Создание простой формы

Ниже будет показано, как построить форму входа на сайт.

Сначала реализуем действие login:

[php]
public function actionLogin()
{
    $model = new LoginForm;
    $form = new CForm('application.views.site.loginForm', $model);
    if($form->submitted('login') && $form->validate())
        $this->redirect(array('site/index'));
    else
        $this->render('login', array('form'=>$form));
}

Вкратце, здесь мы создали объект [CForm], используя конфигурацию, найденную
по пути, который задан псевдонимом application.views.site.loginForm.
Объект [CForm], как описано в разделе «Создание модели»,
использует модель LoginForm.

Если форма отправлена, и все входные данные прошли проверку без ошибок,
перенаправляем пользователя на страницу site/index. Иначе выводим представление
login, описывающее форму.

Псевдоним пути application.views.site.loginForm указывает на файл PHP
protected/views/site/loginForm.php. Этот файл возвращает массив, описывающий
настройки, необходимые для [CForm]:

[php]
return array(
    'title'=>'Пожалуйста, представьтесь',

    'elements'=>array(
        'username'=>array(
            'type'=>'text',
            'maxlength'=>32,
        ),
        'password'=>array(
            'type'=>'password',
            'maxlength'=>32,
        ),
        'rememberMe'=>array(
            'type'=>'checkbox',
        )
    ),

    'buttons'=>array(
        'login'=>array(
            'type'=>'submit',
            'label'=>'Вход',
        ),
    ),
);

Настройки, приведённые выше, являются ассоциативным массивом, состоящим из пар имя-значение,
используемых для инициализации соответствующих свойств [CForm]. Самыми важными
свойствами, как мы уже упомянули, являются [CForm::elements] и [CForm::buttons].
Каждое из них содержит массив, определяющий элементы формы. Более детальное описание
элементов формы будет приведено в следующем подразделе.

Опишем шаблон представления login:

[php]
<h1>Вход</h1>

<div class="form">
<?php echo $form; ?>
</div>

Tip|Подсказка: Приведённый выше код echo $form; эквивалентен echo $form->render();.
Использование более компактной записи возможно, так как [CForm] реализует магический
метод __toString, в котором вызывается метод render(), возвращающий
код формы.

5.27 Описание элементов формы

При использовании конструктора форм вместо написания разметки мы, главным образом,
описываем элементы формы. В данном подразделе мы опишем, как задать свойство [CForm::elements].
Мы не будем описывать [CForm::buttons], так как конфигурация этого свойства практически
ничем не отличается от [CForm::elements].

Свойство [CForm::elements] является массивом, каждый элемент которого соответствует
элементу формы. Это может быть поле ввода, статический текст или вложенная форма.

5.27.1 Описание поля ввода

Поле ввода, главным образом, состоит из заголовка, самого поля, подсказки и текста ошибки и
должно соответствовать определённому атрибуту модели. Описание поля ввода содержится в
экземпляре класса [CFormInputElement]. Приведённый ниже код массива [CForm::elements]
описывает одно поле ввода:

[php]
'username'=>array(
    'type'=>'text',
    'maxlength'=>32,
),

Здесь указано, что атрибут модели называется username, тип поля — text и его атрибут
maxlength равен 32.

Любое доступное для записи свойство [CFormInputElement] может быть настроено приведённым выше
способом. К примеру, можно задать свойство [hint|CFormInputElement::hint] для того, чтобы
показывать подсказку или свойство [items|CFormInputElement::items], если
поле является выпадающим списком или группой элементов checkbox или radio.
Если имя опции не является свойством [CFormInputElement], оно будет считаться атрибутом
соответствующего HTML-тега input. Например, так как опция maxlength не является
свойством [CFormInputElement], она будет использована как атрибут maxlength HTML-элемента
input.

Следует отдельно остановиться на свойстве [type|CFormInputElement::type].
Оно определяет тип поля ввода. К примеру, тип text означает, что будет использован
элемент формы input, а password — поле для ввода пароля. В [CFormInputElement]
реализованы следующие типы полей ввода:

  • text
  • hidden
  • password
  • textarea
  • file
  • radio
  • checkbox
  • listbox
  • dropdownlist
  • checkboxlist
  • radiolist

Отдельно следует описать использование “списочных” типов dropdownlist, checkboxlist
и radiolist. Для них необходимо задать свойство [items|CFormInputElement::items]
соответствующего элемента input. Сделать это можно так:

[php]
'gender'=>array(
    'type'=>'dropdownlist',
    'items'=>User::model()->getGenderOptions(),
    'prompt'=>'Выберите значение:',
),

…

class User extends CActiveRecord
{
    public function getGenderOptions()
    {
        return array(
            0 => 'Мужчина',
            1 => 'Женщина',
        );
    }
}

Данный код сгенерирует выпадающий список с текстом «Выберите значение:» и опциями
«Мужчина» и «Женщина», которые мы получаем из метода getGenderOptions модели User.

Кроме данных типов полей, в свойстве [type|CFormInputElement::type] можно указать
класс или псевдоним пути виджета. Класс виджета должен наследовать [CInputWidget] или [CJuiInputWidget].
В ходе генерации элемента формы будет создан и выполнен экземпляр класса виджета.
Виджет будет использовать конфигурацию, переданную через настройки элемента формы.

5.27.2 Описание статического текста

Довольно часто в форме, помимо полей ввода, содержится некоторая декоративная
HTML разметка. К примеру, горизонтальный разделитель для выделения определённых
частей формы или изображение, улучшающее внешний вид формы. Подобный HTML код
можно описать в коллекции [CForm::elements] как статический текст. Для этого
в [CForm::elements] в нужном нам месте вместо массива необходимо использовать строку.
Например:

[php]
return array(
    'elements'=>array(
        ......
        'password'=>array(
            'type'=>'password',
            'maxlength'=>32,
        ),

        '<hr />',

        'rememberMe'=>array(
            'type'=>'checkbox',
        )
    ),
    ......
);

В приведённом коде мы вставили горизонтальный разделитель между полями password и rememberMe.

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

5.27.3 Описание вложенных форм

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

Вложенная форма, как и главная, описывается объектом [CForm]. Для того чтобы
описать вложенную форму, необходимо определить элемент типа form в
свойстве [CForm::elements]:

[php]
return array(
    'elements'=>array(
        ......
        'user'=>array(
            'type'=>'form',
            'title'=>'Данные для входа',
            'elements'=>array(
                'username'=>array(
                    'type'=>'text',
                ),
                'password'=>array(
                    'type'=>'password',
                ),
                'email'=>array(
                    'type'=>'text',
                ),
            ),
        ),

        'profile'=>array(
            'type'=>'form',
            ......
        ),
        ......
    ),
    ......
);

Так же как и у главной формы, у вложенной формы необходимо задать свойство [CForm::elements].
Если вложенной форме необходимо сопоставить модель данных, то это можно сделать, задав свойство [CForm::model].

В некоторых случаях бывает полезно определить форму в объекте класса, отличного от [CForm].
К примеру, как будет показано ниже, можно расширить [CForm] для реализации своего
алгоритма построения разметки. При указании типа элемента form, вложенная форма
будет автоматически использовать объект того же класса, что и у главной формы.
Если указать тип элемента как, например, XyzForm (строка, оканчивающаяся на Form),
то вложенная форма будет использовать объект класса XyzForm.

5.28 Доступ к элементам формы

Обращаться к элементам формы так же просто, как и к элементам массива. Свойство [CForm::elements] возвращает
объект [CFormElementCollection], наследуемый от [CMap], что позволяет получить доступ к элементам формы как к
элементам массива. Таким образом, чтобы обратиться к элементу username формы login из вышеприведённого примера,
можно использовать следующий код:

[php]
$username = $form->elements['username'];

Аналогично, для доступа к элементу email формы регистрации, можно использовать следующий код:
~~~
[php]
$email = $form->elements[‘user’]->elements[‘email’];
~~~

Так как [CForm] реализует доступ к элементам [CForm::elements] как к массиву, можно упростить
приведённый код до:

[php]
$username = $form['username'];
$email = $form['user']['email'];

5.29 Создание вложенной формы

Ранее мы уже описывали вложенные формы. Форма, содержащая вложенные формы, называется главной.
В данном разделе мы будем использовать форму регистрации пользователя в качестве примера
создания вложенных форм, соответствующих нескольким моделям данных. Далее данные для входа
пользователя хранятся в модели User, а данные профиля — в модели Profile.

Реализуем действие register следующим образом:

[php]
public function actionRegister()
{
    $form = new CForm('application.views.user.registerForm');
    $form['user']->model = new User;
    $form['profile']->model = new Profile;
    if($form->submitted('register') && $form->validate())
    {
        $user = $form['user']->model;
        $profile = $form['profile']->model;
        if($user->save(false))
        {
            $profile->userID = $user->id;
            $profile->save(false);
            $this->redirect(array('site/index'));
        }
    }

    $this->render('register', array('form'=>$form));
}

Выше мы создаём форму, используя настройки из application.views.user.registerForm.
После отправки данных формы и успешной их валидации мы пытаемся сохранить модели
пользовательских данных и профиля. Мы получаем модели через свойство model соответствующего
объекта вложенной формы. Так как валидация уже пройдена, мы вызываем $user->save(false)
с параметром false, позволяющим не проводить её повторно. Точно так же поступаем с
моделью профиля.

Далее описываем настройки формы в файле protected/views/user/registerForm.php:

[php]
return array(
    'elements'=>array(
        'user'=>array(
            'type'=>'form',
            'title'=>'Данные для входа',
            'elements'=>array(
                'username'=>array(
                    'type'=>'text',
                ),
                'password'=>array(
                    'type'=>'password',
                ),
                'email'=>array(
                    'type'=>'text',
                )
            ),
        ),

        'profile'=>array(
            'type'=>'form',
            'title'=>'Профиль',
            'elements'=>array(
                'firstName'=>array(
                    'type'=>'text',
                ),
                'lastName'=>array(
                    'type'=>'text',
                ),
            ),
        ),
    ),

    'buttons'=>array(
        'register'=>array(
            'type'=>'submit',
            'label'=>'Зарегистрироваться',
        ),
    ),
);

При задании каждой вложенной формы мы указываем свойство [CForm::title].
По умолчанию при построении HTML-формы каждая вложенная форма будет выведена в
fieldset с заданным нами заголовком.

Описываем очень простой код шаблона представления register:

[php]
<h1>Регистрация</h1>

<div class="form">
<?php echo $form; ?>
</div>

5.30 Настройка отображения формы

Главное преимущество при использовании конструктора форм — разделение логики
(конфигурация формы хранится в отдельном файле) и отображения (метод [CForm::render]).
Мы можем настроить рендеринг формы, переопределив метод [CForm::render] либо используя собственный файл представления.
Оба варианта позволяют не менять конфигурацию формы и использовать её повторно.

При переопределении [CForm::render] необходимо, главным образом, обойти коллекции [CForm::elements]
и [CForm::buttons] и вызвать метод [CFormElement::render] для каждого элемента. Например:

[php]
class MyForm extends CForm
{
    public function render()
    {
        $output = $this->renderBegin();

        foreach($this->getElements() as $element)
            $output .= $element->render();

        $output .= $this->renderEnd();

        return $output;
    }
}

Также можно использовать представление _form:

[php]
<?php
echo $form->renderBegin();

foreach($form->getElements() as $element)
    echo $element->render();

echo $form->renderEnd();

Для этого достаточно написать:

[php]
<div class="form">
<?php $this->renderPartial('_form', array('form'=>$form)); ?>
</div>

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

[php]
какие-нибудь сложные элементы интерфейса

<?php echo $form['username']; ?>

какие-нибудь сложные элементы интерфейса

<?php echo $form['password']; ?>

какие-нибудь сложные элементы интерфейса

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

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

В зависимости от того, каким образом используются введённые данные,
мы можем использовать два типа моделей. Если мы получаем данные, обрабатываем их,
а затем удаляем, то используем модель формы; если же
после получения и обработки данных мы сохраняем их в базе данных, то используем Active Record.
Оба типа моделей данных используют один и тот же базовый класс [CModel], который определяет
общий интерфейс, используемый формами.

Note|Примечание: В примерах этого раздела используются модели формы. Тем не менее,
всё может быть в равной степени применено и к моделям Active Record.

5.31 Определение класса модели

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

[php]
class LoginForm extends CFormModel
{
    public $username;
    public $password;
    public $rememberMe=false;
}

Мы объявили в LoginForm три атрибута: $username, $password и $rememberMe.
Они используются для хранения имени пользователя, пароля, а также значения опции сохранения
имени пользователя. Так как $rememberMe по умолчанию имеет значение false, то изначально
в форме аутентификации галочка этой опции будет снята.

Info|Информация: Термин «атрибут» используется, чтобы отделить свойства, определяемые этими
переменными-членами класса, от прочих свойств. Здесь атрибут — это свойство, которое
используется в основном для хранения данных, вводимых пользователем, или данных,
получаемых из базы данных.

5.32 Определение правил проверки

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

[php]
class LoginForm extends CFormModel
{
    public $username;
    public $password;
    public $rememberMe=false;

    private $_identity;

    public function rules()
    {
        return array(
            array('username, password', 'required'),
            array('rememberMe', 'boolean'),
            array('password', 'authenticate'),
        );
    }

    public function authenticate($attribute,$params)
    {
        $this->_identity=new UserIdentity($this->username,$this->password);
        if(!$this->_identity->authenticate())
            $this->addError('password','Неправильное имя пользователя или пароль.');
    }
}

В коде, представленном выше, username и password — обязательные для заполнения поля,
поле password должно быть проверено также на соответствие указанному имени пользователя.
Поле rememberMe может принимать значения true или false.

Каждое правило, возвращаемое rules(), должно быть задано в следующем формате:

[php]
array('AttributeList', 'Validator', 'on'=>'ScenarioList', …дополнительные параметры)

где AttributeList — строка, содержащая разделённые запятыми имена атрибутов, которые должны
быть проверены в соответствии с правилами; Validator указывает на тип используемой проверки;
параметр on — необязательный параметр, определяющий список сценариев, в которых должно
использоваться правило; дополнительные параметры — это пары имя-значение, которые используются для
инициализации значений свойств соответствующих валидаторов.

Начиная с версии 1.1.11 можно исключать отдельные правила. Если вы не хотите
проводить валидацию для какого-либо правила и сценария, можно указать параметр
except с указанием имени сценария. Синтаксис точно такой же, как и для
параметра on.

Список сценариев (в параметрах on и except) может быть указан двумя
эквивалентными способами:

[php]
// в виде массива имён сценариев
'on'=>array('update', 'create'),
// строкой с именами, разделённой запятыми (пробелы не учитываются)
'except'=>'ignore, this, scenarios, at-all',

Есть три способа указать Validator в правиле проверки. Во-первых, Validator может быть именем
метода в классе модели данных, аналогично authenticate в примере выше. Метод проверки
оформляется следующим образом:

[php]
/**
 * @param string $attribute имя поля, которое будем валидировать
 * @param array $params дополнительные параметры для правила валидации
 */
public function ValidatorName($attribute,$params) { … }

Второй способ — указать Validator в качестве имени класса. В этом случае для проверки данных в момент применения правила создаётся
экземпляр класса проверки. Дополнительные параметры в правиле используются для
инициализации значений его атрибутов. Класс проверки должен быть производным классом от [CValidator].

Третий вариант — предопределить псевдоним класса валидатора. В примере выше
required — это псевдоним класса [CRequiredValidator], который проверяет, не является ли значение атрибута пустым.
Ниже приведён полный список предопределенных псевдонимов валидаторов,
включенных в состав Yii:

  • boolean: псевдоним класса [CBooleanValidator], который проверяет, равно ли
    значение атрибута [CBooleanValidator::trueValue] или [CBooleanValidator::falseValue];

  • captcha: псевдоним класса [CCaptchaValidator], который проверяет, равно ли
    значение атрибута коду верификации на капче;

  • compare: псевдоним класса [CCompareValidator], который проверяет, совпадает ли
    значение атрибута со значением другого атрибута или константой;

  • email: псевдоним класса [CEmailValidator], который отвечает за проверку корректности email адреса;

  • date: псевдоним класса [CDateValidator], проверяющего, является ли атрибут
    корректной датой, временем или и тем и другим.

  • default: псевдоним класса [CDefaultValueValidator], который присваивает значение
    по умолчанию выбранным атрибутам;

  • exist: псевдоним класса [CExistValidator], который проверяет наличие значения атрибута в указанном столбце таблицы базы данных;

  • file: псевдоним класса [CFileValidator], отвечающего за проверку атрибута на
    наличие в нём имени загруженного файла;

  • filter: псевдоним класса [CFilterValidator], преобразующего атрибут с использованием фильтра;

  • in: псевдоним класса [CRangeValidator], который проверяет, содержится ли значение атрибута в
    указанном наборе значений;

  • length: псевдоним класса [CStringValidator], который проверяет, находится ли длина строкового значения атрибута в
    указанном интервале;

  • match: псевдоним класса [CRegularExpressionValidator], проверяющего значение атрибута на соответствие регулярному выражению;

  • numerical: псевдоним класса [CNumberValidator], проверяющего, является ли значение атрибута числом;

  • required: псевдоним класса [CRequiredValidator], который проверяет, не является ли значение атрибута пустым;

  • type: псевдоним класса [CTypeValidator], проверяющего значение атрибута на соответствие указанному типу данных;

  • unique: псевдоним класса [CUniqueValidator], который проверяет, является ли значение атрибута уникальными в пределах столбца таблицы базы данных;

  • url: псевдоним класса [CUrlValidator], отвечающего за проверку корректности URL.

Ниже представлены несколько примеров использования предопределенных валидаторов:

[php]
// имя пользователя — обязательное поле формы
array('username', 'required'),
// длина имени пользователя должна быть от 3 до 12 символов включительно
array('username', 'length', 'min'=>3, 'max'=>12),
// в сценарии регистрации значения полей «password» и «password2» должны быть равны
array('password', 'compare', 'compareAttribute'=>'password2', 'on'=>'register'),
// в сценарии аутентификации поле `password` должно быть проверено на соответствие указанному имени пользователя
array('password', 'authenticate', 'on'=>'login'),

5.33 Безопасное присваивание значений атрибутам

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

[php]
$model=new LoginForm;
if(isset($_POST['LoginForm']))
    $model->attributes=$_POST['LoginForm'];

Последнее выражение в примере как раз и является массовым присваиванием, где значение каждой переменной в
$_POST['LoginForm'] присваивается соответствующему атрибуту модели.
Это эквивалентно следующей операции:

[php]
foreach($_POST['LoginForm'] as $name=>$value)
{
    if($name является безопасным атрибутом)
        $model->$name=$value;
}

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

5.33.1 Описание безопасных атрибутов

Атрибут считается безопасным, если он присутствует в правиле валидации,
применяемом в данном сценарии. Например,

[php]
array('username, password', 'required', 'on'=>'login, register'),
array('email', 'required', 'on'=>'register'),

В коде выше атрибуты username и password необходимы в сценарии
login, а атрибуты username, password и email — в
сценарии register. В результате, если мы проводим массовое присваивание в
сценарии login, то только атрибуты username и password будут массово
присвоены, т.к. только они входят в правило валидации для сценария login.
С другой стороны, если текущим сценарием является register, то все три атрибута могут
быть массово присвоены.

[php]
// сценарий входа
$model=new User('login');
if(isset($_POST['User']))
    $model->attributes=$_POST['User'];

// сценарий регистрации
$model=new User('register');
if(isset($_POST['User']))
    $model->attributes=$_POST['User'];

Так почему же мы используем именно такую политику для определения, является атрибут
безопасным или нет? Если атрибут уже есть в одном или
нескольких правилах валидации, зачем беспокоиться о чём-то ещё?

Важно помнить, что правила валидации используются для проверки введённых
пользователем данных, а не данных, которые мы генерируем в коде (например,
текущее время или автоматически сгенерированный первичный ключ). Поэтому
НИКОГДА НЕ ДОБАВЛЯЙТЕ правила валидации для атрибутов, не доступных для ввода конечным пользователем.

Иногда мы хотим объявить атрибут безопасным, даже если в действительности не
имеем правила для него. Пример — текст статьи, который может
принимать любое введённое пользователем значение. Для этого мы можем использовать
специальное правило safe:

[php]
array('content', 'safe')

Существует также правило unsafe, используемое для явного указания небезопасного атрибута:

[php]
array('permission', 'unsafe')

Правило unsafe используется редко и является противоположным описанному
нами ранее определению безопасных атрибутов.

Для установки значений небезопасных атрибутов мы должны использовать отдельные операции присваивания:

[php]
$model->permission='admin';
$model->id=1;

5.34 Выполнение проверки

Как только модель будет заполнена пользовательскими данными, мы можем вызвать метод [CModel::validate()],
чтобы запустить процесс проверки. По итогам проверки метод возвращает положительный
или отрицательный результат. Для моделей [CActiveRecord] проверка может выполняться
автоматически в момент вызова метода [CActiveRecord::save()].

Мы можем задать сценарий через свойство [scenario|CModel::scenario], таким образом
указав, какой набор правил будет использован для проверки.

Проверка выполняется в зависимости от сценария. Свойство [scenario|CModel::scenario]
задаёт сценарий, в котором используется модель, и определяет, какой набор правил валидации
будет использован. К примеру, в сценарии login мы хотим проверить только поля модели
пользователя username и password. В сценарии register нам необходимо проверять
большее количество данных: email, address и т.д. Ниже показано, как провести
проверку для сценария register:

[php]
// создаём модель User и устанавливаем сценарий `register`. Выражение ниже эквивалентно следующему:
// $model=new User;
// $model->scenario='register';
$model=new User('register');

// наполняем модель данными
$model->attributes=$_POST['User'];

// выполняем проверку
if($model->validate())   // если данные верны
    …
else
    …

Сценарий для правил проверки задаётся в свойстве on правила. Если on не определено,
правило используется для всех сценариев. Например,

[php]
public function rules()
{
    return array(
        array('username, password', 'required'),
        array('password_repeat', 'required', 'on'=>'register'),
        array('password', 'compare', 'on'=>'register'),
    );
}

Первое правило будет распространяться на любые сценарии, а два последующих будут применяться
только к сценарию register.

5.35 Информация об ошибках

После проверки все возможные ошибки находятся в объекте модели. Мы можем получить
их через [CModel::getErrors()] и [CModel::getError()]. Первый метод возвращает
все ошибки для указанного атрибута модели, второй — только первую ошибку.

Чтобы узнать, возникли ли во время выполнения проверки какие-либо ошибки,
можно воспользоваться методом [CModel::hasErrors()]. И, если ошибки действительно есть,
получить их можно с помощью метода [CModel::getErrors()]. Оба метода могут быть
использованы как для всех, так и для конкретного атрибута.

5.36 Метки атрибутов

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

По умолчанию [CModel] в качестве меток возвращает названия атрибутов. Изменить их можно,
переопределив метод [attributeLabels()|CModel::attributeLabels].

Далее мы увидим, что возможность указания меток в модели данных позволяет быстро создавать сложные формы.
Работа с формами
=================

Сбор пользовательских данных с использованием HTML форм — одна из основных
задач в разработке веб-приложений. Кроме непосредственного проектирования формы,
разработчик должен заполнить эту форму имеющимися данными или значениями по умолчанию,
проверить введённые данные, отобразить соответствующие сообщения в случае
некорректности данных ввода, а также сохранить данные, например, в базу данных.
Yii существенно упрощает этот процесс за счёт своей MVC архитектуры.

Работа с формами в Yii, как правило, включает в себя следующие шаги:

  1. Создание класса модели данных, представляющей поля для ввода.
  2. Создание действия контроллера, код которого будет реагировать на отправку формы.
  3. Создание формы в файле представления, ассоциированного с действием контроллера.

Далее мы подробно опишем каждый из этих шагов.
Обработка табличного ввода
==========================

Иногда нам может потребоваться обрабатывать данные, вводимые пользователем, в пакетном режиме. Иначе говоря,
случаются ситуации, когда пользователь вводит данные для множества экземпляров модели и отправляет их
на сервер все разом. Будем называть это табличным вводом (tabular input), поскольку поля ввода, как
правило, организованы в виде таблицы.

Для работы с табличным вводом нам, в первую очередь, понадобится создать или заполнить массив, состоящий
из экземпляров модели данных, в зависимости от того, добавляем мы данные или обновляем. Затем мы извлекаем
данные из переменной $_POST и присваиваем их каждой модели. В отличие от ввода данных для одной модели,
здесь для присваивания значений атрибутам мы будем использовать $_POST['ModelClass'][$i] вместо $_POST['ModelClass'].

[php]
public function actionBatchUpdate()
{
    // извлекаем элементы, которые будем обновлять в пакетном режиме,
    // предполагая, что каждый элемент является экземпляром класса модели 'Item'
    $items=$this->getItemsToUpdate();
    if(isset($_POST['Item']))
    {
        $valid=true;
        foreach($items as $i=>$item)
        {
            if(isset($_POST['Item'][$i]))
                $item->attributes=$_POST['Item'][$i];
            $valid=$item->validate() && $valid;
        }
        if($valid)  // все элементы корректны
            // …некоторая обработка
    }
    // отображаем представление с формой для ввода табличных данных
    $this->render('batchUpdate',array('items'=>$items));
}

Когда действие готово, необходимо создать представление batchUpdate таким образом, чтобы
отобразить все необходимые поля ввода в HTML таблице:

[php]
<div class="form">
<?php echo CHtml::beginForm(); ?>
<table>
<tr><th>Имя</th><th>Стоимость</th><th>Количество</th><th>Описание</th></tr>
<?php foreach($items as $i=>$item): ?>
<tr>
<td><?php echo CHtml::activeTextField($item,"[$i]name"); ?></td>
<td><?php echo CHtml::activeTextField($item,"[$i]price"); ?></td>
<td><?php echo CHtml::activeTextField($item,"[$i]count"); ?></td>
<td><?php echo CHtml::activeTextArea($item,"[$i]description"); ?></td>
</tr>
<?php endforeach; ?>
</table>

<?php echo CHtml::submitButton('Сохранить'); ?>
<?php echo CHtml::endForm(); ?>
</div><!-- form -->

Обратите внимание, что выше мы используем "[$i]name" вместо "name" в
качестве второго параметра при вызове метода [CHtml::activeTextField].

В случае если возникают ошибки валидации, соответствующие поля ввода будут подсвечены автоматически,
как и в случае работы с одной моделью, рассмотренной ранее.
Создание формы
==============

Написание формы не должно вызвать никаких затруднений. Мы начинаем с тега form, атрибут
action которого должен содержать URL действия login, рассмотренного ранее. Затем добавляем метки
и поля ввода для атрибутов, объявленных в классе LoginForm. В завершение, мы вставляем кнопку
отправки данных формы. Всё это без проблем пишется на чистом HTML коде.

Для упрощения процесса создания формы Yii предоставляет несколько классов-помощников (helpers).
Например, для создания текстового поля, можно вызвать метод [CHtml::textField()], для выпадающего списка —
[CHtml::dropDownList()].

Info|Информация: Безусловно, может возникнуть справедливый вопрос, а в чём преимущество использования
помощника, если объём написанного кода сравним с чистым HTML кодом? Ответ прост: использование
помощника даёт большие возможности. Например, код, приведённый ниже, создаёт текстовое поле, отправляющее
данные формы на сервер, когда пользователь меняет её значение.
~~~
[php]
CHtml::textField($name,$value,array(‘submit’=>’’));
~~~
Заметьте, что всё реализовано без единой строчки JavaScript.

Ниже мы создаём представление — форму авторизации — с помощью класса [CHtml]. Здесь переменная $model
экземпляр класса LoginForm:

[php]
<div class="form">
<?php echo CHtml::beginForm(); ?>

<?php echo CHtml::errorSummary($model); ?>

<div class="row">
<?php echo CHtml::activeLabel($model,'username'); ?>
<?php echo CHtml::activeTextField($model,'username'); ?>
</div>

<div class="row">
<?php echo CHtml::activeLabel($model,'password'); ?>
<?php echo CHtml::activePasswordField($model,'password'); ?>
</div>

<div class="row rememberMe">
<?php echo CHtml::activeCheckBox($model,'rememberMe'); ?>
<?php echo CHtml::activeLabel($model,'rememberMe'); ?>
</div>

<div class="row submit">
<?php echo CHtml::submitButton('Войти'); ?>
</div>

<?php echo CHtml::endForm(); ?>
</div><!-- form -->

Форма, которую мы создали выше, обладает куда большей динамичностью. К примеру,
[CHtml::activeLabel()] создаёт метку, соответствующую атрибуту модели, и если при вводе данных была
допущена ошибка, то CSS класс метки сменится на error, изменив внешний вид метки в соответствии с CSS стилями.
Похожим образом метод [CHtml::activeTextField()] создаёт текстовое поле для соответствущего атрибута модели и графически выделяет ошибки ввода.

Если использовать файл CSS стилей form.css, создаваемый скриптом yiic, то наша форма будет
выглядеть так:

Страница авторизации

Страница авторизации с сообщением об ошибке

Начиная с версии 1.1.1, для создания форм можно воспользоваться новым
виджетом [CActiveForm], который позволяет реализовать валидацию как на клиенте,
так и на сервере. При использовании [CActiveForm] код отображения будет выглядеть
следующим образом:

[php]
<div class="form">
<?php $form=$this->beginWidget('CActiveForm'); ?>

    <?php echo $form->errorSummary($model); ?>

    <div class="row">
        <?php echo $form->label($model,'username'); ?>
        <?php echo $form->textField($model,'username') ?>
    </div>

    <div class="row">
        <?php echo $form->label($model,'password'); ?>
        <?php echo $form->passwordField($model,'password') ?>
    </div>

    <div class="row rememberMe">
        <?php echo $form->checkBox($model,'rememberMe'); ?>
        <?php echo $form->label($model,'rememberMe'); ?>
    </div>

    <div class="row submit">
        <?php echo CHtml::submitButton('Войти'); ?>
    </div>

<?php $this->endWidget(); ?>
</div><!-- form -->

6 Полное руководство по Yii

Данное руководство выпущено в соответствии с положениями о документации Yii.

6.1 Переводчики

  • Константин Мирин, Konstantin Mirin (programmersnotes.info)
  • Александр Макаров, Sam Dark (rmcreative.ru)
  • Алексей Лукьяненко, Caveman (caveman.ru)
  • Евгений Халецкий, xenon
  • Александр Овчинников, multif
  • Сергей Кузнецов, cr0t (summercode.ru)
  • Александр Кожевников, Bethrezen (bethrezen.ru)

© 2008—2013, Yii Software LLC.
Конфигурация веб-серверов Apache и Nginx
========================================

6.2 Apache

Yii готов к работе с настроенным по умолчанию Apache. Файлы
.htaccess во фреймворке и директориях приложения ограничивают доступ к
некоторым ресурсам. Для сокрытия файла точки входа (обычно это index.php) в
URL можно добавить инструкцию для модуля mod_rewrite в файл .htaccess
в корневой директории приложения или в настройках виртуальных хостов:

RewriteEngine on

# не позволять httpd отдавать файлы, начинающиеся с точки (.htaccess, .svn, .git и прочие)
RedirectMatch 403 /\..*$
# если директория или файл существуют, использовать их напрямую
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# иначе отправлять запрос на файл index.php
RewriteRule . index.php

6.3 Nginx

Yii можно использовать с веб-сервером Nginx и PHP с
помощью FPM SAPI. Ниже приведён пример простой
конфигурации хоста. Он определяет файл точки входа и заставляет Yii
перехватывать все запросы к несуществующим файлам, что позволяет создавать
человекопонятные URL-адреса.

server {
    set $host_path "/www/mysite";
    access_log  /www/mysite/log/access.log  main;

    server_name  mysite;
    root   $host_path/htdocs;
    set $yii_bootstrap "index.php";

    charset utf-8;

    location / {
        index  index.html $yii_bootstrap;
        try_files $uri $uri/ /$yii_bootstrap?$args;
    }

    location ~ ^/(protected|framework|themes/\w+/views) {
        deny  all;
    }

    # отключаем обработку запросов фреймворком к несуществующим статичным файлам
    location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {
        try_files $uri =404;
    }

    # передаем PHP-скрипт серверу FastCGI, прослушивающему адрес 127.0.0.1:9000
    location ~ \.php {
        fastcgi_split_path_info  ^(.+\.php)(.*)$;

        # позволяем yii перехватывать запросы к несуществующим PHP-файлам
        set $fsn /$yii_bootstrap;
        if (-f $document_root$fastcgi_script_name){
            set $fsn $fastcgi_script_name;
        }

        fastcgi_pass   127.0.0.1:9000;
        include fastcgi_params;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fsn;

        # PATH_INFO и PATH_TRANSLATED могут быть опущены, но стандарт RFC 3875 определяет для CGI
        fastcgi_param  PATH_INFO        $fastcgi_path_info;
        fastcgi_param  PATH_TRANSLATED  $document_root$fsn;
    }

    # не позволять nginx отдавать файлы, начинающиеся с точки (.htaccess, .svn, .git и прочие)
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Используя данную конфигурацию, можно в файле php.ini установить опцию
cgi.fix_pathinfo=0 во избежание множества нежелательных системных вызовов stat().
Генерация кода при помощи консоли (устаревшее)
==============================================

Note|Примечание: Генераторы кода yiic shell считаются устаревшими, начиная с
версии 1.1.2. Пожалуйста, используйте более мощные расширяемые веб-генераторы
Gii.

Откроем консоль и выполним следующие команды:

% cd WebRoot/testdrive
% protected/yiic shell
Yii Interactive Tool v1.1
Please type 'help' for help. Type 'exit' to quit.
>> model User tbl_user
   generate models/User.php
   generate fixtures/tbl_user.php
   generate unit/UserTest.php

The following model classes are successfully generated:
    User

If you have a 'db' database connection, you can test these models now with:
    $model=User::model()->find();
    print_r($model);

>> crud User
   generate UserController.php
   generate UserTest.php
   mkdir D:/testdrive/protected/views/user
   generate create.php
   generate update.php
   generate index.php
   generate view.php
   generate admin.php
   generate _form.php
   generate _view.php

Crud 'user' has been successfully created. You may access it via:
http://hostname/path/to/index.php?r=user

В примере выше мы использовали команду shell утилиты yiic для взаимодействия с
созданным каркасом приложения. В командной строке мы вводим две команды: model User tbl_user и crud User.
Команда model автоматически создает класс модели User, основываясь на структуре таблицы tbl_user,
а команда crud генерирует класс контроллера и файлы представлений, которые обеспечивают
выполнение соответствующих операций CRUD.

Note|Примечание: Даже если проверка соответствия требованиям показывает, что расширение PDO и драйвер PDO, соответствующий используемой базе
данных, включены, могут возникать ошибки типа «…could not find driver». В этом случае необходимо запустить утилиту yiic следующим образом:

% php -c path/to/php.ini protected/yiic.php shell

где path/to/php.ini — путь до файла PHP ini

Давайте порадуемся нашим трудам, перейдя по следующему URL:

http://hostname/testdrive/index.php?r=user

Мы увидим страницу со списком пользователей из таблицы tbl_user. Поскольку наша таблица пустая, то записей в ней не будет.
Кликнем по кнопке Create User и, если мы еще не авторизованы, отобразится страница авторизации.
Затем загрузится форма добавления нового пользователя. Заполним её и нажмем кнопку Create.
Если при заполнении формы были допущены ошибки, мы увидим аккуратное сообщение об ошибке.

Вернувшись назад к списку пользователей, мы должны увидеть только что созданного пользователя.
Повторите описанную операцию и добавьте ещё несколько пользователей. Обратите внимание, что при значительном
количестве пользователей для их отображения на одной странице список будет автоматически разбиваться на страницы.
Выполнив вход в качестве администратора (admin/admin),
можно увидеть страницу управления пользователями по адресу:

http://hostname/testdrive/index.php?r=user/admin

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

Всё это не требует написания ни одной строчки кода!

Страница управления пользователями

Страница добавления нового пользователя
Создание первого приложения
===========================

В этом разделе мы расскажем, как создать наше первое приложение.
Для создания нового приложения мы будем использовать yiic (консольную утилиту), для
генерации кода — Gii (мощный веб кодогенератор). Будем считать для удобства,
что YiiRoot — это директория, куда установлен Yii, а WebRoot — корневая
директория веб-сервера.

Запускаем yiic в консоли со следующими параметрами:
~~~
% YiiRoot/framework/yiic webapp WebRoot/testdrive
~~~

Note|Примечание: При использовании yiic на Mac OS,
Linux или Unix вам может понадобиться изменить права доступа
для файла yiic, чтобы сделать его исполняемым.
Альтернативный вариант запуска утилиты представлен ниже:

% cd WebRoot
% php YiiRoot/framework/yiic.php webapp testdrive

В результате в директории WebRoot/testdrive будет создан каркас приложения.

Созданное приложение — хорошая отправная точка для добавления необходимого функционала,
так как оно уже содержит все необходимые директории и файлы.
Не написав ни единой строчки кода, мы уже можем протестировать наше первое Yii-приложение, перейдя в браузере по следующему URL:

http://hostname/testdrive/index.php

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

Главная страница

Страница обратной связи

Страница обратной связи с ошибками ввода

Страница обратной связи с успешно отправленной формой

Страница авторизации

Наше приложение имеет следующую структуру директорий.
Подробное описание этой структуры можно найти в соглашениях.

testdrive/
    index.php                    скрипт инициализации приложения
    index-test.php               скрипт инициализации функциональных тестов
    assets/                      содержит файлы ресурсов
    css/                         содержит CSS-файлы
    images/                      содержит картинки
    themes/                      содержит темы оформления приложения
    protected/                   содержит защищённые файлы приложения
        yiic                     скрипт yiic
        yiic.bat                 скрипт yiic для Windows
        yiic.php                 PHP-скрипт yiic
        commands/                содержит команды 'yiic'
            shell/               содержит команды 'yiic shell'
        components/              содержит компоненты для повторного использования
            Controller.php       класс базового контроллера
            UserIdentity.php     класс 'UserIdentity' для аутентификации
        config/                  содержит конфигурационные файлы
            console.php          файл конфигурации консоли
            main.php             файл конфигурации веб-приложения
            test.php             файл конфигурации функциональных тестов
        controllers/             содержит файлы классов контроллеров
            SiteController.php   класс контроллера по умолчанию
        data/                    содержит пример базы данных
            schema.mysql.sql     схема БД для MySQL
            schema.sqlite.sql    схема БД для SQLite
            testdrive.db         файл БД для SQLite
        extensions/              содержит сторонние расширения
        messages/                содержит переведённые сообщения
        models/                  содержит файлы классов моделей
            LoginForm.php        модель формы для действия 'login'
            ContactForm.php      модель формы для действия 'contact'
        runtime/                 содержит временные файлы
        tests/                   содержит тесты
        views/                   содержит файлы представлений контроллеров и файлы макетов (layout)
            layouts/             содержит файлы представлений макетов
                main.php         общая для всех страниц разметка
                column1.php      разметка для страниц с одной колонкой
                column2.php      разметка для страниц с двумя колонками
            site/                содержит файлы представлений для контроллера 'site'
                pages/           статические страницы
                    about.php    страница «о проекте»
                contact.php      файл представления для действия 'contact'
                error.php        файл представления для действия 'error' (отображение ошибок)
                index.php        файл представления для действия 'index'
                login.php        файл представления для действия 'login'

Описанный выше генератор может создать файлы, необходимые при работе с
системой контроля версий Git. Приведённая далее команда создаст все необходимые
.gitignore (содержимое assets и runtime не должно оказаться в репозитории)
и .gitkeep (важные директории включаем в репозиторий даже если они пустые):

% YiiRoot/framework/yiic webapp WebRoot/testdrive git

Ещё одна поддерживаемая система контроля версий — Mercurial. Если вы пользуетесь
ей, передайте третьим параметром hg. Данная возможность доступна с версии 1.1.11.

6.4 Соединение с базой данных

Большинство веб-приложений используют базы данных, и наше приложение не исключение. Для использования базы данных
необходимо объяснить приложению, как к ней подключиться.
Это делается в конфигурационном файле WebRoot/testdrive/protected/config/main.php.
Например, так:

[php]
return array(
    …
    'components'=>array(
        …
        'db'=>array(
            'connectionString'=>'sqlite:protected/data/testdrive.db',
        ),
    ),
    …
);

В приведённом выше коде указано, что приложение должно подключиться к базе данных SQLite
WebRoot/testdrive/protected/data/testdrive.db как только это понадобится. Отметим, что
база данных SQLite уже включена в сгенерированное приложение. В этой базе имеется только
одна таблица tbl_user:

[sql]
CREATE TABLE tbl_user (
    id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    username VARCHAR(128) NOT NULL,
    password VARCHAR(128) NOT NULL,
    email VARCHAR(128) NOT NULL
);

Если вы хотите использовать базу данных MySQL, то вы можете воспользоваться файлом
WebRoot/testdrive/protected/data/schema.mysql.sql для её создания.

Note|Примечание: Для работы с базой данных Yii требуется расширение PHP PDO
и соответствующий драйвер PDO. Для тестового приложения необходимо подключить
расширения php_pdo и php_pdo_sqlite.

6.5 Реализация операций CRUD

А теперь самое интересное. Мы бы хотели добавить операции CRUD
(создание, чтение, обновление и удаление) для только что созданной таблицы tbl_user
это часто необходимо при разработке реальных приложений. Вместо ручного написания кода мы
воспользуемся веб кодогенератором Gii.

Info|Информация: Gii доступен, начиная с версии 1.1.2. Ранее для тех же целей
использовался уже упомянутый yiic. Подробнее yiic описан в разделе
«генерация CRUD при помощи yiic shell».

6.5.1 Настройка Gii

Для того чтобы использовать Gii, нужно отредактировать
файл конфигурации приложения
WebRoot/testdrive/protected/config/main.php:

[php]
return array(
    …
    'import'=>array(
        'application.models.*',
        'application.components.*',
    ),

    'modules'=>array(
        'gii'=>array(
            'class'=>'system.gii.GiiModule',
            'password'=>'задайте свой пароль',
        ),
    ),
);

После этого перейдите по URL http://hostname/testdrive/index.php?r=gii и
введите указанный в конфигурации пароль.

6.5.2 Генерация модели User

После входа зайдите в раздел Model Generator:

Model Generator

В поле Table Name введите tbl_user. В поле Model ClassUser.
Затем нажмите на кнопку Preview. Вы увидите новый файл, который будет
сгенерирован. После нажатия кнопки Generate в protected/models будет создан
файл User.php. Как будет описано далее в руководстве, класс модели User
позволяет работать с данными в таблице tbl_user в стиле ООП.

6.5.3 Генерация CRUD

После генерации класса модели мы сгенерируем код, реализующий для неё операции CRUD.
Выбираем Crud Generator:

CRUD Generator

В поле Model Class вводим User. В поле Controller IDuser (в нижнем регистре).
Теперь нажимаем Preview и затем Generate. Генерация кода CRUD завершена.

6.5.4 Доступ к страницам CRUD

Давайте порадуемся нашим трудам, перейдя по следующему URL:

http://hostname/testdrive/index.php?r=user

Мы увидим страницу со списком пользователей из таблицы tbl_user. Поскольку наша таблица пуста, то записей в ней не будет.
Кликнем по кнопке Create User и, если мы еще не авторизованы, отобразится страница авторизации.
В случае успешной авторизации загрузится форма добавления нового пользователя. Заполним её и нажмем кнопку Create.
Если при заполнении формы были допущены ошибки, мы увидим красивое сообщение об ошибке.

Вернувшись назад к списку пользователей, мы должны увидеть только что созданного пользователя.
Повторите описанную операцию и добавьте ещё несколько пользователей. Обратите внимание, что при значительном
количестве пользователей для их отображения на одной странице список будет автоматически разбиваться на страницы.
Выполнив вход в качестве администратора (admin/admin),
можно увидеть страницу управления пользователями по адресу:

http://hostname/testdrive/index.php?r=user/admin

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

Всё это не требует написания ни одной строчки кода!

Страница управления пользователями

Страница добавления нового пользователя
Установка
=========

Для установки Yii, как правило, необходимо выполнить два шага:

  1. Скачать Yii Framework с yiiframework.com.
  2. Распаковать релиз Yii в директорию, доступную из веб.

Tip|Подсказка: На самом деле, Yii содержит входной скрипт,
который обычно является единственным файлом со свободным доступом к нему из веб.
Другие PHP-скрипты, включая и файлы Yii, должны быть защищены от прямого доступа,
так как могут быть использованы для взлома.

6.6 Требования

После установки Yii проверьте, соответствует ли ваш веб-сервер необходимым
требованиям для использования Yii. Вы можете сделать это, воспользовавшись
специальным скриптом, доступным из веб-браузера по адресу:

http://hostname/path/to/yii/requirements/index.php

Для работы Yii требуется как минимум PHP 5.1. Yii тестировался на
Apache HTTP server под Windows и Linux, но может
работать также и на других веб-серверах и платформах с поддержкой PHP 5.1.
Что такое Yii
=============

Yii — это высокоэффективный, основанный на компонентной структуре PHP-фреймворк
для быстрой разработки крупных веб-приложений. Он позволяет максимально применить
концепцию повторного использования кода и может существенно ускорить процесс
веб-разработки. Название Yii (произносится как Yee или [ji:])
означает простой (easy), эффективный (efficient) и расширяемый (extensible).

6.7 Требования

Для запуска веб-приложений, построенных на Yii, вам понадобится веб-сервер с
поддержкой PHP версии 5.1.0.

Для разработчиков, желающих использовать Yii, крайне полезным будет понимание
концепции объектно-ориентированного программирования (OOП), так как Yii — это
строго объектно-ориентированный фреймворк.

6.8 Для чего Yii будет лучшим выбором?

Yii — это фреймворк для веб-программирования общего назначения, который может
быть использован для разработки практически любых веб-приложений. Благодаря
своей легковесности и наличию продвинутых средств кэширования, Yii особенно
подходит для разработки приложений с большим потоком трафика, таких как порталы,
форумы, системы управления контентом (CMS), системы электронной коммерции и др.

6.9 Yii в сравнении с другими фреймворками

Подобно большинству других PHP-фреймворков, Yii — это MVC-фреймворк.

Превосходство Yii над другими фреймворками заключается в эффективности, широких
возможностях и качественной документации. Yii изначально спроектирован очень
тщательно для соответствия всем требованиям при разработке серьёзных веб-приложений.
Yii не является ни побочным продуктом какого-либо проекта, ни сборкой сторонних
решений. Он является результатом большого опыта авторов в разработке веб-приложений,
а также их исследований наиболее популярных веб-фреймворков и приложений.
Определение фикстур
===================

Автоматические тесты необходимо выполнять неоднократно. Мы хотели бы выполнять
тесты в некоторых известных состояниях для гарантии повторяемости процесса
тестирования. Эти состояния называются фикстуры. Например, для тестирования
функции создания записи в приложении блога, каждый раз, когда мы выполняем
тесты, таблицы, хранящие соответствующие данные о записях (например, таблицы
Post, Comment), должны быть восстановлены к некоторому фиксированому
состоянию. Документация по PHPUnit
хорошо описывает основную установку фикстур. В основном в этом разделе мы
описываем установку фикстур базы данных так, как мы только что описали в
примере.

Установка фикстур базы данных является, наверное, одной из наиболее
длительных частей в тестировании основанных на БД веб-приложений. Yii
вводит компонент приложения [CDbFixtureManager] для облегчения этой проблемы.
В основном он делает следующие вещи при выполнении ряда тестов:

  • Перед выполнением всех тестов сбрасывает все таблицы, относящиеся к тестам к некоторому известному состоянию.
  • Перед выполнением отдельного тестового метода сбрасывает определенные таблицы к некоторому известному состоянию.
  • Во время выполнения тестового метода обеспечивает доступ к строкам данных, которые вносятся в фикстуру.

Для использования компонента [CDbFixtureManager], мы настраиваем его в
конфигурации приложения
следующим образом:

[php]
return array(
    'components'=>array(
        'fixture'=>array(
            'class'=>'system.test.CDbFixtureManager',
        ),
    ),
);

Далее мы сохраняем данные фикстуры в директории protected/tests/fixtures. Эта
директория может быть настроена свойством [CDbFixtureManager::basePath]
конфигурации приложения. Данные фикстур организованы как коллекция PHP-файлов,
называемых файлами фикстур. Каждый файл фикстуры возвращает массив,
представляющий начальные строки данных для конкретной таблицы. Имя файла -
такое же, как название таблицы. Далее приведен пример данных фикстуры для
таблицы Post, сохраненной в файле Post.php:

[php]
<?php
return array(
    'sample1'=>array(
        'title'=>'Тестовая запись 1',
        'content'=>'Содержимое тестовой записи 1',
        'createTime'=>1230952187,
        'authorId'=>1,
    ),
    'sample2'=>array(
        'title'=>'Тестовая запись 2',
        'content'=>'Содержимое тестовой записи 2',
        'createTime'=>1230952287,
        'authorId'=>1,
    ),
);

Как видим, в коде выше возвращаются 2 строки данных. Каждая строка представлена
в виде ассоциативного массива, ключи которого — это имена столбцов, а значения

  • значения соответствующих столбцов. Кроме того, каждая строка индексирована
    строкой (например sample1, sample2), которую называют псевдоним строки.
    Позже, когда мы пишем тестовые скрипты, мы можем легко обращаться к строке по
    ее псевдониму. Мы опишем это подробно в следующем разделе.

Вы могли заметить, что мы не определяем значения столбца id в коде фикстуры
выше. Это потому, что столбец id — автоинкрементный первичный ключ, значение
которого будет заполнено при вставке новых строк.

При первом обращении к компоненту [CDbFixtureManager] он будет просматривать
каждый файл фикстуры и использовать его для сброса соответствующей таблицы. Он
сбрасывает таблицу, очищая её, сбрасывая значение первичного ключа, и затем
вставляя строки данных из файла фикстуры в таблицу.

Иногда мы не хотим сбрасывать каждую таблицу, имеющую файл фикстуры, прежде,
чем мы выполним ряд тестов, потому что сброс слишком многих файлов фикстур
может занять длительное время. В этом случае, мы можем написать PHP-скрипт для
возможности настройки работы инициализации. PHP-скрипт должен быть сохранен в
файле init.php в той же директории, что и файлы фикстур. Когда компонент
[CDbFixtureManager] обнаружит этот скрипт, он выполнит этот скрипт вместо того,
чтобы сбрасывать каждую таблицу.

Также возможно, что нам не нравится способ сброса таблицы по умолчанию, то
есть, очистка таблицы полностью и вставка данных фикстуры. Если дело обстоит
так, мы можем написать скрипт инициализации для определенного файла фикстуры.
Скрипт должен иметь имя, в начале которого идет имя таблицы, а далее -
.init.php. Например, скрипт инициализации для таблицы Post назывался бы
Post.init.php. Когда компонент [CDbFixtureManager] увидит этот скрипт, он
выполнит скрипт вместо того, чтобы использовать значение сброса таблицы по
умолчанию.

Tip|Подсказка: Наличие большого количества файлов фикстур может сильно
увеличить время выполнения теста. Поэтому, Вы должны создавать файлы фикстур
только для тех таблиц, содержание которых может измениться во время теста.
Таблицы, которые служат для просмотра, не изменяются и, таким образом, не
нуждаются в файлах фикстур.

В следующих двух разделах мы опишем, как использовать фикстуры, которыми
управляет компонент [CDbFixtureManager], в модульных и функциональных тестах.
Функциональное тестирование
===========================

Перед прочтением даного раздела рекомендуется прочитать
документацию по Selenium и
документацию по PHPUnit. Дальше мы
подытожим основные принципы написания функциональных тестов в Yii:

  • Как и модульный тест, функциональный тест пишется в классе XyzTest,
    наследующего класс [CWebTestCase], где Xyz — имя класса, подлежащего
    тестированию. Мы можем использовать все методы класса
    PHPUnit_Extensions_SeleniumTestCase, потому что он является предком класса
    [CWebTestCase].

  • Класс функционального теста сохраняется в файле с именем XyzTest.php. По
    соглашению, файл функционального теста может храниться в директории
    protected/tests/functional.

  • Основное содержимое класса теста — набор тестовых методов с именами
    testAbc, где Abc — часто имя тестируемой особенности. Например, для
    тестирования особенности входа пользователя у нас есть метод testLogin.

  • Тестовый метод обычно содержит последовательность выражений, которые будут
    являться командами проверки для Selenium RC, показывающей ход и результаты
    тестирования веб-приложения. В нем также содержатся выражения утверждений для
    проверки, что веб-приложение отвечает именно так, как ожидалось.

Перед описанием, как же писать функциональный тест, давайте глянем файл
WebTestCase.php, сгенерированный командой yiic webapp. Этот файл определяет
класс WebTestCase, который может служить базовым для всех классов
функциональных тестов.

[php]
define('TEST_BASE_URL','http://localhost/yii/demos/blog/index-test.php/');

class WebTestCase extends CWebTestCase
{
    /**
     * Метод выполняется перед запуском теста.
     * В основном, устанавливает базовый URL тестируемого приложения.
     */
    protected function setUp()
    {
        parent::setUp();
        $this->setBrowserUrl(TEST_BASE_URL);
    }

    …
}

Класс WebTestCase в основном устанавливает базовый URL тестируемых страниц.
Далее, в тестовых методах, мы можем использовать относительные URL для
определения тестируемых страниц.

Мы также должны обратить внимание, что согласно базового тестового URL в
качестве входной точки используется файл index-test.php вместо файла
index.php. Единственное различие между сценариями index-test.php и
index.php то, что в качестве файла конфигурации приложения первый использует
файл test.php, а второй — файл main.php.

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

[php]
class PostTest extends WebTestCase
{
    public $fixtures=array(
        'posts'=>'Post',
    );

    public function testShow()
    {
        $this->open('post/1');
        // проверяем наличие заголовка некой записи
        $this->assertTextPresent($this->posts['sample1']['title']);
        // проверяем наличие формы комментария
        $this->assertTextPresent('Leave a Comment');
    }

    …
}

Как и при написании класса модульного теста, мы объявляем фикстуры для
использования этим тестом. Здесь мы показываем, что должна использоваться
фикстура Post. В тестирующем методе testShow мы сначала поручаем
Selenium RC открыть URL post/1. Заметим, что это относительный URL, а
полный URL формируется путем добавления относительного к базовому URL (т.е.
http://localhost/yii/demos/blog/index-test.php/post/1), который мы установили
в базовом классе. Затем мы проверяем, что можем найти заголовок записи
sample1 на данной странице. И мы также проверяем, что страница содержит текст
Leave a comment.

Tip|Подсказка: Перед запуском функциональных тестов запустите сервер Selenium-RC.
Сделать это можно командой java -jar selenium-server.jar, выполненной в
директории, в которую установлен Selenium.
Тестирование
============

Тестирование — важная составляющая процесса разработки ПО. Вне зависимости от того, осознаём
ли мы это или нет, мы проводим тестирование на протяжении всего процесса разработки приложения.
К примеру, при написании PHP-класса мы используем echo или die для того, чтобы проверить
корректность выполнения метода. При создании страницы, содержащей сложные HTML-формы, мы вводим
некоторые тестовые данные, чтобы проверить её работу. Более опытные разработчики напишут код,
автоматизирующий этот процесс и дающий возможность выполнить все тесты автоматически за один раз.
Этот процесс называется автоматизированное тестирование и является главной темой данного раздела.

В Yii поддерживается модульное тестирование и функциональное тестирование.

Модульный тест проверяет, что единица кода работает так, как должна. В ООП такой единицей является класс.
Поэтому модульный тест должен проверить, что каждый открытый метод класса работает должным образом.
То есть, имея входные тестовые данные, тест проверяет, что метод возвращает ожидаемый результат.
Модульные тесты обычно пишутся теми же, кто разрабатывает сам класс.

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

6.10 Разработка через тестирование

Ниже приведён цикл разработки через тестирование (TDD):

  1. Создаём новый тест, описывающий функциональность планируемой возможности. Тест должен при первом запуске
    завершиться неудачей, так как сама возможность ещё не реализована.
  2. Запускаем все тесты. Проверяем, что все они завершились неудачно.
  3. Пишем код, который проходит тесты.
  4. Запускаем все тесты. Проверяем, что все они завершились удачно.
  5. Рефакторим написанный код и проверяем, что он всё ещё проходит тесты.

Повторяем шаги 1 — 5 для новой функциональности.

6.11 Настройка тестового окружения

Тестирование в Yii требует установленного PHPUnit 3.5+ и
Selenium Remote Control 1.0+.
Как устанавливать PHPUnit и Selenium Remote Control вы можете прочитать в их
документации.

При использовании консольной команды yiic webapp для создания нового приложения
генерируются следующие директории, позволяющие писать и выполнять тесты:

testdrive/
   protected/                файлы приложения
      tests/                 тесты
         fixtures/           фикстуры БД
         functional/         функциональные тесты
         unit/               модульные тесты
         report/             отчёты по покрытию кода тестами
         bootstrap.php       загрузчик
         phpunit.xml         конфигурация для PHPUnit
         WebTestCase.php     базовый класс для функциональных тестов страниц

Как показано выше, код тестов главным образом находится в трёх директориях:
fixtures, functional и unit. Директория report используется для хранения
генерируемых отчётов о покрытии кода тестами.

Для того, чтобы запустить тесты (как модульные, так и функциональные), необходимо
выполнить следующие команды в консоли:

% cd testdrive/protected/tests
% phpunit functional/PostTest.php // запускает отдельный тест
% phpunit --verbose functional    // запускает все тесты в директории 'functional'
% phpunit --coverage-html ./report unit

Последняя команда выполнит все тесты в директории unit и создаст отчёт о покрытии кода в
директории report. Стоит отметить, что для отчётов требуется установленное
расширение xdebug.

6.12 Загрузчик тестов

Давайте посмотрим, что может находиться в файле bootstrap.php. Мы уделяем ему большое внимание,
так как он играет такую же роль, как входной скрипт и является
стартовой точкой при запуске набора тестов.

[php]
$yiit='path/to/yii/framework/yiit.php';
$config=dirname(__FILE__).'/../config/test.php';
require_once($yiit);
require_once(dirname(__FILE__).'/WebTestCase.php');
Yii::createWebApplication($config);

В приведённом коде мы сначала подключаем файл yiit.php, который инициализирует
некоторые глобальные константы и подключает необходимые базовые классы тестов.
Затем мы создаём экземпляр приложения, используя файл конфигурации test.php.
Конфигурация в нём наследуется от main.php и добавляет компонент
fixture (класс [CDbFixtureManager]). Фикстуры будут детально описаны в
следующем разделе.

[php]
return CMap::mergeArray(
    require(dirname(__FILE__).'/main.php'),
    array(
        'components'=>array(
            'fixture'=>array(
                'class'=>'system.test.CDbFixtureManager',
            ),
            /* раскомментируйте, если вам нужно подключение к тестовой БД
            'db'=>array(
                'connectionString'=>'DSN для БД',
            ),
            */
        ),
    )
);

При запуске тестов, включающих работу с базой данных, необходимо
переопределить БД для того, чтобы не испортить реальные данные или данные,
используемые при разработке. Для этого необходимо раскомментировать конфигурацию db
выше и указать DSN тестовой базы в свойстве connectionString.

При помощи такого входного скрипта при запуске тестов мы получаем экземпляр приложения,
максимально приближённый к реальному. Главное отличие в том, что в нём есть поддержка
фикстур и используется тестовая БД.
Модульное тестирование
======================

Поскольку тестировочная часть Yii построена на
PHPUnit, рекомендуется сначала изучить
документацию PHPUnit, чтобы
получить общее представление о том, как писать модульные тесты. Далее мы
приведём основные принципы написания модульных тестов в Yii:

  • Модульный тест — это класс XyzTest, наследующий класс [CTestCase] или
    [CDbTestCase], где Xyz — название тестируемого класса. Например, для
    тестирования класса Post по соглашению мы называем соответствующий класс
    модульного теста PostTest. Базовый класс [CTestCase] предназначен для общего
    модульного тестирования, а класс [CDbTestCase] — для тестирования классов
    моделей Active Record. Мы можем использовать все
    методы этих классов, унаследованные от класса PHPUnit_Framework_TestCase,
    поскольку он — предок обоих классов ([CTestCase] и [CDbTestCase]).

  • Класс модульного теста хранится в PHP-файле с именем XyzTest.php. По
    соглашению файл модульного теста может быть сохранен в директории
    protected/tests/unit.

  • Основное содержание тестового класса — набор тестовых методов с именами вида
    testAbc, где Abc — часто имя тестируемого метода класса.

  • Обычно тестовый метод содержит последовательность выражений утверждений
    (например, assertTrue, assertEquals), служащих контрольными точками при
    проверке поведения целевого класса.

Далее мы опишем, как писать модульные тесты для классов моделей
Active Record. Мы расширяем наши тестовые классы,
наследуя их от класса [CDbTestCase], поскольку он обеспечивает поддержку
фикстур базы данных, которые мы представили в предыдущем разделе.

Предположим, что мы хотим проверить класс модели Comment в
демо-блоге. Начнем с создания класса
CommentTest и сохраним его в файле protected/tests/unit/CommentTest.php:

[php]
class CommentTest extends CDbTestCase
{
    public $fixtures=array(
        'posts'=>'Post',
        'comments'=>'Comment',
    );

    …
}

В этом классе мы определяем переменную-член класса fixtures массивом,
содержащий список фикстур, используемых в данном тесте. Массив представляет
собой отображение имен фикстур на имена классов моделей или имена таблиц
фикстур (например, фикстуры с именем posts на класс модели Post). Заметим,
что при отображении на имя таблицы фикстуры мы должны использовать имя таблицы
с префиксом : (например, :Post), чтобы отличать его от имени класса модели.
А при использовании имен классов моделей, соответствующие таблицы будут
рассматриваться в качестве таблиц фикстур. Как описано выше, таблицы фикстур
будут сброшены в некоторое известное состояние каждый раз при выполнении
тестового метода.

Имя фикстуры позволяет нам получить удобный доступ к данным фикстуры в тестовых
методах. Следующий код показывает типичное использование:

[php]
// возвращает все строки таблицы фикстур `Comment`
$comments = $this->comments;
// возвращает строку с псевдонимом 'sample1' в таблице фикстур `Post`
$post = $this->posts['sample1'];
// возвращает экземпляр класса AR, представляющего строку данных фикстуры 'sample1'
$post = $this->posts('sample1');

Note|Примечание: Если фикстура объявлена с использованием имени её таблицы
(например, 'posts'=>':Post'), то третий пример в коде выше не является
допустимым, так как мы не имеем информации о том, какой класс модели
ассоциирован с таблицей.

Далее мы пишем метод testApprove для тестирования метода approve в классе
модели Comment. Код очень прямолинеен: сначала мы вставляем комментарий со
статусом ожидания, затем проверяем, комментарий имеет статус ожидания или
другой, извлекая его из базы данных, и, наконец, мы вызываем метод approve и
проверяем, изменился ли статус, как ожидалось.

[php]
public function testApprove()
{
    // вставить комментарий в лист ожидания
    $comment=new Comment;
    $comment->setAttributes(array(
        'content'=>'comment 1',
        'status'=>Comment::STATUS_PENDING,
        'createTime'=>time(),
        'author'=>'me',
        'email'=>'me@example.com',
        'postId'=>$this->posts['sample1']['id'],
    ),false);
    $this->assertTrue($comment->save(false));

    // проверить наличие комментария в листе ожидания
    $comment=Comment::model()->findByPk($comment->id);
    $this->assertTrue($comment instanceof Comment);
    $this->assertEquals(Comment::STATUS_PENDING,$comment->status);

    // вызвать метод approve() и проверить, что комментарий утвержден
    $comment->approve();
    $this->assertEquals(Comment::STATUS_APPROVED,$comment->status);
    $comment=Comment::model()->findByPk($comment->id);
    $this->assertEquals(Comment::STATUS_APPROVED,$comment->status);
}

Аутентификация и авторизация необходимы на страницах, доступных лишь некоторым
пользователям. Аутентификация — проверка, является ли некто тем, за кого
себя выдаёт. Обычно она подразумевает ввод логина и пароля, но также могут быть
использованы и другие средства, такие как использование смарт-карты, отпечатков
пальцев и др. Авторизация — проверка, может ли аутентифицированный пользователь
выполнять определённые действия (их часто обозначают как ресурсы). Чаще всего это
определяется проверкой, назначена ли пользователю определённая роль, имеющая доступ
к ресурсам.

В Yii встроен удобный фреймворк аутентификации и авторизации (auth), который, в
случае необходимости, может быть настроен под ваши задачи.

Центральным компонентом auth-фреймворка является предопределённый
компонент приложения «user» — объект, реализующий интерфейс [IWebUser].
Данный компонент содержит постоянную информацию о текущем пользователе. Мы можем
получить к ней доступ из любого места приложения, используя Yii::app()->user.

Используя этот компонент, мы можем проверить, аутентифицирован ли пользователь, используя
[CWebUser::isGuest]. Мы можем произвести [вход|CWebUser::login] или [выход|CWebUser::logout].
Для проверки прав на определённые действия удобно воспользоваться [CWebUser::checkAccess].
Также есть возможность получить [уникальный идентификатор|CWebUser::name] и другие
постоянные данные пользователя.

6.13 Определение класса Identity

Как было упомянуто ранее, аутентификация — это процесс проверки личности пользователя.
Типичное веб-приложение для такой проверки обычно использует логин и пароль. Тем не менее,
может потребоваться реализовать проверку другими методами. Чтобы добавить поддержку
различных методов аутентификации, в Yii имеется соответствующий identity класс.

Мы реализуем класс identity, который содержит нужную нам логику аутентификации.
Такой класс должен реализовать интерфейс [IUserIdentity]. Для различных подходов
к аутентификации могут быть реализованы различные классы (например, OpenID, LDAP,
Twitter OAuth или Facebook Connect). При создании своей реализации необходимо
расширить класс [CUserIdentity], являющийся базовым классом, который реализует
проверку по логину и паролю.

Главная задача при создании класса Identity — реализация метода [IUserIdentity::authenticate].
Данный метод используется для описания основного алгоритма аутентификации.
Также данный класс может содержать дополнительную информацию о пользователе, которая
необходима нам в процессе работы с его сессией.

6.13.0.1 Пример

В приведённом ниже примере мы используем класс identity и покажем, как реализовать
аутентификацию по базе данных. Данный подход типичен почти для всех приложений.
Пользователь будет вводить логин и пароль в форму. Введённые данные будем проверять
с использованием модели ActiveRecord, соответствующей
таблице пользователей в БД. В данном примере показано следующее:

  1. Реализация метода authenticate() для проверки данных по БД.
  2. Перекрытие метода CUserIdentity::getId() для возврата _id. По умолчанию
    в качестве ID возвращается имя пользователя.
  3. Использование метода setState() ([CBaseUserIdentity::setState]) для хранения
    информации, необходимой при каждом запросе.
[php]
class UserIdentity extends CUserIdentity
{
    private $_id;
    public function authenticate()
    {
        $record=User::model()->findByAttributes(array('username'=>$this->username));
        if($record===null)
            $this->errorCode=self::ERROR_USERNAME_INVALID;
        else if(!CPasswordHelper::verifyPassword($this->password,$record->password))
            $this->errorCode=self::ERROR_PASSWORD_INVALID;
        else
        {
            $this->_id=$record->id;
            $this->setState('title', $record->title);
            $this->errorCode=self::ERROR_NONE;
        }
        return !$this->errorCode;
    }

    public function getId()
    {
        return $this->_id;
    }
}

В следующем подразделе мы рассмотрим реализацию входа и выхода, используя наш identity класс
в методе login пользователя. Вся информация, которую мы храним в состояниях
(путём вызова [CBaseUserIdentity::setState]) будет передана в [CWebUser], который,
в свою очередь, будет хранить её в постоянном хранилище, таком как сессии.
К данной информации можно будет обращаться как к свойствам [CWebUser]. В нашем примере
мы сохранили имя пользователя, используя $this->setState('title', $record->title);.
Как только пользователь успешно войдёт в приложение, мы сможем получить
его title используя Yii::app()->user->title.

Info|Инфо: По умолчанию [CWebUser] использует сессии для хранения данных.
Если вы используете автоматический вход пользователя с помощью cookie
([CWebUser::allowAutoLogin] выставлен в true), данные пользователя будут
также сохраняться в cookie. Убедитесь, что эти данные не содержат конфиденциальной
информации, такой как пароли.

6.13.1 Хранение паролей в базе данных

Безопасное хранение паролей в базе данных требует определённой аккуратности. Атакующий, получивший досутп к базе или
резеврным копиям может восстановить пароли используя достаточно распространённые приёмы, если от них не защититься.
Пример кода выше использует встроенный класс [CPasswordHelper], доступный с версии 1.1.14, для хеширования и проверки
пароля. [CPasswordHelper::hashPassword] возвращает стойкий ко взлому хеш.

6.14 Вход и выход

Теперь, когда мы разобрали пример реализации класса identity, мы можем
использовать его для реализации входа и выхода:

[php]
// Аутентифицируем пользователя по имени и паролю
$identity=new UserIdentity($username,$password);
if($identity->authenticate())
    Yii::app()->user->login($identity);
else
    echo $identity->errorMessage;
…
// Выходим
Yii::app()->user->logout();

Мы создаём новый объект UserIdentity и передаём в его конструктор параметры аутентификации
(то есть $username и $password, введённые пользователем). Далее просто вызываем
метод authenticate(). В случае успешной проверки данных мы передаём объект в
метод [CWebUser::login], который сохраняет информацию в постоянном хранилище
(по умолчанию в сессиях PHP) и делает её доступной в последующих запросах.
Если аутентификация не проходит, мы можем получить информацию об ошибке из свойства errorMessage.

Проверить, является ли пользователь аутентифицированным, очень просто. Для этого можно
воспользоваться Yii::app()->user->isGuest. При использовании постоянного
хранилища, такого как сессии (по умолчанию) и/или cookie (описано ниже), для хранения
информации о пользователе, пользователь может оставаться аутентифицированным в
последующих запросах. В этом случае нет необходимости использовать класс UserIdentity
и показывать форму входа. [CWebUser] автоматически загрузит необходимую информацию
из постоянного хранилища и использует её при обращении к Yii::app()->user->isGuest.

По умолчанию, после некоторого времени бездействия, зависящего от
настроек сессии,
будет произведён выход из системы. Для того, чтобы этого не происходило,
необходимо выставить свойства компонента User [allowAutoLogin|CWebUser::allowAutoLogin]
в true и передать необходимое время жизни cookie в метод [CWebUser::login].
Пользователь будет автоматически аутентифицирован на сайте в течение указанного
времени даже в том случае, если он закроет браузер. Данная возможность требует
поддержки cookie в браузере пользователя.

[php]
// Автоматический вход в течение 7 дней.
// allowAutoLogin для компонента user должен быть выставлен в true.
Yii::app()->user->login($identity,3600*24*7);

Как уже упоминалось выше, когда включен вход на основе cookie, состояния,
сохраняемые при помощи [CBaseUserIdentity::setState], также будут сохраняться в cookie.
При следующем входе состояния считываются из cookie и становятся доступными через Yii::app()->user.

Несмотря на то, что в Yii имеются средства для предотвращения подмены состояний в cookie
на стороне клиента, не рекомендуется хранить в состояниях важную информацию.
Гораздо более правильным решением будет хранение её в постоянном хранилище на стороне
сервера (например, в БД).

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

  • При успешном входе после заполнения формы генерируем и храним случайный ключ
    как в cookie состояния, так и в постоянном хранилище на сервере (т.е. в БД).

  • При последующих запросах, когда аутентификация производится на основе
    информации в cookie, мы сравниваем две копии ключа и, перед тем, как
    аутентифицировать пользователя, проверяем, что они равны.

  • Если пользователь входит через форму ещё раз, ключ регенерируется.

Данная стратегия исключает возможность повторного использования старого
состояния cookie, в котором может находится устаревшая информация.

Для реализации нужно переопределить два метода:

  • [CUserIdentity::authenticate()]. Здесь производится аутентификация.
    Если пользователь аутентифицирован, необходимо сгенерировать новый ключ и сохранить его
    в cookie состояния (при помощи [CBaseUserIdentity::setState]) и в постояное хранилище
    на стороне сервера (например, в БД).

  • [CWebUser::beforeLogin()]. Вызывается перед входом. Необходимо проверить
    соответствие ключей в состоянии и базе данных.

6.16 Фильтр контроля доступа

Фильтр контроля доступа — схема авторизации, подразумевающая предварительную проверку
прав текущего пользователя на вызываемое действие контроллера. Авторизация
производится по имени пользователя, IP-адресу и типу запроса. Данный фильтр называется
«[accessControl|CController::filterAccessControl]».

Tip|Подсказка: Фильтр контроля доступа достаточен для реализации простых систем.
Для более сложных вы можете использовать доступ на основе ролей
(RBAC), который будет описан ниже.

Для управления доступом к действиям контроллера необходимо переопределить метод
[CController::filters] (более подробно описано в разделе
Фильтры).

[php]
class PostController extends CController
{
    …
    public function filters()
    {
        return array(
            'accessControl',
        );
    }
}

Выше было описано, что фильтр [access
control|CController::filterAccessControl] применяется ко всем действиям
контроллера PostController. Правила доступа, используемые фильтром, определяются
переопределением метода [CController::accessRules] контроллера.

[php]
class PostController extends CController
{
    …
    public function accessRules()
    {
        return array(
            array('deny',
                'actions'=>array('create', 'edit'),
                'users'=>array('?'),
            ),
            array('allow',
                'actions'=>array('delete'),
                'roles'=>array('admin'),
            ),
            array('deny',
                'actions'=>array('delete'),
                'users'=>array('*'),
            ),
        );
    }
}

Приведённый код описывает три правила, каждое из которых представлено в виде массива.
Первый элемент массива может принимать значения 'allow' или 'deny'. Остальные
пары ключ-значение задают параметры правила. Правила, заданные выше, можно
прочитать следующим образом: действия create и edit не могут быть выполнены
анонимными пользователями, а действие delete может быть выполнено только
пользователями с ролью admin.

Правила доступа разбираются поочерёдно в порядке их описания. Первое правило,
совпадающее с текущими данными (например, с именем пользователя, ролью или
IP) определяет результат авторизации. Если это разрешающее правило, действие
может быть выполнено, если запрещающее — не может. Если ни одно из правил не
совпало — действие может быть выполнено.

Tip|Подсказка: Чтобы быть уверенным, что действие не будет выполнено,
необходимо запретить все действия, которые не разрешены, определив соответствующее
правило в конце списка:
~~~
[php]
return array(
// … разные правила …
// это правило полностью запрещает действие ‘delete’
array(‘deny’,
‘actions’=>array(‘delete’),
),
);
~~~
Данное правило необходимо, так как если ни одно из правил не совпадёт,
действие продолжит выполнение.

Правило доступа может включать параметры, по которым проверяется совпадение:

  • их идентификаторов. Сравнение регистронезависимо;

  • массива их идентификаторов. Сравнение регистронезависимо.

  • используется [CWebUser::name]. Сравнение регистронезависимо. В параметре могут быть
    использованы следующие специальные символы:

    • *: любой пользователь, включая анонимного.
    • ?: анонимный пользователь.
    • @: аутентифицированный пользователь.
  • доступ на основе ролей, описанный в следующем
    разделе. В частном случае, правило применится, если [CWebUser::checkAccess]
    вернёт true для одной из ролей. Роли стоит использовать в разрешающих
    правилах так как роль ассоциируется с возможностью выполнения какого-либо действия.
    Также стоит отметить, что, несмотря на то, что мы используем термин «роль»,
    значением может быть любой элемент auth-фреймворка, такой как роли,
    задачи или операции;

  • [verbs|CAccessRule::verbs]: позволяет указать тип запросов (например,
    GET или POST). Сравнение регистронезависимо;

  • которого будет определять совпадение правила. Внутри выражения доступна переменная
    $user, указывающая на Yii::app()->user.

6.17 Обработка запроса авторизации

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

  • Если пользователь не аутентифицирован и в свойстве [loginUrl|CWebUser::loginUrl]
    компонента user задан URL страницы входа, браузер будет перенаправлен на эту страницу.
    Заметим, что по умолчанию [loginUrl|CWebUser::loginUrl] перенаправляет к странице site/login;

  • Иначе будет отображена ошибка HTTP с кодом 403.

При задании свойства [loginUrl|CWebUser::loginUrl] используется как относительный,
так и абсолютный URL. Также можно передать массив, который будет использоваться
[CWebApplication::createUrl] при формировании URL. Первый элемент массива
задаёт маршрут до действия login вашего
контроллера, а остальные пары имя-значение — GET-параметры. К примеру,

[php]
array(
    …
    'components'=>array(
        'user'=>array(
            // это значение устанавливается по умолчанию
            'loginUrl'=>array('site/login'),
        ),
    ),
)

Если браузер был перенаправлен на страницу входа и вход удачный, вам может понадобиться
перенаправить пользователя к той странице, на которой неудачно прошла авторизация.
Как же узнать URL той страницы? Мы можем получить эту информацию из свойства
[returnUrl|CWebUser::returnUrl] компонента user. Имея её, мы можем сделать
перенаправление:

[php]
Yii::app()->request->redirect(Yii::app()->user->returnUrl);

6.18 Контроль доступа на основе ролей

Контроль доступа на основе ролей (RBAC) — простой, но мощный способ централизованного
контроля доступа. Для сравнения данного метода с другими обратитесь к
статье в Википедии.

В Yii иерархический RBAC реализован через компонент [authManager|CWebApplication::authManager].
Ниже мы сначала опишем основы данной схемы, затем то, как описывать данные,
необходимые для авторизации. В завершение мы покажем, как использовать эти данные для
контроля доступа.

6.18.1 Общие принципы

Основным понятием в RBAC Yii является элемент авторизации. Элемент авторизации —
это права на выполнение какого-либо действия (создать новую запись в
блоге, управление пользователями). В зависимости от структуры и цели,
элементы авторизации могут быть разделены на операции,
задачи и роли. Роль состоит из задач. Задача состоит из операций.
Операция — разрешение на какое-либо действие (дальше не делится).
К примеру, в системе может быть роль администратор, состоящая из задач
управление записями и управление пользователями. Задача управление пользователями
может состоять из операций создать пользователя, редактировать пользователя и
удалить пользователя. Для достижения большей гибкости, роль в Yii может состоять
из других ролей и операций. Задача может состоять из других задач. Операция — из
других операций.

Элемент авторизации однозначно идентифицируется его уникальным именем.

Элемент авторизации может быть ассоциирован с бизнес-правилом — PHP-кодом,
который будет использоваться при проверке доступа. Пользователь получит доступ
к элементу только если код вернёт true. К примеру, при определении операции updatePost,
будет не лишним добавить бизнес-правило, проверяющее соответствие ID пользователя
ID автора записи. То есть, доступ к редактированию записи имеет только её автор.

Используя элементы авторизации мы можем построить иерархию авторизации.
Элемент A является родителем элемента B в иерархии, если
A состоит из B (или A наследует права, представленные в B).
Элемент может иметь несколько потомков и несколько предков.
Поэтому иерархия авторизации является скорее частично упорядоченным графом, чем
деревом. В ней роли находятся на верхних уровнях, а операции — на нижних. Посередине
расположены задачи.

После построения иерархии авторизации мы можем назначать роли из неё пользователям
нашего приложения. Пользователь получает все права роли, которая ему назначена.
К примеру, если назначить пользователю роль администратор, он получит административные
полномочия, такие как управление записями или управление пользователями
(и соответствующие им операции, такие как создать пользователя).

А теперь самое приятное. В действии контроллера мы хотим проверить, может
ли текущий пользователь удалить определённую запись. При использовании иерархии RBAC и
назначенной пользователю роли, это делается очень просто:

[php]
if(Yii::app()->user->checkAccess('deletePost'))
{
    // удаляем запись
}

6.19 Настройка менеджера авторизации

Перед тем, как мы перейдём к построению иерархии авторизации и непосредственно
проверке доступа, нам потребуется настроить компонент приложения
[authManager|CWebApplication::authManager]. В Yii есть два типа менеджеров
авторизации: [CPhpAuthManager] и [CDbAuthManager]. Первый использует для
хранения данных PHP, второй — базу данных. При настройке
[authManager|CWebApplication::authManager] необходимо указать, который из
компонентов мы собираемся использовать и указать начальные значения свойств
компонента. К примеру,

[php]
return array(
    'components'=>array(
        'db'=>array(
            'class'=>'CDbConnection',
            'connectionString'=>'sqlite:path/to/file.db',
        ),
        'authManager'=>array(
            'class'=>'CDbAuthManager',
            'connectionID'=>'db',
        ),
    ),
);

После этого мы можем обращаться к компоненту [authManager|CWebApplication::authManager]
используя Yii::app()->authManager.

6.20 Построение иерархии авторизации

Построение иерархии авторизации состоит из трёх этапов: задания элементов
авторизации, описания связей между ними и назначение ролей пользователям.
Компонент [authManager|CWebApplication::authManager] предоставляет полный набор
API для выполнения поставленных задач.

Для определения элемента авторизации следует воспользоваться одним из приведённых
ниже методов:

  • [CAuthManager::createRole]
  • [CAuthManager::createTask]
  • [CAuthManager::createOperation]

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

  • [CAuthManager::addItemChild]
  • [CAuthManager::removeItemChild]
  • [CAuthItem::addChild]
  • [CAuthItem::removeChild]

После этого мы назначаем роли пользователям:

  • [CAuthManager::assign]
  • [CAuthManager::revoke]

Приведём пример построения иерархии авторизации с использованием данного API:

[php]
$auth=Yii::app()->authManager;

$auth->createOperation('createPost','создание записи');
$auth->createOperation('readPost','просмотр записи');
$auth->createOperation('updatePost','редактирование записи');
$auth->createOperation('deletePost','удаление записи');

$bizRule='return Yii::app()->user->id==$params["post"]->authID;';
$task=$auth->createTask('updateOwnPost','редактирование своей записи',$bizRule);
$task->addChild('updatePost');

$role=$auth->createRole('reader');
$role->addChild('readPost');

$role=$auth->createRole('author');
$role->addChild('reader');
$role->addChild('createPost');
$role->addChild('updateOwnPost');

$role=$auth->createRole('editor');
$role->addChild('reader');
$role->addChild('updatePost');

$role=$auth->createRole('admin');
$role->addChild('editor');
$role->addChild('author');
$role->addChild('deletePost');

$auth->assign('reader','readerA');
$auth->assign('author','authorB');
$auth->assign('editor','editorC');
$auth->assign('admin','adminD');

После создания элементов авторизации, компонент [authManager|CWebApplication::authManager]
(или его наследники, например, [CPhpAuthManager], [CDbAuthManager]) загружает их автоматически.
То есть, приведённый код запускается один раз, а НЕ для каждого запроса.

Info|Инфо: Довольно громоздкий пример выше предназначен скорее для демонстрации.
Разработчикам обычно требуется создать интерфейс администратора и дать возможность
пользователям самим построить иерархию авторизации.

6.21 Использование бизнес-правил

При построении иерархии авторизации мы можем назначить роль, задачу или операцию
бизнес-правилу. Также мы можем указать его при назначении роли пользователю.
Бизнес-правило — PHP-код, использующийся при проверке доступа. Возвращаемое данным кодом
значение определяет, применять ли данную роль к текущему пользователю.
В примере выше мы применили бизнес-правило для описания задачи updateOwnPost.
В нём мы проверяем, совпадает ли ID текущего пользователя с ID автора записи.
Информация о записи в массиве $params передаётся разработчиком при проверке
доступа.

6.21.1 Проверка доступа

Для проверки доступа нам необходимо знать имя элемента авторизации.
К примеру, чтобы проверить, может ли текущий пользователь создать запись,
необходимо узнать, имеет ли он права, описанные операцией createPost.
После этого мы можем вызвать [CWebUser::checkAccess]:

[php]
if(Yii::app()->user->checkAccess('createPost'))
{
    // создаём запись
}

Если правило авторизации использует бизнес-правило, требующее дополнительных
параметров, необходимо их передать. К примеру, чтобы проверить, может ли
пользователь редактировать запись, мы передаём данные о записи в $params:

[php]
$params=array('post'=>$post);
if(Yii::app()->user->checkAccess('updateOwnPost',$params))
{
    // обновляем запись
}

6.21.2 Использование ролей по умолчанию

Некоторым веб-приложениям требуются очень специфичные роли, которые назначаются
каждому или почти каждому пользователю. К примеру, нам необходимо наделить некоторыми
правами всех аутентифицированных пользователей. Определять явно и хранить роли для
каждого пользователя в этом случае явно неудобно. Для решения этой проблемы можно
использовать роли по умолчанию.

Роль по умолчанию автоматически назначается каждому пользователю.
При вызове [CWebUser::checkAccess] сначала проверяются роли по умолчанию. Назначать
их явно не требуется.

Роли по умолчанию описываются в свойстве [CAuthManager::defaultRoles].
К примеру, приведённая ниже конфигурация описывает две роли по умолчанию:
authenticated и admin.

[php]
return array(
    'components'=>array(
        'authManager'=>array(
            'class'=>'CDbAuthManager',
            'defaultRoles'=>array('authenticated', 'admin'),
        ),
    ),
);

Так как роль по умолчанию назначается каждому пользователю, обычно требуется
использовать бизнес-правило, определяющее, к каким именно пользователям её применять.
К примеру, следующий код определяет две роли: authenticated и admin,
которые соответственно применяются к аутентифицированным пользователям и пользователям
с именем admin.

[php]
$bizRule='return Yii::app()->user->name === "admin";';
$auth->createRole('admin', 'администратор', $bizRule);

$bizRule='return Yii::app()->user->isGuest;';
$auth->createRole('guest', 'гость', $bizRule);

Info|Информация: Начиная с версии 1.1.11 массив $params, передаваемый в
бизнес-правило, всегда содержит ключ userId с id пользователя, для которого проверяется
правило. Это особенно удобно при использовании [CDbAuthManager::checkAccess()]
или [CPhpAuthManager::checkAccess()] когда Yii::app()->user не
является пользователем, которого вы проверяете.
Консольные приложения
=====================

Консольные приложения главным образом используются для выполнения вторичных или фоновых задач,
таких как генерация кода, компиляция поискового индекса, отправка сообщений электронной почты и т.д.
Yii предоставляет инструмент для разработки консольных приложений, придерживаясь
объектно-ориентированного подхода. Он позволяет консольному приложению получить доступ
к ресурсам, которые использует основное веб-приложение (например, к базе данных).

6.22 Обзор

Каждая консольная задача представлена в Yii как [команда|CConsoleCommand].
Консольная команда описывается в классе, наследуемом от [CConsoleCommand].

После использования yiic webapp для создания начального прототипа приложения,
в protected будут два файла:

  • yiic — скрипт для Linux/Unix;
  • yiic.bat — скрипт для Windows.

В консоли можно ввести следующие команды:

cd protected
yiic help

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

Для получения справки по команде можно запустить

yiic help <имя-команды>

Для запуска команды используется следующий формат:

yiic <имя-команды> [параметры…]

6.23 Создание команд

Консольные команды находятся в файлах с классами в папке, указанной в
[CConsoleApplication::commandPath]. По умолчанию это protected/commands.

Класс консольной команды должен быть унаследован от [CConsoleCommand]. Имя класса
должно быть вида XyzCommand, где Xyz соответствует имени команды, первая буква
которого приведена к верхнему регистру. К примеру, команда sitemap должна использовать
класс SitemapCommand. Имена консольных команд регистрозависимы.

Tip|Подсказка: Конфигурируя [CConsoleApplication::commandMap], можно при желании изменить порядок
именования и расположения классов команд.

Для создания новой команды необходимо либо реализовать метод [CConsoleCommand::run()],
либо одно или несколько действий (будут описаны далее).

При вводе консольной команды приложение запускает метод [CConsoleCommand::run()].
Параметры метода передаются в соответствии с следующим заголовком:

[php]
public function run($args) { ... }

где $args — дополнительные параметры, переданные из командной строки.

Внутри консольной команды для получения доступа к экземпляру консольного приложения
можно использовать Yii::app(). Через полученный экземпляр можно обращаться
к различным компонентам, таким как соединение с базой данных (Yii::app()->db).
Насколько можно судить, это очень похоже на обычное веб-приложение.

Info|Информация: Начиная с версии 1.1.1 можно создавать глобальные команды,
которые используются всеми приложениями. Для этого определяется
переменная окружения YII_CONSOLE_COMMANDS и в её значение записывается
путь к директории с классами глобальных консольных команд.

6.24 Действие консольной команды

Note|Примечание: Данная возможность доступна начиная с версии 1.1.5.

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

Действие консольной команды — метод в её классе. Имя метода должно быть
вида actionXyz, где Xyz соответствует имени действия и первой буквой, приведённой
к верхнему регистру. К примеру, метод actionIndex задаёт действие
с именем index.

Для того, чтобы запустить определённое действие, используется следующий формат команды:

yiic <имя-команды> <имя-действия> --параметр1=значение1 --параметр2=значение2 ...

Дополнительные пары имя-значение передаются методу действия как именованные параметры.
Значение опции xyz соответствует параметру $xyz метода действия.
К примеру, если мы определим следующий класс команды:

[php]
class SitemapCommand extends CConsoleCommand
{
    public function actionIndex($type, $limit=5) { ... }
    public function actionInit() { ... }
}

То все следующие консольные команды вызовут actionIndex('News', 5):

yiic sitemap index --type=News --limit=5

// $limit принимает значение по умолчанию
yiic sitemap index --type=News

// $limit принимает значение по умолчанию.
// Так как 'index' — действие по умолчанию, мы можем опустить имя действия.
yiic sitemap --type=News

// порядок опций не важен
yiic sitemap index --limit=5 --type=News

Если значение опции не указано (то есть --type вместо --type=News), соответствующему
параметру действия будет присвоено значение true.

Note|Примечание: Альтернативные форматы указания опций, такие как
--type News или -t News не поддерживаются.

Если объявить параметр как массив, он сможет принять массив значений:

[php]
public function actionIndex(array $types) { ... }

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

yiic sitemap index --types=News --types=Article

Команда, приведённая выше, запустит actionIndex(array('News', 'Article')).

Начиная с версии 1.1.6, Yii позволяет использовать анонимные параметры действий и
глобальные опции.

Анонимные параметры — это параметры командной строки, не являющиеся опциями.
К примеру, в команде yiic sitemap index --limit=5 News встречается
анонимный параметр со значением News. Именованный параметр (опция) limit
принимает значение, равное 5.

Для того, чтобы использовать анонимные параметры, действие должно описать параметр
с именем $args:

[php]
public function actionIndex($limit=10, $args=array()) {...}

В массиве $args будут содержаться все доступные значения анонимных параметров.

Глобальные опции — это параметры командной строки, общие для всех действий команды.
К примеру, нам может понадобиться для команды с несколькими действиями завести общую
опцию verbose. Конечно, можно определить параметр $verbose для каждого действия, но
лучше задать его public свойством класса команды, что автоматически сделает
verbose глобальной опцией:

[php]
class SitemapCommand extends CConsoleCommand
{
    public $verbose=false;
    public function actionIndex($type) {...}
}

Приведённый код позволяет использовать опцию verbose:

yiic sitemap index --verbose=1 --type=News

6.25 Код возврата

Note|Примечание: возможность указать код возврата в консольной команде
появилась в версии 1.1.11.

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

Данные коды являются целочисленными значениями от 0 до 254 (данный интервал
задан в PHP),
где 0 возвращается в случае успеха, а все остальные значения используются для
различных ошибок.

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

Пример:

[php]
if (/* ошибка */) {
    return 1; // выходим с кодом 1
}
// … всякая всячина …
return 0; // всё хорошо, выходим с кодом 0

Если консольная команда ничего не возвращает, приложение завершается с кодом 0.

6.26 Настройка консольного приложения

По умолчанию, если приложение создаётся с использованием yiic webapp, конфигурация
консольного приложения находится в protected/config/console.php. Как и конфигурация
веб-приложения, данный файл является PHP-скриптом, возвращающим массив с начальными
значениями экземпляра консольного приложения. То есть в данном файле можно задать
любое public свойство [CConsoleApplication].

Так как консольные команды часто создаются для поддержки веб-приложения, требуется
использовать те же ресурсы (такие, как соединения с БД), что используются в веб-приложении.
Это можно сделать настроив соответствующие компоненты в конфигурации консольного
приложения:

[php]
return array(
    ......
    'components'=>array(
        'db'=>array(
            ......
        ),
    ),
);

Формат конфигурации очень похож на тот, что используется в веб-приложении, так как
и [CConsoleApplication] и [CWebApplication] наследуют один и тот же базовый класс.
Обработка ошибок
================

Yii предоставляет полноценный функционал обработки ошибок на базе механизма
обработки ошибок в РНР 5. В момент поступления пользовательского запроса
создается экземпляр приложения, который регистрирует метод [handleError|CApplication::handleError] для
обработки предупреждений и уведомлений, а также метод [handleException|CApplication::handleException]
для обработки не пойманных исключений. Таким образом, если в процессе выполнения приложения возникают
предупреждения, уведомления РНР или непойманные исключения, один из обработчиков ошибок получит управление
и запустит необходимую процедуру обработки ошибок.

Tip|Подсказка: Регистрация обработчиков ошибок осуществляется в конструкторе приложения путем вызова функций РНР
set_exception_handler и
set_error_handler.
Если вы не хотите, чтобы Yii обрабатывал ошибки и исключения, во входном скрипте
установите значение false константам YII_ENABLE_ERROR_HANDLER и YII_ENABLE_EXCEPTION_HANDLER.

По умолчанию, метод [handleError|CApplication::handleError] (или [handleException|CApplication::handleException])
вызывает событие [onError|CApplication::onError] (или [onException|CApplication::onException]). Если ошибка (или исключение)
не обрабатывается обработчиком события, он обращается за помощью к компоненту приложения [errorHandler|CErrorHandler].

6.27 Вызов исключений

Вызов исключений в Yii ничем не отличается от вызова обычного исключения РНР.
В случае необходимости, вызов исключения осуществляется следующим образом:

[php]
throw new ExceptionClass('ExceptionMessage');

Yii определяет три класса для исключений: [CException], [CDbException] и
[CHttpException]. [CException] — типовой класс исключения. [CDbException]
представляет исключения, вызываемые некоторыми операциями базы данных.
[CHttpException] отвечает за исключения, которые отображаются конечному
пользователю, и содержит свойство [statusCode|CHttpException::statusCode],
соответствующее коду состояния НТТР. Класс исключения определяет также, каким
образом отображается ошибка. Об этом будет рассказано ниже.

Tip|Подсказка: Вызов исключения [CHttpException] — это простой способ сообщить об ошибках,
вызванных неверными действиями пользователя. Например, если пользователь указывает в адресе URL
неверный идентификатор записи, для отображения ошибки 404 (страница не найдена) мы можем
выполнить следующее действие:

[php]
// если идентификатора записи не существует
throw new CHttpException(404,'Указанная запись не найдена');

6.28 Отображение ошибок

В момент, когда компонент приложения [CErrorHandler] получает ошибку, выбирается
соответствующее представление для её отображения. Если предполагается, что сообщение об ошибке
должно отображаться конечным пользователям, например [CHttpException], то используется
представление с именем errorXXX, где XXX соответствует коду состояния НТТР (400, 404, 500 и т.д.).
Если же это внутренняя ошибка и отображаться она должна только разработчикам, используется представление
с именем exception. В последнем случае будет отображен весь стек вызовов, а также указание на строку возникновения ошибки.

Info|Инфо: Если приложение запускается в производственном режиме,
все ошибки, включая внутренние, отображаются с использованием представления errorXXX. Это сделано из
соображений безопасности, поскольку стек вызова может содержать важную информацию. В этом случае
для выявления причин возникновения ошибки необходимо использовать протокол ошибок.

[CErrorHandler] осуществляет поиск файла, соответствующего представлению, в следующем порядке:

  1. WebRoot/themes/ThemeName/views/system: папка системных представлений текущей темы оформления;

  2. WebRoot/protected/views/system: папка системных представлений приложения, используемая по умолчанию;

  3. yii/framework/views: папка стандартных системных представлений, предоставляемых фреймворком.

Следовательно, если нам необходимо изменить внешний вид сообщений, мы можем просто создать
файлы представлений ошибок в папке системных представлений приложения или темы. Каждый файл представления —
это обычный РНР-скрипт, состоящий преимущественно из HTML-кода. Подробнее с этим можно разобраться, просто изучив
используемые по умолчанию файлы, расположенные в папке фреймворка с именем view.

6.29 Управление отображением ошибок в действии контроллера

Yii позволяет использовать действие контроллера
для отображения ошибок. Для этого необходимо задать обработчик ошибок в настройках
приложения:

[php]
return array(
    …
    'components'=>array(
        'errorHandler'=>array(
            'errorAction'=>'site/error',
        ),
    ),
);

Выше мы задали маршрут site/error, ведущий к действию error контроллера
SiteController, свойству [CErrorHandler::errorAction]. Если необходимо,
можно использовать другой маршрут.

Код действия error должен выглядеть примерно так:

[php]
public function actionError()
{
    if($error=Yii::app()->errorHandler->error)
        $this->render('error', $error);
}

Сначала мы получаем подробную информацию об ошибке из [CErrorHandler::error]. Если
она не пуста — отображаем её в представлении error. Информация, получаемая из
[CErrorHandler::error] является массивом, содержащим следующие данные:

  • code: код ответа HTTP (например, 403 или 500);
  • type: тип ответа (например, [CHttpException] или PHP Error);
  • message: текст сообщения;
  • file: имя PHP-скрипта, в котором возникла ошибка;
  • line: номер строки, на которой возникла ошибка;
  • trace: стэк вызовов ошибки;
  • source: часть кода, где возникла ошибка.

Tip|Подсказка: Проверка [CErrorHandler::error] на пустое значение делается, т.к.
действие error может быть вызвано пользователем напрямую. Так как мы передаём
массив $error представлению, он будет автоматически развёрнут в отдельные переменные,
поэтому мы можем обращаться к ним напрямую, как $code или $type.

6.30 Протоколирование сообщений

Если возникает ошибка, то соответствующее сообщение с уровнем error всегда вносится в лог.
В случае, если ошибка — результат предупреждения или уведомления РНР, сообщению присваивается категория
php, если же ошибка вызвана не пойманным исключением, сообщению присваивается категория
exception.ExceptionClassName (в случае [CHttpException] к категории добавляется
[код состояния|CHttpException::statusCode]). Для отслеживания ошибок, возникающих в процессе
выполнения приложения, можно использовать функционал журналирования.
Автоматическая генерация кода
=============================

Начиная с версии 1.1.2, в состав Yii входит веб-инструмент для генерации кода,
называемый Gii. Он заменяет существовавший до этого консольный генератор
yiic shell. В данном разделе описано, как использовать Gii и как расширить его
для ускорения разработки.

6.31 Использование Gii

Gii является модулем и должен быть использован в составе существующего приложения Yii.
Для использования Gii необходимо отредактировать файл конфигурации приложения
следующим образом:

[php]
return array(
    …
    'modules'=>array(
        'gii'=>array(
            'class'=>'system.gii.GiiModule',
            'password'=>'задайте свой пароль',
            // 'ipFilters'=>array(…список IP…),
            // 'newFileMode'=>0666,
            // 'newDirMode'=>0777,
        ),
    ),
);

Выше мы объявили модуль с именем gii и классом [GiiModule]. Также мы задали
пароль, который будет использоваться для доступа к Gii.

По умолчанию, в целях безопасности, Gii доступен только для localhost.
Если необходимо дать доступ к нему с других компьютеров, нужно задать
свойство [GiiModule::ipFilters] как показано в коде выше.

Так как Gii будет генерировать и сохранять новые файлы с кодом в существующее
приложение, необходимо убедиться в том, что процесс веб-сервера имеет на это права.
Показанные выше свойства [GiiModule::newFileMode] и [GiiModule::newDirMode]
содержат права, с которыми будут создаваться файлы и директории.

Note|Примечание: Gii является инструментом разработчика. Поэтому он должен быть
установлен исключительно на компьютере или сервере разработчика. Так как
он может генерировать новые скрипты PHP, необходимо уделить особое внимание
безопасности (пароль, IP фильтры).

Теперь можно запустить Gii по URL http://hostname/path/to/index.php?r=gii, где
http://hostname/path/to/index.php — URL вашего приложения.

Если существующее приложение использует формат URL path
(см. красивые адреса URL), мы можем запустить Gii по URL
http://hostname/path/to/index.php/gii. Может понадобиться добавить следующие правила
URL перед уже существующими:

[php]
'components'=>array(
    …
    'urlManager'=>array(
        'urlFormat'=>'path',
        'rules'=>array(
            'gii'=>'gii',
            'gii/<controller:\w+>'=>'gii/<controller>',
            'gii/<controller:\w+>/<action:\w+>'=>'gii/<controller>/<action>',
            …существующие правила…
        ),
    ),
)

В составе Gii есть готовый набор генераторов кода. Каждый генератор отвечает за
свой тип кода. К примеру, генератор контроллера создаёт класс контроллера
вместе с несколькими шаблонами отображения; генератор модели создаёт класс ActiveRecord
для определённой таблицы БД.

Последовательность работы с генератором следующая:

  1. Зайти на страницу генератора;
  2. Заполнить поля, которые задают параметры генерируемого кода. К примеру,
    для генерации модуля необходимо указать его ID;
  3. Нажать кнопку Preview для предварительной оценки генерируемого кода.
    Вы увидите таблицу файлов, которые будут сгенерированы и сможете просмотреть их
    код;
  4. Нажать кнопку Generate для создания файлов;
  5. Просмотреть журнал генерации кода.

Note|Примечание: после генерации модели стоит проверить и скорректировать метод rules так как структура базы данных
часто не содержит достаточно данных о требованиях валидации.

6.32 Расширение Gii

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

Gii можно расширять двумя способами: изменяя существующие шаблоны кодогенераторов
и создавая свои генераторы.

6.32.1 Структура кодогенератора

Генератор кода размещается в директории, чьё имя является именем генератора.
Директория обычно содержит:

model/                       корневая директория генератора модели
   ModelCode.php             модель, используемая для генерации кода
   ModelGenerator.php        контроллер кодогенератора
   views/                    отображения генератора
      index.php              шаблон по умолчанию
   templates/                шаблоны кода
      default/               набор шаблонов 'default'
         model.php           шаблон для генерации класса модели

6.32.2 Путь поиска генераторов

Gii ищет генераторы в списке директорий, указанных в свойстве
[GiiModule::generatorPaths]. В том случае, если необходимо добавить
свои генераторы, следует настроить приложение следующим образом:

[php]
return array(
    'modules'=>array(
        'gii'=>array(
            'class'=>'system.gii.GiiModule',
            'generatorPaths'=>array(
                'common.gii',   // псевдоним пути
            ),
        ),
    ),
);

Приведённые выше настройки заставляют Gii искать генераторы в директории
с псевдонимом common.gii в дополнение к стандартным system.gii.generators и application.gii.

Возможно иметь несколько одноимённых генераторов, если у них разные пути
поиска. В этом случае будет использоваться генератор, путь поиска которого
указан выше в [GiiModule::generatorPaths].

6.32.3 Изменение шаблонов кода

Изменение шаблонов кода — самый простой и самый распространённый путь расширения
Gii. Мы будем использовать примеры для того, чтобы описать, как изменить шаблоны
кода. Допустим, нам необходимо изменить код, создаваемый генератором модели.

Сначала мы создаём директорию protected/gii/model/templates/compact. Здесь model
означает, что мы собираемся перекрыть генератор модели по умолчанию.
А templates/compact — что мы добавляем новый набор шаблонов кода compact.

После этого мы добавляем в настройки приложения в свойство [GiiModule::generatorPaths]
значение application.gii, как показано в предыдущем подразделе.

Теперь открываем страницу генератора модели. Щёлкаем на поле Code Template.
Вы должны увидеть выпадающий список, содержащий нашу только что созданную директорию
шаблонов compact. Тем не менее, если мы выберем этот шаблон, будет выведена
ошибка. Происходит это потому, что в наборе compact ещё нет самих шаблонов кода.

Скопируем файл framework/gii/generators/model/templates/default/model.php в
protected/gii/model/templates/compact. Если попробовать сгенерировать код
с набором compact ещё раз, генерация должна пройти успешно. Тем не менее,
генерируемый код ничем не отличается от кода, получаемого из набора default.

Время сделать некоторые изменения.
Откроем файл protected/gii/model/templates/compact/model.php. Данный файл будет
использован как шаблон отображения, что означает, что он может содержать
выражения и код PHP. Изменим шаблон таким образом, что метод attributeLabels()
генерируемого кода будет использовать Yii::t() для перевода заголовков полей:

[php]
public function attributeLabels()
{
    return array(
<?php foreach($labels as $name=>$label): ?>
            <?php echo "'$name' => Yii::t('application', '$label'),\n"; ?>
<?php endforeach; ?>
    );
}

В каждом шаблоне кода у нас есть доступ к некоторым предопределённым переменным,
таким как, например, $labels. Эти переменные задаются соответствующим генератором
кода. Разные генераторы могут предоставлять шаблонам различные наборы переменных.
Стоит внимательно изучить описание шаблонов кода по умолчанию.

6.32.4 Создание новых генераторов

В этом подразделе мы покажем, как реализовать новый генератор, который сможет
создавать новые классы виджетов.

Сначала создадим директорию protected/gii/widget. В ней создадим следующие файлы:

  • WidgetGenerator.php: содержит класс контроллера WidgetGenerator, который
    является входной точкой генератора виджетов.
  • WidgetCode.php: содержит класс модели WidgetCode, который отвечает за
    логику генерации кода.
  • views/index.php: отображение, содержащее форму ввода генератора.
  • templates/default/widget.php: шаблон кода по умолчанию для генерации класса
    виджета.

6.32.4.1 Реализация WidgetGenerator.php

Файл WidgetGenerator.php предельно простой. Он содержит лишь следующий код:

[php]
class WidgetGenerator extends CCodeGenerator
{
    public $codeModel='application.gii.widget.WidgetCode';
}

Здесь мы описываем, что генератор будет использовать класс модели, чей псевдоним
пути application.gii.widget.WidgetCode. Класс WidgetGenerator наследуется
от [CCodeGenerator], реализующего большое количество функций, включая
действия контроллера, необходимые для координации процесса генерации кода.

6.32.4.2 Реализация WidgetCode.php

Файл WidgetCode.php содержит класс модели WidgetCode, в котором реализована
логика генерации класса виджета на основе полученных от пользователя параметров.
В данном примере будем считать, что единственное, что вводит пользователь —
имя класса виджета. WidgetCode выглядит следующим образом:

[php]
class WidgetCode extends CCodeModel
{
    public $className;

    public function rules()
    {
        return array_merge(parent::rules(), array(
            array('className', 'required'),
            array('className', 'match', 'pattern'=>'/^\w+$/'),
        ));
    }

    public function attributeLabels()
    {
        return array_merge(parent::attributeLabels(), array(
            'className'=>'Widget Class Name',
        ));
    }

    public function prepare()
    {
        $path=Yii::getPathOfAlias('application.components.' . $this->className) . '.php';
        $code=$this->render($this->templatepath.'/widget.php');

        $this->files[]=new CCodeFile($path, $code);
    }
}

Класс WidgetCode наследуется от [CCodeModel]. Как и в обычном классе модели, в
данном классе мы реализуем методы rules() и attributeLabels() для валидации
ввода и генерации подписей полей соответственно. Стоит отметить, что так как
базовый класс [CCodeModel] уже описывает некоторое количество правил валидации
и названий подписей, то мы должны объединить их с нашими правилами и подписями.

Метод prepare() подготавливает код к генерации. Главная задача метода — подготовить
список объектов [CCodeFile], каждый из которых представляет будущий файл с кодом.
В нашем примере необходимо создать всего один объект [CCodeFile], представляющий
класс виджета, который будет сгенерирован в директории protected/components.
Для непосредственной генерации кода используется метод [CCodeFile::render].
Данный метод содержит PHP-шаблон кода и возвращает сгенерированный код.

6.32.4.3 Реализация views/index.php

После реализации контроллера (WidgetGenerator) и модели (WidgetCode)
самое время заняться отображением views/index.php:

[php]
<h1>Генератор виджета</h1>

<?php $form=$this->beginWidget('CCodeForm', array('model'=>$model)); ?>

    <div class="row">
        <?php echo $form->labelEx($model,'className'); ?>
        <?php echo $form->textField($model,'className',array('size'=>65)); ?>
        <div class="tooltip">
            Класс виджета должен содержать только буквы.
        </div>
        <?php echo $form->error($model,'className'); ?>
    </div>

<?php $this->endWidget(); ?>

В данном коде мы отображаем форму, используя виджет [CCodeForm]. В этой форме мы
показываем поле для ввода атрибута className модели WidgetCode.

При создании формы мы можем использовать две замечательные возможности [CCodeForm].
Одна — подсказки для полей. Вторая — запоминание введённых значений.

Если вы использовали один из стандартных генераторов кода, вы могли заметить красивые
всплывающие подсказки, появляющиеся рядом с полем при получении им фокуса. Использовать
данную возможность очень легко: достаточно после поля вставить div с CSS классом tooltip.

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

Для того, чтобы сделать поле запоминаемым, необходимо сделать две вещи.

Во-первых, нужно описать правило валидации sticky для соответствующего атрибута
модели. К примеру, для стандартного генератора контроллера используется приведённое
ниже правило для запоминания атрибутов baseClass и actions:

[php]
public function rules()
{
    return array_merge(parent::rules(), array(
        …
        array('baseClass, actions', 'sticky'),
    ));
}

Во-вторых, в отображении необходимо добавить CSS класс sticky контейнеру div
поля ввода:

[php]
<div class="row sticky">
    …поле ввода…
</div>

6.32.4.4 Реализация templates/default/widget.php

Наконец, мы создаём шаблон кода templates/default/widget.php. Как было описано
ранее, он используется как PHP-шаблон отображения. В шаблоне кода мы всегда
можем обратиться к переменной $this, которая содержит экземпляр модели кода.
В нашем примере $this содержит объект WidgetModel. Таким образом, мы можем
получить введённый пользователем класс виджета через $this->className.

[php]
<?php echo '<?php'; ?>

class <?php echo $this->className; ?> extends CWidget
{
    public function run()
    {

    }
}

На этом реализация генератора кода завершена. Обратиться к нему можно по
URL http://hostname/path/to/index.php?r=gii/widget.
Интернационализация
===================

Интернационализация (сокращённо I18N) — процесс создания приложения, которое может
работать на различных языках и с различными региональными особенностями без каких-либо
дополнительных изменений. Для веб-приложений это особенно важно, так как пользователь
может быть из любой точки мира.

Yii поддерживает интернационализацию на нескольких уровнях:

  • Предоставляет региональные данные для всех возможных языков и их вариаций.
  • Сервис для перевода сообщений и файлов.
  • Форматирование дат и чисел в зависимости от региональных настроек.

В последующих разделах мы рассмотрим перечисленные возможности более подробно.

6.33 Региональные настройки и язык

Региональные настройки — это набор параметров, задающих язык пользователя,
страну и любые другие специальные настройки интерфейса. Обычно настройки определяются идентификатором,
состоящем из языка и региона. К примеру, идентификатор en_US соответствует английскому языку и настройкам для США.
В Yii все идентификаторы региональных настроек названы единообразно: LanguageID (язык) или LanguageID_RegionID (язык_регион)
в нижнем регистре. Например: ru, en_us.

Региональные настройки хранятся в объекте класса [CLocale], который можно использовать для получения
зависимой от них информации, такой как символы и форматы валют и чисел, форматы дат и времени, название
месяцев и дней недели. Так как информация о языке уже содержится в идентификаторе,
в [CLocale] она не дублируется. По этой причине мы часто применяем в одном контексте термины «язык», «региональные настройки» и «локаль».

Имея идентификатор языка, мы можем получить соответствующий ему объект [CLocale]:
CLocale::getInstance($localeID) или CApplication::getLocale($localeID).

Info|Информация: в состав Yii включены региональные данные практически по
всем языкам и регионам. Данные получены из
Common Locale Data Repository (CLDR). Доступны не
все данные, предоставляемые CLDR, так как там содержится большое количество
редко используемой информации. Пользователи также могут
предоставлять свои собственные специальные региональные данные. Для этого
настройте свойство [CApplication::localeDataPath], чтобы оно указывало на
директорию, содержащую специальные региональные данные. Обратитесь к файлам
региональных данных в директории framework/i18n/data для того, чтобы создать
файлы специальных региональных данных.

В приложении Yii мы различаем [язык приложения|CApplication::language] и [исходный язык приложения|CApplication::sourceLanguage].
Язык приложения — это язык (локаль) пользователя, который работает с приложением. Исходный язык приложения — язык, который используется
в исходном коде приложения. Интернационализация требуется только в том случае, если эти два языка различаются.

Tip|Подсказка: Лучше всего в качестве исходного языка оставить английский.
Вам будет несложно найти переводчиков с английского на любой другой язык, чего
нельзя сказать о переводчиках с любого другого языка.

Вы можете установить [язык приложения|CApplication::language] в
настройках приложения или
изменить его непосредственно перед использованием возможностей интернационализации.

Tip|Подсказка: Иногда нам требуется считать язык пользователя из браузера.
Мы можем получить идентификатор локали используя [CHttpRequest::preferredLanguage].

6.34 Перевод

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

В процессе перевода участвуют объект перевода, исходный язык и конечный язык.
В Yii исходный язык по умолчанию приравнивается к [исходному языку приложения|CApplication::sourceLanguage], а
язык — к [языку приложения|CApplication::language]. Если оба языка совпадают — перевод не производится.

6.34.1 Перевод сообщений

Перевод сообщений осуществляется при помощи метода [Yii::t()|YiiBase::t]. Метод переводит
данное сообщение с [исходного языка приложения|CApplication::sourceLanguage] на [текущий язык приложения|CApplication::language].

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

Сообщения могут содержать параметры, которые при вызове [Yii::t()|YiiBase::t] заменяются
соответствующими им значениями. К примеру, следующий вызов метода заменит
{alias} на значение соответствующей переменной:

[php]
Yii::t('app', 'Path alias "{alias}" is redefined.',
    array('{alias}'=>$alias))

Note|Примечание: переводимые сообщения не должны содержать переменных, изменяющих
строку ("Invalid {$message} content."). Если есть необходимость подставлять в строку
какие-либо значения — используйте параметры.

Переведённые сообщения хранятся в репозитории, называемом источник сообщений. Источник
сообщений представляет собой экземпляр класса [CMessageSource] или его наследника. При выполнении
[Yii::t()|YiiBase::t] производится поиск сообщения в источнике сообщений и, если оно найдено —
возвращается его переведённая версия.

Yii поддерживает несколько типов источников сообщений, перечисленных ниже. Также вы можете
создать свой источник, унаследовав его от [CMessageSource].

  • в массиве PHP. Исходное сообщение при этом является ключом, а переведённое —
    значением. Каждый массив содержит переводы для определённой категории сообщений
    и находится в отдельном файле, имя которого совпадает с названием категории.
    Файлы с переводом для одного и того же языка хранятся в одной директории,
    имеющей такое же имя, как и идентификатор языка. Директории для всех языков
    располагаются в директории, указанной в [basePath|CPhpMessageSource::basePath];

  • [CGettextMessageSource]: переводы сообщений хранятся в формате GNU
    Gettext
    ;

  • [CDbMessageSource]: переводы сообщений хранятся в базе данных. Подробнее см. [CDbMessageSource].

Источник сообщений загружается как компонент приложения.
Сообщения, которые используются в приложении, хранятся в компоненте [messages|CApplication::messages].
По умолчанию тип данного источника сообщений — CPhpMessageSource. Путь, по которому хранятся файлы перевода — protected/messages.

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

  1. Вызвать [Yii::t()|YiiBase::t] в нужных местах;

  2. Создать файлы перевода protected/messages/IdЯзыка/ИмяКатегории.php. Каждый такой файл
    просто возвращает массив переведённых сообщений. Обратите внимание, что при этом используется CPhpMessageSource;

  3. В файле конфигурации установите значения [CApplication::sourceLanguage] и [CApplication::language].

Tip|Подсказка: Если в качестве источника сообщений используется CPhpMessageSource, для работы с сообщениями
может использоваться утилита yiic. При помощи команды message возможно выбрать из исходного кода все сообщения,
для которых необходим перевод и, при необходимости, объединить их с уже существующим переводом. Подробное
описание команды message можно получить набрав в консоли yiic help message.

При использовании CPhpMessageSource, сообщения для расширений, таких, как виджет или модуль, могут быть использованы особым образом.
То есть, если сообщение принадлежит расширению с именем класса Xyz, то категория сообщений может быть указана в формате
Xyz.имяКатегории. Соответствующий ей файл сообщений будет
ПутьДоРасширения/messages/IDЯзыка/имяКатегории.php, где ПутьДоРасширения — директория,
в которой находится класс расширения. При использовании Yii::t() для перевода
сообщения расширений должен использоваться следующий формат:

[php]
Yii::t('Xyz.имяКатегории', 'сообщение для перевода')

Yii поддерживает [формат выбора|CChoiceFormat], известный также как множественные
формы. Формат выбора предназначен
для выбора перевода в зависимости от заданного числа. К примеру, в английском языке
слово ‘book’ может быть единственного или множественного числа в зависимости от количества книг. В других
языках слово может не иметь специальной формы (как в китайском) или может подчиняться более сложным правилам
для множественного числа (как в русском). Формат выбора решает данную проблему простым, но в то же время
эффективным способом.

Для использования формата выбора перевод должен содержать последовательность пар выражение-сообщение, разделённых
символом |:

[php]
'expr1#message1|expr2#message2|expr3#message3'

где exprN — выражение PHP, возвращающее логическое значение. Если выражение равно true —
используется соответствующий ему перевод и дальнейшие выражения не вычисляются. Выражение
может содержать специальную переменную n (не $n!), которая содержит число, переданное
первым параметром. Допустим, если мы используем перевод

[php]
'n==1#one book|n>1#many books'

и передаём число 2 параметром [Yii::t()|YiiBase::t], то получим many books:

[php]
Yii::t('app', 'n==1#one book|n>1#many books', array(1));
//or since 1.1.6
Yii::t('app', 'n==1#one book|n>1#many books', 1);

Если проверяется соответствие определённому числу, можно использовать сокращённую запись,
которая будет рассматриваться как n==Number:

[php]
'1#one book|n>1#many books'

6.34.2 Формат для множественных форм

С версии 1.1.6 доступен ещё один механизм, выполняющий перевод множественных
форм слова по правилам CLDR. Он позволяет использовать более простую запись
правил, что особенно актуально для языков со сложными правилами для множественных
форм слова.

Правило для множественных форм английского языка, приведённое выше, можно
записать так:

[php]
Yii::t('test', 'cucumber|cucumbers', 1);
Yii::t('test', 'cucumber|cucumbers', 2);
Yii::t('test', 'cucumber|cucumbers', 0);

что даст на выходе:

cucumber
cucumbers
cucumbers

Если требуется включить в сообщение число, можно использовать следующий код:

[php]
echo Yii::t('test', '{n} cucumber|{n} cucumbers', 1);

Здесь {n} — специальный токен, который будет заменён на переданное число.
В данном случае будет напечатано 1 cucumber.

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

[php]
Yii::t('test', '{username} has a cucumber|{username} has {n} cucumbers',
array(5, '{username}' => 'samdark'));

и даже заменить число чем-нибудь ещё:

[php]
function convertNumber($number)
{
    // число прописью
    return $number;
}

Yii::t('test', '{n} cucumber|{n} cucumbers',
array(5, '{n}' => convertNumber(5)));

Количество множественных форм будет варьироваться от языка к языку. К примеру:

[php]
Yii::t('app', '{n} cucumber|{n} cucumbers', 62);
Yii::t('app', '{n} cucumber|{n} cucumbers', 1.5);
Yii::t('app', '{n} cucumber|{n} cucumbers', 1);
Yii::t('app', '{n} cucumber|{n} cucumbers', 7);

при переводе на русский будет содержать четыре множественных формы вместо двух:

[php]
'{n} cucumber|{n} cucumbers' => '{n} огурец|{n} огурца|{n} огурцов|{n} огурца',

На выходе:

62 огурца
1.5 огурца
1 огурец
7 огурцов

Info|Информация: число и порядок выражений можно узнать в разделе
Language Plural Rules
на сайте CLDR.

6.34.3 Перевод файлов

Перевод файлов осуществляется вызовом [CApplication::findLocalizedFile()]. Параметром
передаётся путь к файлу, который необходимо перевести. Метод ищет одноимённый файл в
подпапке LocaleID. Если файл найден — возвращается его путь, иначе — путь к исходному файлу.

Перевод файла используется в основном при отображении представлений. При вызове одного из методов
отображения контроллера или виджета, файлы представления будут переведены автоматически. К примеру, если
[язык приложения|CApplication::language] установлен как zh_cn, а [исходный язык|CApplication::sourceLanguage] как en_us
— при отображении представления edit будет произведён поиск представления
protected/views/ControllerID/zh_cn/edit.php. Если оно найдено — будет использована переведённая версия, если нет — исходная,
расположенная в protected/views/ControllerID/edit.php.

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

6.35 Форматирование даты и времени

Дата и время в разных странах и регионах часто форматируются по-разному. Задача форматирования
даты и времени таким образом сводится к генерации строки, подходящей для данной страны и региона.
Для этого в Yii используется [CDateFormatter].

Каждый экземпляр [CDateFormatter] соответствует некому языку приложения. Чтобы получить
форматтер для выбранного языка, мы можем просто обратиться к свойству приложения
[dateFormatter|CApplication::dateFormatter].

Класс [CDateFormatter] содержит два метода, предназначенных для форматирования UNIX
timestamp:

  • согласно шаблону даты-времени (например, $dateFormatter->format('dd.MM.yyyy', $timestamp));

  • согласно шаблону, заданному для выбранного языка (например формат даты short, формат времени long).

6.36 Форматирование чисел

Также, как дата и время, числа могут писаться по-разному в разных странах и регионах.
Форматирование чисел включает в себя форматирование десятичных дробей,
валют и чисел с процентами. Для выполнения данных задач в Yii используется класс [CNumberFormatter].

Для того, чтобы воспользоваться форматтером чисел, соответствующим выбранному языку, мы можем обратиться к
свойству приложения [numberFormatter|CApplication::numberFormatter].

Для форматирования целых чисел и дробей в классе [CNumberFormatter] есть следующие методы:

  • [format|CNumberFormatter::format]: форматирует число в соответствии с заданным форматом
    (например, $numberFormatter->format('#,##0.00',$number));

  • заданным для текущего языка приложения;

  • форматом, заданным для текущего языка приложения;

  • заданным для текущего языка приложения.
    Журналирование
    ==============

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

6.37 Протоколирование сообщений

Сообщение может быть запротоколировано путем вызова [Yii::log] или [Yii::trace]. Разница между ними
заключается в том, что последний пишет в лог только тогда, когда приложение работает в
режиме отладки.

[php]
Yii::log($message, $level, $category);
Yii::trace($message, $category);

Для внесения сообщения в лог, необходимо указать категорию и уровень сообщения.
Категория представляет собой строку формата xxx.yyy.zzz, что очень схоже с форматом
представления псевдонима пути. Например, если сообщение
добавляется в лог в [CController], мы можем использовать категорию system.web.CController.
Уровень сообщения может иметь одно из следующих значений:

  • trace: этот уровень используется методом [Yii::trace]. Он предназначен для отслеживания процесса выполнения
    приложения в ходе разработки;

  • info: этот уровень предназначен для протоколирования информации общего характера;

  • profile: данный уровень используется для профилирования (измерения) производительности;

  • warning: этот уровень предназначен для сообщений-предупреждений;

  • error: этот уровень используется для сообщений о критических ошибках.

6.38 Маршрутизация сообщений

Сообщения, протоколируемые с использованием [Yii::log] или [Yii::trace], хранятся в памяти. Как правило,
нам требуется либо отобразить их в окне браузера, либо сохранить в файле, отправить электронным письмом и пр.
Направление сообщений в различные места назначения называется маршрутизацией сообщений.

В Yii за маршрутизацию сообщений отвечает компонент приложения [CLogRouter]. Этот компонент управляет
множеством так называемых маршрутов сообщений. Каждый маршрут представляет одно место назначения потока сообщений.
Сообщения, направляемые по тому или иному маршруту, можно отфильтровать в зависимости от их уровня и типа.

Для того, чтобы воспользоваться маршрутизацией сообщений, нам необходимо установить и подгрузить заранее
компонент приложения [CLogRouter]. Кроме того, необходимо настроить свойство [routes|CLogRouter::routes] этого компонента,
указав маршруты сообщений, которые предполагается использовать. Ниже приведен пример необходимой
конфигурации приложения:

[php]
array(
    …
    'preload'=>array('log'),
    'components'=>array(
        …
        'log'=>array(
            'class'=>'CLogRouter',
            'routes'=>array(
                array(
                    'class'=>'CFileLogRoute',
                    'levels'=>'trace, info',
                    'categories'=>'system.*',
                ),
                array(
                    'class'=>'CEmailLogRoute',
                    'levels'=>'error, warning',
                    'emails'=>'admin@example.com',
                ),
            ),
        ),
    ),
)

В примере выше, у нас есть два маршрута сообщений. Первый - CFileLogRoute - сохраняет сообщения в
папке приложения для временных файлов runtime. Сохраняются только сообщения с уровнем trace или info и чья
категория начинается с system.. Второй маршрут - CEmailLogRoute - отправляет сообщения на указанный электронный адрес.
Отправляются только сообщения уровня error или warning.

Начиная с Yii версии 1.1.13 можно исключать определённые категории:

[php]
    'routes'=>array(
        array(
            'class'=>'CEmailLogRoute',
            'levels'=>'error, warning',
            'except'=>'system.CModule.*' // отсылаем почтой всё кроме сообщений от CModule
            'emails'=>'admin@example.com',
        ),
        array(
            'class'=>'CWebLogRoute',
            'categories'=>'system.db.*',
            'except'=>'system.db.ar.*', // показываем всё, что касается базы данных, но не касается AR
        ),

В Yii доступны для использования следующие маршруты сообщений:

[php]
array(
    ......
    'preload'=>array('log'),
    'components'=>array(
        ......
        'log'=>array(
            'class'=>'CLogRouter',
            'routes'=>array(
                array(
                    'class'=>'CProfileLogRoute',
                    'report'=>'summary',
                    // Показывает время выполнения каждого отмеченного блока кода.
                    // Значение "report" также можно указать как "callstack".
                ),
                ...остальные маршруты...
            ),
        ),
    ),
)

Info|Информация: Маршрутизация сообщения происходит в конце каждого текущего цикла обработки запроса, в момент, когда
вызывается событие [onEndRequest|CApplication::onEndRequest]. Для прерывания процесса обработки текущего запроса,
используйте метод [CApplication::end()] вместо die() или exit(). [CApplication::end()] вызывает событие
[onEndRequest|CApplication::onEndRequest], что позволяет корректно запротоколировать сообщения.

6.39 Фильтрация сообщений

Как уже упоминалось выше, сообщения можно отфильтровать по их уровню и типу до того, как
они будут направлены тем или иным маршрутом. Это осуществляется путем настройки свойств
[levels|CLogRoute::levels] и [categories|CLogRoute::categories] соответствующего маршрута.
Если необходимо указать несколько уровней или типов, значения должны быть разделены запятыми.

Поскольку типы сообщений указываются в формате xxx.yyy.zzz, мы можем воспринимать их как иерархию типов.
В частности, мы говорим, что xxx является родителем xxx.yyy, а последний в свою очередь является
родителем для xxx.yyy.zzz. Поэтому для указания типа xxx, а также всех его типов-потомков можно
использовать выражение xxx.*.

6.40 Сохранение контекста сообщений

Мы можем сохранять дополнительную информацию, такую как
предопределённые переменные PHP ($_GET, $_SERVER), ID сессии, имя
пользователя и т.д. Для этого необходимо задать необходимый фильтр в
свойстве [CLogRoute::filter].

В состав фреймворка входит удобный класс [CLogFilter], который может быть использован
в качестве фильтра в большинстве случаев. По умолчанию, [CLogFilter] будет записывать
сообщение вместе с такими переменными, как $_GET и $_SERVER, которые обычно
содержат ценную системную информацию. Можно настроить [CLogFilter] таким образом,
чтобы перед каждым сообщением записывать ID сессии, имя пользователя и другие данные,
которые могут облегчить поиск по большому количеству сообщений.

Следующие настройки включают запись контекста сообщений. У каждого журнального маршрута
может быть задан свой фильтр. По умолчанию никакого фильтра не задано.

[php]
array(
    …
    'preload'=>array('log'),
    'components'=>array(
        …
        'log'=>array(
            'class'=>'CLogRouter',
            'routes'=>array(
                array(
                    'class'=>'CFileLogRoute',
                    'levels'=>'error',
                    'filter'=>'CLogFilter',
                ),
                …other log routes…
            ),
        ),
    ),
)

Yii поддерживает журналирование информации стека вызова
в сообщениях, протоколируемых путем вызова [Yii::trace]. По умолчанию, данная
особенность отключена, т.к. снижает производительность. Для ее использования,
необходимо просто определить константу YII_TRACE_LEVEL в начале входного
скрипта (до включения файла yii.php) целым числом большим нуля. Тогда
Yii будет добавлять в каждое трассирующее сообщение имя файла и номер строки
стека вызова, в которых был сделан вызов кода. Число YII_TRACE_LEVEL
определяет количество слоев каждого стека вызова, которое должно быть записано.
Эта информация особенно полезна на стадии разработки, так как может помочь нам
определить места, в которых вызываются трассирующие сообщения.

6.41 Профилирование производительности

Для целей измерения производительности используется специальный тип сообщений. Его можно
использовать для измерения времени исполнения некоторого блока кода и определения
узких мест в производительности.

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

[php]
Yii::beginProfile('blockID');
…блок профилируемого кода…
Yii::endProfile('blockID');

где blockID — это уникальный идентификатор блока кода.

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

Для того, чтобы увидеть результат профилирования, нам потребуется установить компонент приложения CProfileLogRoute,
отвечающий за соответствующий маршрут протоколирования. Здесь все аналогично работе с простыми маршрутами сообщений.
Маршрут CProfileLogRoute отобразит результаты измерения производительности внизу текущей страницы.

6.42 Профилирование SQL-запросов

Профилирование особенно полезно при работе с базой данных, так как SQL-запросы
часто являются самым узким местом производительности приложения. Несмотря на то,
что мы можем вставить в нужные места beginProfile и endProfile для того, чтобы
замерить время, затраченное на каждый SQL-запрос, Yii предоставляет
более удобное решение данной проблемы.

Выставив в настройках приложения [CDbConnection::enableProfiling] в true, мы получим
профилирование всех выполняемых SQL-запросов. Полученные результаты можно вывести при
помощи вышеупомянутого CProfileLogRoute, показывающего, какой SQL-запрос сколько времени
занял. Для вывода общего количества запросов и общего времени выполнения можно
использовать [CDbConnection::getStats()].
Улучшение производительности
============================

Производительность веб-приложения зависит от многих факторов. Главные из них —
обращение к базе данных, файловой системе и пропускная способность сети.
В Yii, для уменьшения падения производительности из-за самого фреймворка, учтён
каждый из этих факторов. Несмотря на это, многие части приложения можно улучшить
для получения более высокой производительности.

6.43 Включение расширения APC

Включение расширения PHP APC
возможно, самый простой способ улучшить общую производительность приложения.
Расширение оптимизирует и кэширует промежуточный код PHP и выигрывает время,
затрачиваемое на интерпретацию скриптов PHP при каждом запросе.

6.44 Отключение режима отладки

Отключение режима отладки — ещё один лёгкий способ увеличить производительность.
Приложение Yii работает в режиме отладки если константа YII_DEBUG определена
как true. Режим отладки полезен при разработке, но не лучшим образом влияет на
производительность из-за использования большего числа компонентов. К примеру,
при журналировании ошибок, с каждым сообщением может записываться дополнительная
информация.

6.45 Использование yiilite.php

Если используется расширение PHP APC,
мы можем заменить yii.php другим загрузчиком — yiilite.php. Это даст приложению
ещё больший прирост производительности.

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

Стоит заметить, что использование yiilite.php без APC может отрицательно
повлиять на производительность, так как yiilite.php включает в себя классы,
которые могут не требоваться при каждом запросе и отнимать некоторое время на
парсинг. Также было отмечено, что на некоторых конфигурациях сервера yiilite.php
медленнее даже при использовании APC. Лучший способ принятия решения об
использовании yiilite.php — провести тесты на прилагающемся демонстрационном
приложении hello world.

6.46 Использование кэширования

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

Если используется Active Record, можно включить
кэширование структуры базы данных. Это можно сделать, установив в настройках
свойству [CDbConnection::schemaCachingDuration] значение, большее 0.

Кроме описанных настроек приложения можно использовать кэширование на уровне
сервера. Описанное выше
кэширование APC относится
как раз к ним. Существуют и другие решения, такие как
Zend Optimizer, eAccelerator
и Squid.

6.47 Оптимизация базы данных

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

Будьте осмотрительны при выборе индексов. Их использование может значительно
ускорить SELECT-запросы, но замедляет запросы INSERT, UPDATE и DELETE.

Для сложных запросов рекомендуется создать view в базе данных вместо использования
запросов из кода PHP, которые СУБД разбирает каждый раз.

Не злоупотребляйте Active Record. Хоть Active
Record
и является удобной проекцией данных в стиле ООП,
но производительность при её использовании, из-за использования объектов для
представления каждой строки результата, падает. Для приложений, интенсивно
работающих с данными, рекомендуется использовать DAO
или API для работы с СУБД на ещё более низком уровне.

Последний по счёту, но не по значению совет: используйте LIMIT в
SELECT-запросах. Так вы сможете избежать получение избыточных данных из базы и
расхода требующейся для их хранения памяти, выделенной PHP.

6.48 Минимизация файлов скриптов

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

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

Для начала опишем, какие файлы минимизировать. Зададим свойство
[scriptMap|CClientScript::scriptMap] компонента
[clientScript|CWebApplication::clientScript]. Это можно сделать как в настройках
приложения, так и в коде. К примеру:

[php]
$cs=Yii::app()->clientScript;
$cs->scriptMap=array(
    'jquery.js'=>'/js/all.js',
    'jquery.ajaxqueue.js'=>'/js/all.js',
    'jquery.metadata.js'=>'/js/all.js',
    …
);

Приведённый код сделает файлы JavaScript доступными по URL /js/all.js.
Если какой-либо из этих файлов требуется для каких-либо компонент, Yii
подключит URL (один раз) вместо того, чтобы подключать отдельные файлы.

Нам понадобится использовать какой-либо инструмент для слияния (и, возможно, сжатия)
JavaScript в один файл и записать результат в js/all.js.

То же относится и к файлам CSS.

Увеличить скорость загрузки страницы можно также при помощи
Google AJAX Libraries API. К примеру,
мы можем подключить jquery.js с серверов Google вместо того, чтобы использовать
свой сервер. Для того, чтобы это сделать нужно настроить scriptMap следующим
образом:

[php]
$cs=Yii::app()->clientScript;
$cs->scriptMap=array(
    'jquery.js'=>false,
    'jquery.ajaxqueue.js'=>false,
    'jquery.metadata.js'=>false,
    …
);

Устанавливая значения в false мы запрещаем Yii генерировать код для включения
соответствующих файлов. Вместо этого подключим их с серверов Google:

[php]
<head>
<?php echo CGoogleApi::init(); ?>

<?php echo CHtml::script(
    CGoogleApi::load('jquery','1.3.2') . "\n" .
    CGoogleApi::load('jquery.ajaxqueue.js') . "\n" .
    CGoogleApi::load('jquery.metadata.js')
); ?>
…
</head>

6.49 Использование символьных ссылок для ресурсов

Если ваш проект интенсивно использует ресурсы, то вы можете увеличить его производительность
посредством символьных ссылок
вместо стандартного копирования файлов. Для того, чтобы включить их вам нужно задать
свойство [linkAssets|CAssetManager::linkAssets] компонента приложения assetManager
используя конфигурационный файл protected/config/main.php:

[php]
return array(
    // ...
    'components' => array(
        // ...
        'assetManager' => array(
            'linkAssets' => true,
        ),
    ),
);

Имейте ввиду, что [это потребует дополнительных настроек|CAssetManager::linkAssets].

7 Альтернативный язык шаблонов

Yii позволяет разработчику использовать свой любимый язык шаблонов (например,
Prado или Smarty) для описания представлений контроллера или виджета. Для этого
требуется написать и установить свой компонент [viewRenderer|CWebApplication::viewRenderer].
Обработчик представления перехватывает вызовы [CBaseController::renderFile],
компилирует файл представления с альтернативным синтаксисом и отдаёт результат
компиляции.

Info|Информация: Не рекомендуется использовать альтернативный синтаксис шаблонов
для описания представлений компонентов, выкладываемых в открытый доступ. Это
приведёт к требованию использовать тот же синтаксис, что использован в представлении
компонента.

Далее мы покажем, как использовать [CPradoViewRenderer] — обработчик представлений,
позволяющий разработчику использовать синтаксис шаблонов, используемый в фреймворке
Prado. Если вы хотите реализовать свои обработчики
представлений, обязательно изучите [CPradoViewRenderer].

7.1 Использование CPradoViewRenderer

Для использования [CPradoViewRenderer] необходимо настроить приложение
следующим образом:

[php]
return array(
    'components'=>array(
        …,
        'viewRenderer'=>array(
            'class'=>'CPradoViewRenderer',
        ),
    ),
);

По умолчанию [CPradoViewRenderer] будет компилировать исходные файлы представлений и
сохранять получаемые файлы PHP в директорию runtime.
PHP-файлы изменяются только в том случае, если изменено исходное представление.
Поэтому, использование [CPradoViewRenderer] влечёт за собой очень незначительное
падение производительности.

Tip|Подсказка: Несмотря на то, что [CPradoViewRenderer] добавляет новый синтаксис
для более быстрого и удобного описания представлений, вы можете использовать код
PHP также, как и в обычных представлениях.

Ниже будут описаны конструкции, поддерживаемые [CPradoViewRenderer].

7.1.1 Сокращённые PHP-тэги

Сокращённые PHP-тэги — хороший способ сократить код, используемый в представлении.
Выражение <%= expression %> преобразуется в <?php echo expression ?>.
<% statement %> — в <?php statement ?>. К примеру:

[php]
<%= CHtml::textField($name,'value'); %>
<% foreach($models as $model): %>

преобразуется в

[php]
<?php echo CHtml::textField($name,'value'); ?>
<?php foreach($models as $model): ?>

7.1.2 Компонентные тэги

Компонентные тэги используются для того, чтобы вставить в представление
виджет. Синтаксис следующий:

[php]
<com:WidgetClass property1=value1 property2=value2 …>
    // содержимое виджета
</com:WidgetClass>

// виджет без содержимого
<com:WidgetClass property1=value1 property2=value2 …/>

Здесь WidgetClass определяет имя класса виджета или
псевдоним пути. Начальные значения свойств могут
быть как строками, заключенными в кавычки, так и выражениями PHP, заключёнными в
фигурные скобки. К примеру:

[php]
<com:CCaptcha captchaAction="captcha" showRefreshButton={false} />

преобразуется в

[php]
<?php $this->widget('CCaptcha', array(
    'captchaAction'=>'captcha',
    'showRefreshButton'=>false)); ?>

Note|Примечание: Значение showRefreshButton задано как {false}
вместо "false" так как последнее означает строку, а не логическое значение.

7.1.3 Кэширующие тэги

Кэширующие тэги — краткий способ использования
кэширования фрагментов. Синтаксис следующий:

[php]
<cache:fragmentID property1=value1 property2=value2 …>
    // содержимое, которое необходимо кэшировать
</cache:fragmentID >

Здесь fragmentID — уникальный идентификатор кэшируемого объекта. Пары имя-значение
используются для настройки кэширования фрагментов. К примеру:

[php]
<cache:profile duration={3600}>
    // информация из профиля пользователя
</cache:profile >

будет преобразовано в

[php]
<?php if($this->beginCache('profile', array('duration'=>3600))): ?>
    // информация из профиля пользователя
<?php $this->endCache(); endif; ?>

7.1.4 Захватывающие тэги

Как и кэширующие тэги, захватывающие тэги — компактный способ использования
[CBaseController::beginClip] и [CBaseController::endClip]. Синтаксис следующий:

[php]
<clip:clipID>
    // содержимое для захвата
</clip:clipID >

Здесь clipID — уникальный идентификатор захваченного содержимого.
Захватывающие тэги преобразуются следующим образом:

[php]
<?php $this->beginClip('clipID'); ?>
    // содержимое для захвата
<?php $this->endClip(); ?>

7.1.5 Тэги комментариев

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

[php]
<!---
Этот комментарий будет вырезан… цензурой
--->

7.2 Одновременное использование шаблонов разного формата

Начиная с версии 1.1.2 возможно использовать одновременно как альтернативный, так и
обычный PHP синтаксис шаблонов. Для этого необходимо задать свойству обработчика
шаблонов [CViewRenderer::fileExtension] значение, отличное от .php.
К примеру, если оно будет выставлено в .tpl, то все шаблоны с расширением .tpl будут
обрабатываться выбранным обработчиком представлений. Шаблоны с расширением .php,
как и ранее, будут использовать стандартный синтаксис PHP.
Безопасность
============

7.3 Предотвращение межсайтового скриптинга

Межсайтововый скриптинг (также известный как XSS) — злонамеренный сбор информации
пользователя через страницы веб-приложения. Чаще всего, производящий атаку, используя
уязвимости приложения, включает в текст страницы JavaScript, VBScript, ActiveX,
HTML или Flash. Делается это для получения информации других пользователей приложения
и последующего её использования в нехороших целях. К примеру, плохо написанный форум
может отображать сообщения пользователей без какой-либо проверки. Атакующий может
вставить JavaScript-код в сообщение. Все, кто прочитает это сообщение, выполнит
код на своём компьютере.

Чтобы не допустить XSS-атак, нужно всегда проверять то, что ввёл пользователь,
прежде чем это отображать. Конечно, чтобы не допустить ввода скриптов, можно
кодировать все HTML-сущности. В некоторых ситуациях такое поведение нежелательно,
так как ввод HTML становится недоступен.

Yii включает в себя библиотеку HTMLPurifier
и предоставляет разработчику полезный компонент [CHtmlPurifier], который
может отфильтровать весь вредоносный код при помощи тщательно проверенного
белого листа. Также компонент делает код совместимым со стандартами.

[CHtmlPurifier] может быть использован и как виджет,
и как фильтр. При использовании в качестве
виджета [CHtmlPurifier] обрабатывает заключённое в него содержимое:

[php]
<?php $this->beginWidget('CHtmlPurifier'); ?>
…этот текст будет подвергнут деликатной санобработке…
<?php $this->endWidget(); ?>

7.4 Предотвращение подделки межсайтовых запросов

Подделка межсайтового запроса (CSRF) — атака, при которой сайт атакующего
заставляет браузер пользователя выполнить какое-либо действие на другом сайте.
К примеру, на сайте атакующего есть страница, содержащая тэг img с атрибутом src,
указывающим на сайт банка: http://bank.example/перевод?сумма=10000&кому=кулхацкеру.
Если в браузере пользователя установлен cookie, позволяющий запомнить
его на сайте, посещение такой страницы вызовет перевод 10000 тугриков нехорошему
кулхацкеру. В CSRF, в отличие от межсайтового скриптинга, основанного на доверии
пользователя к некоторому сайту, используется доверие сайта определённому пользователю.

Для того, чтобы не допустить CSRF, важно придерживаться простого правила:
GET — только для получения данных. Ничего менять при GET-запросах нельзя.
Для POST необходимо использовать случайное значение, которое можно проверить
на сервере и убедиться, что запрос идёт оттуда, откуда нужно.

В Yii реализована защита от CSRF-атаки, проводимой через POST. Защита основана
на хранении случайного значения в cookie и сравнения его со значением в POST.

По умолчанию, защита от CSRF отключена. Для её включения необходимо настроить
компонент [CHttpRequest] в
файле конфигурации:

[php]
return array(
    'components'=>array(
        'request'=>array(
            'enableCsrfValidation'=>true,
        ),
    ),
);

Для отображения формы следует использовать [CHtml::form] вместо написания HTML-тэга.
Данный метод позволяет автоматически включить случайное значение, используемое для
проверки на CSRF, как скрытое поле формы.

Защита cookie очень важна, так как именно в них чаще всего хранится ID
сессии. Если злоумышленник получит ID сессии, он получит и всю информацию, которая
в ней хранится.

Есть несколько способов предотвращения атак через cookie:

  • Использовать SSL для создания защищённого соединения и передавать cookie только
    через него. Атакующий не сможет расшифровать содержимое передаваемых cookie.
  • Вовремя объявлять сессию устаревшей, включая все cookie и маркеры сессии,
    для того, чтобы снизить возможность атаки.
  • Предотвратить XSS, тем самым исключив захват cookie.
  • Проверять данные cookie и определять, изменены ли они.

В Yii реализована проверка на изменения через подсчёт хэша HMAC от значений
cookie.

По умолчанию проверка cookie отключена. Для её включения необходимо в
конфигурации приложения
настроить компонент [CHttpRequest] следующим образом:

[php]
return array(
    'components'=>array(
        'request'=>array(
            'enableCookieValidation'=>true,
        ),
    ),
);

При использовании проверки cookie, обращаться к ним необходимо через коллекцию
[cookies|CHttpRequest::cookies], а не напрямую через $_COOKIES:

[php]
// Получаем cookie с заданным именем
$cookie=Yii::app()->request->cookies[$name];
$value=$cookie->value;
…
// Отсылаем cookie
$cookie=new CHttpCookie($name,$value);
Yii::app()->request->cookies[$name]=$cookie;

8 Темы оформления

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

В Yii каждая тема представлена как папка, содержащая файлы представлений, макетов и прочих необходимых файлов, таких,
как CSS, JavaScript и пр. Название папки соответственно определяет название темы. Все темы хранятся в папке
WebRoot/themes, при этом быть активной, т.е. использоваться в текущий момент, может только одна из тем.

Tip|Подсказка: Папку, где по умолчанию хранятся темы — WebRoot/themes — можно легко изменить путем
установки свойств [basePath|CThemeManager::basePath] и [baseUrl|CThemeManager::baseUrl]
компонента [themeManager|CWebApplication::themeManager] на желаемые.

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

Для активации темы нужно установить значение [theme|CWebApplication::theme] равным имени соответствующей темы.
Это можно проделать путем конфигурации приложения или
прямо в ходе выполнения в действиях контроллера.

Note|Примечание: Имя темы чувствительно к регистру, и, если попытаться активировать несуществующую тему, свойство
Yii::app()->theme вернет null.

8.2 Создание темы

Содержимое папки с темами должно быть организовано точно так же, как и содержимое базовой директории приложения, то есть, все файлы представлений
должны находиться в папке views, макеты представлений в папке views/layouts, а файлы системных представлений
в папке views/system. Например, если необходимо заменить представление create контроллера PostController на
представление темы classic, нужно сохранить новый файл представления как WebRoot/themes/classic/views/post/create.php.

Для представлений контроллеров в модулях, соответствующие файлы оформленных представлений нужно
также поместить в папку views. Например, если упомянутый выше контроллер PostController входит в модуль forum, необходимо
сохранить файл представления create как WebRoot/themes/classic/views/forum/post/create.php. Если модуль forum является
составной частью другого модуля support, то файл представления должен быть сохранен как WebRoot/themes/classic/views/support/forum/post/create.php.

Note|Примечание: Папка views может содержать данные чувствительные с точки зрения безопасности, поэтому необходимо
ограничить доступ к папке извне сервера.

В момент вызова метода [render|CController::render] или [renderPartial|CController::renderPartial] для отображения
представления происходит обращение к соответствующим файлам представлений и макетов активной темы. Если файлы найдены, начнется
формирование странички, в противном случае, будут использоваться файлы оформления по умолчанию, месторасположение
которых устанавливается свойствами [viewPath|CController::viewPath] и [layoutPath|CWebApplication::layoutPath].

Tip|Подсказка: Часто в представлениях темы приходится ссылаться на прочие файлы темы, например, для отображения
картинки, находящейся в подпапке темы images. Используя свойство [baseUrl|CTheme::baseUrl] активной темы, можно
сформировать корректную ссылку на картинку следующим образом:
~~~
[php]
Yii::app()->theme->baseUrl . ‘/images/FileName.gif’
~~~

Ниже приведён пример организации директорий приложения с двумя темами basic и fancy:

WebRoot/
    assets
    protected/
        .htaccess
        components/
        controllers/
        models/
        views/
            layouts/
                main.php
            site/
                index.php
    themes/
        basic/
            views/
                .htaccess
                layouts/
                    main.php
                site/
                    index.php
        fancy/
            views/
                .htaccess
                layouts/
                    main.php
                site/
                    index.php

В настройках приложения, если мы будем использовать:

[php]
return array(
    'theme'=>'basic',
    …
);

то будет применяться тема basic. То есть главный макет (layout) будет браться из
themes/basic/views/layouts, а представление index — из themes/basic/views/site.
Если файл представления не найден в теме, будет использован файл из
protected/views.

8.3 Темизация виджетов

Начиная с версии 1.1.5, отображения, используемые в виджетах, можно темизировать.
При вызове [CWidget::render()] для вывода отображения, Yii сделает попытку найти
его в темах перед тем, как загрузить из директории виджета.

Для темизации отображения xyz виджета с именем класса Foo, необходимо создать
директорию Foo (с тем же именем, что и у класса) внутри директории с отображениями
активной темы. Если класс виджета находится в пространстве имён (начиная с PHP 5.3.0),
таком как \app\widgets\Foo, то необходимо создать директорию app_widgets_Foo.
В имени мы заменяем разделители пространства имён на подчёркивание.

После этого создаём файл отображения xyz.php в только что добавленной директории.
К этому моменту мы имеем файл themes/basic/views/Foo/xyz.php, который и будет использоваться
виджетом вместо его собственного отображения, если активная тема — basic.

8.4 Глобальная настройка виджетов

Note|Примечание: данная возможность доступна с версии 1.1.3.

При использовании виджета, как стандартного, так и стороннего, часто требуется
его настройка. К примеру, может понадобиться изменить значение
[CLinkPager::maxButtonCount] с 10 (по умолчанию) на 5. Мы можем сделать это,
передав начальные значения при вызове [CBaseController::widget] для создания
виджета. Тем не менее, делать это везде, где мы используем [CLinkPager] довольно
неудобно.

[php]
$this->widget('CLinkPager', array(
    'pages'=>$pagination,
    'maxButtonCount'=>5,
    'cssFile'=>false,
));

При использовании глобальной настройки, необходимо указать начальные значения
лишь в одном месте — в файле конфигурации приложения. Для этого настраиваем
[widgetFactory|CWebApplication::widgetFactory] следующим образом:

[php]
return array(
    'components'=>array(
        'widgetFactory'=>array(
            'widgets'=>array(
                'CLinkPager'=>array(
                    'maxButtonCount'=>5,
                    'cssFile'=>false,
                ),
                'CJuiDatePicker'=>array(
                    'language'=>'ru',
                ),
            ),
        ),
    ),
);

Выше мы указали глобальные настройки виджетов [CLinkPager] и [CJuiDatePicker]
при помощи соответствующих свойств [CWidgetFactory::widgets]. Стоит отметить, что
глобальные настройки указываются в виде пар ключ-массив значений, где
ключ соответствует классу виджета, а массив значений задаёт начальные значения
свойств этого класса.

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

[php]
$this->widget('CLinkPager', array(
    'pages'=>$pagination,
));

Мы можем переопределить начальные значения, если в этом есть необходимость.
К примеру, если в каком-нибудь отображении мы хотим задать maxButtonCount
равным 2, можно сделать следующее:

[php]
$this->widget('CLinkPager', array(
    'pages'=>$pagination,
    'maxButtonCount'=>2,
));

8.5 Скины

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

Скин — это массив пар имя-значение, который может использоваться для
инициализации свойств виджета. Скин принадлежит классу виджета, а класс виджета
может иметь несколько скинов, идентифицируемых по имени. Например, у нас может
быть скин classic для виджета [CLinkPager].

Для использования данной возможности нам, в первую очередь, необходимо изменить
файл конфигурации приложения, выставив свойство [CWidgetFactory::enableSkin]
компонента widgetFactory в true:

[php]
return array(
    'components'=>array(
        'widgetFactory'=>array(
            'enableSkin'=>true,
        ),
    ),
);

В версиях Yii до 1.1.3 необходимо использовать следующую конфигурацию:

[php]
return array(
    'components'=>array(
        'widgetFactory'=>array(
            'class'=>'CWidgetFactory',
        ),
    ),
);

Затем мы создаём необходимые скины. Скины, принадлежащие одному классу виджета,
хранятся в одном файле PHP, имя которого совпадает с названием класса виджета.
Все файлы скинов по умолчанию хранятся в директории protected/views/skins. Для
изменения директории надо настроить свойство skinPath компонента
widgetFactory. Например, мы можем создать в директории
protected/views/skins файл CLinkPager.php, код которого представлен ниже:

[php]
<?php
return array(
    'default'=>array(
        'nextPageLabel'=>'next',
        'prevPageLabel'=>'prev',
    ),
    'classic'=>array(
        'header'=>'',
        'maxButtonCount'=>5,
    ),
);

В коде выше мы создаём для виджета [CLinkPager] два скина: default и
classic. Первый скин будет применяться к любому виджету [CLinkPager], в
котором явно не указано свойство skin, а второй — к виджету, свойство skin
которого имеет значение classic. Например, в следующем коде представления
первым виджет будет использовать скин default, а второй — скин classic:

[php]
<?php $this->widget('CLinkPager'); ?>

<?php $this->widget('CLinkPager', array('skin'=>'classic')); ?>

Если мы создаём виджет с набором первоначальных значений, они будут
иметь приоритет и будут объединены с любыми применяемыми скинами. Например,
следующий код представления создаст постраничную разбивку, чьи первоначальные значения —
это массив array('header'=>'', 'maxButtonCount'=>6, 'cssFile'=>false),
который является результатом слияния первоначальных значений,
указанных в представлении, и скина classic.

[php]
<?php $this->widget('CLinkPager', array(
    'skin'=>'classic',
    'maxButtonCount'=>6,
    'cssFile'=>false,
)); ?>

Заметим, что скинизация НЕ требует использования темы. Однако, если тема
активна, Yii также будет искать скины в директории skins представлений темы
(например, WebRoot/themes/classic/views/skins). В случае, если скин с таким
же именем существует и в директории представления темы и в основной директории
представления приложения, скин темы будет иметь приоритет.

Если виджет использует несуществующий скин, Yii по-прежнему будет создавать виджет как обычно, без каких-либо ошибок.

Info|Информация: Использование скина может привести к снижению производительности, поскольку Yii должен найти файл скина, когда виджет создается впервые.

Использование скинов очень похоже на глобальную конфигурацию виджетов. Главные
отличия следующие:

  • Скины в большей степени относятся к изменению свойств, отвечающих за внешний
    вид виджета;
  • У виджета может быть несколько скинов;
  • Скин можно темизировать;
  • Использование скинов более затратно, чем использование глобальной конфигурации.
    Управление URL
    ==============

Управление URL-адресами в веб-приложениях включает в себя два аспекта:

  1. Приложению необходимо разобрать запрос пользователя, поступающий в виде URL,
    на отдельные параметры.
  2. Приложение должно предоставлять способ формирования адресов URL, с которыми
    оно сможет корректно работать.

В приложениях на Yii эти задачи решаются с использованием класса [CUrlManager].

Note|Примечание: Вы можете не пользоваться Yii для генерации URL, однако,
так делать не рекомендуется, потому как вы не сможете легко поменять URL
приложения через конфигурацию без изменения кода.

8.6 Создание адресов URL

В принципе, адреса URL можно задать прямо в коде представлений контроллера,
однако куда удобнее создавать их динамически:

[php]
$url=$this->createUrl($route,$params);

где $this относится к экземпляру контроллера; $route соответствует маршруту запроса,
а $params является списком параметров GET для добавления к URL.

По умолчанию, адреса создаются посредством [createUrl|CController::createUrl] в get-формате.
Например, при значениях параметров $route='post/read' и $params=array('id'=>100), получим такой URL:

/index.php?r=post/read&id=100

где параметры указаны в виде набора пар имя=значение, соединенных знаком &, а
параметр r указывает на маршрут. Однако, этот формат не очень
дружелюбен по отношению к пользователю.

Tip|Подсказка: Для того, чтобы сгенерировать URL с хештегом, к примеру,
/index.php?r=post/read&id=100#title, необходимо передать параметр #
следующим образом: $this->createUrl('post/read',array('id'=>100,'#'=>'title')).

Мы можем сделать так, чтобы адрес, приведенный в качестве примера выше, выглядел более аккуратно и понятно
за счет использования формата path, который исключает использование
строки запроса и включает все GET-параметры в информационную часть адреса URL:

/index.php/post/read/id/100

Для изменения формата представления адреса URL, нужно настроить компонент приложения [urlManager|CWebApplication::urlManager]
таким образом, чтобы метод [createUrl|CController::createUrl] мог автоматически переключиться на использование нового формата, а
приложение могло корректно воспринимать новый формат адресов URL:

[php]
array(
    …
    'components'=>array(
        …
        'urlManager'=>array(
            'urlFormat'=>'path',
        ),
    ),
);

Обратите внимание, что указывать класс компонента [urlManager|CWebApplication::urlManager] не требуется, т.к.
он уже объявлен как [CUrlManager] в [CWebApplication].

Tip|Подсказка: Адрес URL, генерируемый методом [createUrl|CController::createUrl] является относительным. Для того, чтобы получить
абсолютный адрес, нужно добавить префикс, используя Yii::app()->request->hostInfo, или вызвать метод
[createAbsoluteUrl|CController::createAbsoluteUrl].

8.7 Человекопонятные URL

Если в качестве формата адреса URL используется path, то мы можем определить правила формирования URL, чтобы
сделать адреса более привлекательными и понятными с точки зрения пользователя. Например, мы можем использовать
короткий адрес /post/100 вместо длинного варианта /index.php/post/read/id/100. [CUrlManager] использует правила формирования URL
как для создания, так и для обработки адресов.

Правила формирования URL задаются путем конфигурации свойства [rules|CUrlManager::rules] компонента приложения
[urlManager|CWebApplication::urlManager]:

[php]
array(
    …
    'components'=>array(
        …
        'urlManager'=>array(
            'urlFormat'=>'path',
            'rules'=>array(
                'pattern1'=>'route1',
                'pattern2'=>'route2',
                'pattern3'=>'route3',
            ),
        ),
    ),
);

Правила задаются в виде массива пар шаблон-путь, где каждая пара соответствует одному правилу.
Шаблон правила — строка, которая должна совпадать с путём в URL. Путь правила должен
указывать на существующий путь контроллера.

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

[php]
'pattern1'=>array('route1', 'urlSuffix'=>'.xml', 'caseSensitive'=>false)

Начиная с версии 1.1.7, можно использовать показанный ниже формат. То есть
паттерн указывается как элемент массива, что позволяет указать несколько правил
одного паттерна:

[php]
array('route1', 'pattern'=>'pattern1', 'urlSuffix'=>'.xml', 'caseSensitive'=>false)

Здесь массив содержит список дополнительных параметров для правила. Возможно
указать следующие параметры:

  • и создании URL. Данная возможность доступна с версии 1.1.7.

  • По умолчанию равен null, что означает использование значения [CUrlManager::urlSuffix].

  • равен null, что означает использование значения [CUrlManager::caseSensitive].

  • [defaultParams|CUrlRule::defaultParams]: GET-параметры по умолчанию (имя=>значение) для данного правила.
    При срабатывании правила параметры будут добавлены в $_GET.

  • с соответствующими подвыражениями в основном правиле. По умолчанию параметр равен null,
    что означает использование значения [CUrlManager::matchValue]. При значении параметра false
    правило будет использовано для создания URL только если имена параметров совпадают с именами в правиле.
    При значении true значения параметров дополнительно должны совпадать с подвыражениями в правиле.
    Стоит отметить, что установка значения в true снижает производительность.

  • [verb|CUrlRule::verb]: тип HTTP запроса (например, GET, POST, DELETE),
    для которого работает данное правило. По умолчанию равен null, что означает
    работу правила с любыми HTTP запросами. Если необходимо указать несколько
    типов запросов, их надо разделить запятыми. В том случае, когда правило не
    совпадает с текущим типом запроса, оно пропускается на этапе разбора запроса.
    Данная опция используется только для разбора запроса и введена для поддержки
    URL в стиле REST. Данная возможность доступна с версии 1.1.7.

  • этапе разбора запроса. По умолчанию параметр равен false, что означает, что
    правило используется как для разбора запроса, так и для построения URL.
    Данная возможность доступна с версии 1.1.7.

8.8 Использование именованных параметров

Правило может быть ассоциировано с несколькими GET-параметрами. Эти параметры
указываются в шаблоне правила в виде маркеров следующим образом:

<ParamName:ParamPattern>

где ParamName соответствует имени GET-параметра, а необязательный
ParamPattern — регулярному выражению, которое используется для проверки
соответствия значению GET-параметра. Если ParamPattern не указан, то
параметр должен соответствовать любым символам, кроме слэша /. В момент
создания URL маркеры будут заменены на соответствующие значения параметров,
а в момент обработки URL, соответствующим GET-параметрам будут
присвоены результаты обработки.

Для наглядности приведем несколько примеров. Предположим, что наш набор правил состоит из трех правил:

[php]
array(
    'posts'=>'post/list',
    'post/<id:\d+>'=>'post/read',
    'post/<year:\d{4}>/<title>'=>'post/read',
)
  • Вызов $this->createUrl('post/list') сгенерирует /index.php/posts. Здесь было применено первое правило.

  • Вызов $this->createUrl('post/read',array('id'=>100)) сгенерирует /index.php/post/100. Применено второе правило.

  • Вызов $this->createUrl('post/read',array('year'=>2008,'title'=>'a sample post')) сгенерирует /index.php/post/2008/a%20sample%20post.
    Использовано третье правило.

  • Вызов $this->createUrl('post/read') сгенерирует
    /index.php/post/read. Ни одно из правил не было применено.

При использовании [createUrl|CController::createUrl] для генерации адреса URL, маршрут и GET-параметры, переданные
методу, используются для определения правила, которое нужно применить. Правило применяется в том случае, когда все параметры, ассоциированные с правилом,
присутствуют среди GET-параметров, а маршрут соответствует параметру маршрута.

Если же количество GET-параметров больше, чем требует правило, то лишние параметры
будут включены в строку запроса. Например, если вызвать $this->createUrl('post/read',array('id'=>100,'year'=>2008)),
мы получим /index.php/post/100?year=2008. Для того, чтобы лишние параметры были отражены в информационной части пути, необходимо
добавить к правилу /*. Таким образом, используя правило post/<id:\d+>/* получим URL вида /index.php/post/100/year/2008.

Как уже говорилось, вторая задача правил URL — разбирать URL-запросы. Этот процесс обратный процессу создания URL.
Например, когда пользователь запрашивает /index.php/post/100, применяется второе правило из примера выше и запрос
преобразовывается в маршрут post/read и GET-параметр array('id'=>100) (доступный через $_GET).

Note|Примечание: Использование правил URL снижает производительность приложения.
Это происходит по той причине, что в процессе парсинга запрошенного URL
[CUrlManager] пытается найти соответствие каждому правилу до тех пор, пока
какое-нибудь из правил не будет применено. Чем больше правил, тем больший урон
производительности. Поэтому в случае высоконагруженных приложений использование
правил URL стоит минимизировать.

8.9 Параметризация маршрутов

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

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

[php]
array(
    '<_c:(post|comment)>/<id:\d+>/<_a:(create|update|delete)>' => '<_c>/<_a>',
    '<_c:(post|comment)>/<id:\d+>' => '<_c>/read',
    '<_c:(post|comment)>s' => '<_c>/list',
)

Мы использовали два именованных параметра в маршруте правил: _c и _a.
Первый соответствует названию контроллера и может быть равен post или comment,
второй — названию action-а и может принимать значения create, update или delete.
Вы можете называть параметры по-другому, если их имена не конфликтуют с GET-параметрами,
которые могут использоваться в URL.

При использовании правил, приведённых выше, URL /index.php/post/123/create
будет обработано как маршрут post/create с GET-параметром id=123.
По маршруту comment/list с GET-параметром page=2, мы можем создать URL
/index.php/comments?page=2.

8.10 Параметризация имён хостов

Также возможно использовать имена хостов в правилах для разбора и
создания URL. Можно выделять часть имени хоста в GET-параметр.
Например, URL http://admin.example.com/en/profile может быть разобран в
GET-параметры user=admin и lang=en. С другой стороны, правила с именами
хостов могут также использоваться для создания URL адресов.

Чтобы использовать параметризованные имена хостов, включите имя хоста в правила URL:

[php]
array(
    'http://<user:\w+>.example.com/<lang:\w+>/profile' => 'user/profile',
)

Пример выше говорит, что первый сегмент имени хоста должен стать
параметром user, а первый сегмент пути — параметром lang. Правило
соответствует маршруту user/profile.

Помните, что [CUrlManager::showScriptName] не работает при создании URL
адреса с использованием правил с параметризованным именем хоста.

Стоит отметить, что правило с параметризованным именем хоста не должно
содержать поддиректорий в том случае, если приложение находится в поддиректории
корня вебсервера. К примеру, если приложение располагается по адресу
http://www.example.com/sandbox/blog, мы должны использовать точно такое же
правило URL, как описано выше. Без поддиректории: sandbox/blog.

8.11 Скрываем index.php

С целью сделать адрес URL еще более привлекательным можно спрятать имя входного скрипта index.php. Для этого необходимо настроить
веб-сервер и компонент приложения [urlManager|CWebApplication::urlManager].

Вначале сконфигурируем веб-сервер таким образом, чтобы адрес URL без указания имени входного скрипта по-прежнему
передавался на обработку входному скрипту. Для сервера Apache HTTP server это достигается
путем включения механизма преобразования URL и заданием нескольких правил. Для этого
необходимо создать файл /wwwroot/blog/.htaccess, содержащий правила, приведённые ниже.
Те же правила могут быть размещены в файле конфигурации Apache
в секции Directory для /wwwroot/blog.

RewriteEngine on

# if a directory or a file exists, use it directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

# otherwise forward it to index.php
RewriteRule . index.php

Далее нужно установить свойство [showScriptName|CUrlManager::showScriptName] компонента
[urlManager|CWebApplication::urlManager] равным false.

Теперь, вызвав $this->createUrl('post/read',array('id'=>100)), мы получим URL /post/100. Что важно, этот
адрес URL будет корректно распознан нашим веб-приложением.

8.12 Подмена окончания в адресе URL

В дополнение ко всему перечисленному выше, мы можем добавить к нашим адресам URL окончание. Например,
мы можем получить /post/100.html вместо /post/100, представив пользователю как будто бы статичную страничку.
Для этого нужно просто настроить компонент [urlManager|CWebApplication::urlManager] путем назначения свойству
[urlSuffix|CUrlManager::urlSuffix] любого желаемого окончания.

8.13 Использование своего класса правила URL

Note|Примечание: данная возможность доступна с версии 1.1.8.

По умолчанию каждое правило URL для [CUrlManager] представлено объектом
класса [CUrlRule]. Этот объект разбирает запросы и создаёт URL по заданному правилу.
Несмотря на то, что [CUrlRule] достаточно гибок и подходит для работы с большинством
форматов URL, иногда требуются какие-либо особенные возможности.

К примеру, для сайта по продаже автомобилей может потребоваться поддерживать URL
вида /Производитель/Модель, где и Производитель и Модель должны соответствовать
данным из определённой таблицы базы данных. В этом случае класс [CUrlRule] не
подойдёт так как он, в основном, работает с статически описанными регулярными
выражениями, а не с базой данных.

В данном случае можно реализовать новый класс правила URL, унаследовав [CBaseUrlRule],
и использовать его в одном или нескольких правилах. Для приведённого выше примера
с продажей автомобилей подойдут следующие правила URL:

[php]
array(
    // стандартное правило для обработки '/' как 'site/index'
    '' => 'site/index',

    // стандартное правило для обработки '/login' как 'site/login' и т.д.
    '<action:(login|logout|about)>' => 'site/<action>',

    // своё правило для URL вида '/Производитель/Модель'
    array(
        'class' => 'application.components.CarUrlRule',
        'connectionID' => 'db',
    ),

    // стандартное правило для обработки 'post/update' и др.
    '<controller:\w+>/<action:\w+>' => '<controller>/<action>',
),

Выше мы использовали свой класс правила URL CarUrlRule для обработки URL
вида /Производитель/Модель. Данный класс может быть реализован следующим образом:

[php]
class CarUrlRule extends CBaseUrlRule
{
    public $connectionID = 'db';

    public function createUrl($manager,$route,$params,$ampersand)
    {
        if ($route==='car/index')
        {
            if (isset($params['manufacturer'], $params['model']))
                return $params['manufacturer'] . '/' . $params['model'];
            else if (isset($params['manufacturer']))
                return $params['manufacturer'];
        }
        return false;  // не применяем данное правило
    }

    public function parseUrl($manager,$request,$pathInfo,$rawPathInfo)
    {
        if (preg_match('%^(\w+)(/(\w+))?$%', $pathInfo, $matches))
        {
            // Проверяем $matches[1] и $matches[3] на предмет
            // соответствия производителю и модели в БД.
            // Если соответствуют, выставляем $_GET['manufacturer'] и/или $_GET['model']
            // и возвращаем строку с маршрутом 'car/index'.
        }
        return false;  // не применяем данное правило
    }
}

Свой класс правила URL должен реализовать два абстрактных метода, объявленных в
[CBaseUrlRule]:

  • [createUrl()|CBaseUrlRule::createUrl()]
  • [parseUrl()|CBaseUrlRule::parseUrl()]

Кроме показанного выше типичного использования, свой класс URL может пригодиться
и в других ситуациях. Например, можно реализовать класс правила, который будет
записывать в журнал разбираемые и создаваемые URL. Это может пригодиться на
этапе разработки. Также можно реализовать класс, который показывает особую страницу
ошибки 404 в том случае, когда все остальные правила для разбираемого URL не сработали.
Стоит отметить, что в этом случае правило с этим специальным классом должно
указываться последним в списке.
Веб-сервисы
===========

Веб-сервис — программная система,
разработанная для обеспечения взаимодействия между несколькими компьютерами через
сеть. В веб-приложении это обычно набор API, который можно использовать через
интернет для выполнения действий на удалённом сервере, обслуживающем веб-сервис.
К примеру, клиент, основанный на Flex, может
вызывать функции, реализованные на сервере в PHP-приложении. В качестве базового
уровня протокола используется SOAP.

Для того, чтобы упростить задачу создания веб-сервиса, в Yii включены
[CWebService] и [CWebServiceAction]. API сгруппированы по классам, которые называются
провайдерами. Для каждого класса Yii генерирует WSDL,
описывающий функционал предоставляемого API и правила его использования клиентом.
При обработке вызова клиента, Yii создаёт соответствующий ему экземпляр провайдера,
вызывает метод API и отвечает на запрос.

Note|Примечание: Для работы [CWebService] требуется
расширение PHP SOAP. Убедитесь, что
оно включено, прежде, чем пробовать примеры, описанные далее.

8.14 Создание провайдера

Как уже было описано, провайдер — это класс, реализующий методы, которые могут
быть вызваны удалённо. Для того, чтобы определить, какие методы могут быть
вызваны удалённо и какое значение возвращать, Yii использует
специальные комментарии и
reflection.

Попробуем реализовать простой сервис, отдающий информацию о котировках акций
определённой компании. Для этого нам потребуется реализовать провайдер, как показано
ниже. Стоит отметить, что наследуем класс провайдера StockController
от [CController]. Наследование не является обязательным.

[php]
class StockController extends CController
{
    /**
     * @param string индекс предприятия
     * @return float цена
     * @soap
     */
    public function getPrice($symbol)
    {
        $prices=array('IBM'=>100, 'GOOGLE'=>350);
        return isset($prices[$symbol])?$prices[$symbol]:0;
        //…возвращаем цену для компании с индексом $symbol
    }
}

Выше мы описали, что метод getPrice является частью API веб-сервиса, пометив его
в комментарии тэгом @soap. Там же мы описали типы параметров и возвращаемого
значения. Дополнительные методы API могут быть описаны точно таким же образом.

8.15 Реализация действия веб-сервиса

После создания провайдера необходимо сделать его доступным для клиентов.
Для этого необходимо описать действие контроллера [CWebServiceAction].
В нашем примере мы используем StockController:

[php]
class StockController extends CController
{
    public function actions()
    {
        return array(
            'quote'=>array(
                'class'=>'CWebServiceAction',
            ),
        );
    }

    /**
     * @param string индекс предприятия
     * @return float цена
     * @soap
     */
    public function getPrice($symbol)
    {
        //…возвращаем цену для компании с индексом $symbol
    }
}

Это всё, что требуется для создания веб-сервиса. Теперь при обращении к URL
http://hostname/path/to/index.php?r=stock/quote, мы получим объёмистый
XML, на самом деле являющийся WSDL описанного нами веб-сервиса.

Tip|Подсказка: По умолчанию, при использовании [CWebServiceAction]
подразумевается, что текущий контроллер является провайдером. Именно поэтому мы
определили метод getPrice в классе StockController.

8.16 Использование веб-сервиса

Для того, чтобы наш пример был полным, создадим клиент, использующий веб-сервис,
который мы только что создали. В примере клиент будет написан на PHP, но для его
реализации можно использовать и другие языки, такие как Java, C#, Flex и т.д.

[php]
$client=new SoapClient('http://hostname/path/to/index.php?r=stock/quote');
echo $client->getPrice('GOOGLE');

Запустив данный скрипт через браузер или в консоли, вы должны получить 350,
что соответствует цене акций GOOGLE.

8.17 Типы данных

При описании методов и свойств класса, которые должны быть доступны через
веб-сервис, нам необходимо определить типы параметров и возвращаемых значений.
Для этого могут быть использованы следующие типы:

  • str/string: соответствует xsd:string;
  • int/integer: соответствует xsd:int;
  • float/double: соответствует xsd:float;
  • bool/boolean: соответствует xsd:boolean;
  • date: соответствует xsd:date;
  • time: соответствует xsd:time;
  • datetime: соответствует xsd:dateTime;
  • array: соответствует xsd:string;
  • object: соответствует xsd:struct;
  • mixed: соответствует xsd:anyType.

Если тип не является одним из приведённых выше, он воспринимается как
составной тип, состоящий из свойств. Этот тип соответствует классу, а его
свойства — public-переменным класса, отмеченных в комментариях @soap.

Также можно использовать массивы. Для этого необходимо дописать [] в конец
примитивного или составного типа. Таким образом мы получим массив с элементами
заданного типа.

Ниже приведён пример определения метода API getPosts, возвращающего массив
объектов класса Post.

[php]
class PostController extends CController
{
    /**
     * @return Post[] список записей
     * @soap
     */
    public function getPosts()
    {
        return Post::model()->findAll();
    }
}

class Post extends CActiveRecord
{
    /**
     * @var integer ID записи
     * @soap
     */
    public $id;
    /**
     * @var string заголовок записи
     * @soap
     */
    public $title;

    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
}

8.18 Сопоставление классов

Для получения от клиента параметров составного типа, в приложении должны быть
заданы соответствия типов WSDL классам PHP. Для этого необходимо настроить
свойство [classMap|CWebServiceAction::classMap] класса [CWebServiceAction].

[php]
class PostController extends CController
{
    public function actions()
    {
        return array(
            'service'=>array(
                'class'=>'CWebServiceAction',
                'classMap'=>array(
                    'Post'=>'Post',  // или просто 'Post'
                ),
            ),
        );
    }
    …
}

8.19 Перехват удалённого вызова метода

Если реализован интерфейс [IWebServiceProvider], провайдер может перехватывать
удалённые вызовы методов. Используя [IWebServiceProvider::beforeWebMethod] можно получить
текущий экземпляр [CWebService]. Через [CWebService::methodName] — название
вызываемого метода. Если метод по каким либо причинам (например, отсутствие прав
на его выполнение) не должен быть вызван, необходимо вернуть false.
Обновление с версии 1.0 на версию 1.1
=====================================

8.20 Изменения, связанные со сценариями модели

  • Удалён метод [safeAttributes()|CModel::safeAttributes].
    Теперь безопасными атрибутами являются те, для которых объявлены соответствующие
    правила валидации в методе [rules()|CModel::rules] для конкретного сценария.

  • Изменены методы [validate()|CModel::validate],
    [beforeValidate()|CModel::beforeValidate],
    [afterValidate()|CModel::afterValidate]. Для методов
    [setAttributes()|CModel::setAttributes] и
    [getSafeAttributeNames()|CModel::getSafeAttributeNames]
    параметр ‘scenario’ удалён. Получать и устанавливать сценарий модели теперь необходимо, используя
    свойство [CModel::scenario].

  • Изменён метод [getValidators()|CModel::getValidators] и удалён
    [getValidatorsForAttribute()|CModel::getValidatorsForAttribute].
    CModel::getValidators() теперь возвращает только валидаторы, применяемые к сценарию, определяемому
    свойством сценария модели ([CModel::scenario]).

  • Изменены методы [isAttributeRequired()|CModel::isAttributeRequired] и
    [CModel::getValidatorsForAttribute()]. Параметр сценария удалён. Вместо него
    следует использовать свойство сценария модели.

  • Удалено свойство CHtml::scenario. CHtml теперь использует сценарий, указанный в модели.

8.21 Изменения, связанные с жадной загрузкой для связей Active Record

  • По умолчанию для всех связей, включённых в жадную загрузку, будет
    сгенерировано и выполнено одно выражение с использованием JOIN. Если в основной
    таблице есть опции запроса LIMIT или OFFSET, то сначала будет выполнен этот
    запрос, а затем другой SQL-запрос, который возвратит все связанные объекты.
    Раньше, в версии 1.0.x, по умолчанию выполнялись N+1 SQL-запросов, если
    жадная загрузка включала N связей HAS_MANY или MANY_MANY.

8.22 Изменения, связанные с псевдонимами таблиц в связях Active Record

  • Теперь псевдоним по умолчанию для связанной таблицы такой же, как и
    соответствующее имя связи. Ранее, в версии 1.0.x, по умолчанию Yii
    автоматически генерировал псевдоним таблицы для каждой связанной таблицы,
    и мы должны были использовать префикс ??. для ссылки на этот автоматически
    сгенерированный псевдоним.

  • Псевдоним для главной таблицы в AR запросе теперь всегда равен t.
    В версии 1.0.x он соответствовал имени таблицы. Данное изменение ломает код
    существующих запросов AR в том случае, если в качестве псевдонима было использовано
    имя таблицы. Решение — заменить такие псевдонимы на ‘t.’.

8.23 Изменения, связанные с табличным (пакетным) вводом данных

  • Для имён полей использование записи вида поле[$i] отныне неверно.
    Теперь они должны выглядеть так — [$i]поле, чтобы была возможность поддержки
    множественного ввода однотипных полей (например, [$i]поле[$index]).

8.24 Другие изменения

  • Изменён конструктор [CActiveRecord]. Первый параметр (список атрибутов) удалён.
© https://gittobook.org, 2018. Unauthorized use and/or duplication of this material without express and written permission from this author and/or owner is strictly prohibited. Excerpts and links may be used, provided that full and clear credit is given to this site, with appropriate and specific direction to the original content.
Table