Использование поведений Behavior в Yii

Удивлённое лицо

Это статья про Yii1, но в конце есть ссылка на вебинар по второй версии. А пока...

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

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

class Post extends CActiveRecord
{
    public function behaviors()
    {
        return array(
            'CTimestampBehavior' => array(
                'class'=>'zii.behaviors.CTimestampBehavior',
                'setUpdateOnCreate'=>false,
                'createAttribute'=>'created_at',
                'updateAttribute'=>'updated_at',
            ),
        );
    }
}

...и ваши волосы станут нежными и шелковистыми...

Читатели же, видя это, чаще склоняются к старому доброму

class Post extends CActiveRecord
{
    public function beforeSave()
    {
        if (parent::beforeSave())
        {
            if ($this->isNewRecord)
                $this->created_at = time();
            else
                $this->updated_at = time();
 
            return true;
        } 
        else
            return false;
    }
}

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

class Post extends CActiveRecord
{
    public function beforeSave(){
        $field = $this->isNewRecord ? 'created_at' : 'updated_at';
        return parent::beforeSave() && $this->$field = time();
    }
}

Теперь перейдём к поведениям.

Введение в поведения на примере

В начале поймём суть того, что же называют «поведением».

Внутреннюю работу с поведениями обеспечивает класс CComponent, от которого так или иначе наследуются все классы в Yii. Поэтому не важно, с чем мы будем экспериментировать. Для начала стоит сказать, что в Yii имеется базовый класс поведения CBehavior, его наследник CModelBehavior и наследник второго уровня CActiveRecordBehavior, который дополнен обработчиками событий модели CActiveRecord. Но в первом примере это пока не важно.

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

/**
 * @property string $image
 */
class News extends CActiveRecord {
    const IMAGE_PATH = 'upload/images/news';
}
/**
 * @property string $image
 */
class Post extends CActiveRecord {
    const IMAGE_PATH = 'upload/images/posts';
}

В поле image модели в таблице будет храниться случайно сгенерированное имя файла вроде as0hd3ac.jpg, и рядом с оригиналом будет лежать превью small_as0hd3ac.jpg. Самого процесса загрузки пока касаться не будем.

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

<?php
<a href="<?php echo Yii::app()->request->baseUrl . '/' . Post::IMAGE_PATH . '/' . $model->image; ?>">
    <img src="<?php echo Yii::app()->request->baseUrl . '/' . Post::IMAGE_PATH . '/small_' . $model->image; ?>" alt="" />
</a>

Если кто-то скажет, что вместо вписывания тэга <img src="..." /> более красиво использовать CHtml::image(...) и посоветует написать так:

<?php
<?php echo CHtml::link(
    CHtml::image(Yii::app()->request->baseUrl . '/' . Post::IMAGE_PATH . '/small_' . $model->image), 
    Yii::app()->request->baseUrl . '/' . Post::IMAGE_PATH . '/' . $model->image
); ?>

то мы примем вызов и напишем вообще так:

<?php
<?php echo CHtml::link(CHtml::image($model->imageThumbUrl), $model->imageUrl); ?>

Удобно и красиво.

Для работы этого варианта мы должны добавить соответствующие геттеры в наши модели:

/**
 * @property string $image
 */
class Post extends CActiveRecord 
{
    protected $path = 'upload/images/posts';
 
    public function getImageUrl(){
        return Yii::app()->request->baseUrl . '/' . $this->path . '/' . $this->image;    
    }
 
    public function getImageThumbUrl(){
        return Yii::app()->request->baseUrl . '/' . $this->path . '/small_' . $this->image;    
    }    
}

Вынесем повторяющийся код во вспомогательный приватный метод getBaseImagePath() и скопируем этот же код в другие модели:

/**
 * @property string $image
 */
class Post extends CActiveRecord 
{
    protected $path = 'upload/images/posts';
 
    public function getImageUrl(){
        return $this->getBaseImagePath() . $this->image;    
    }
 
    public function getImageThumbUrl(){
        return $this->getBaseImagePath() . 'small_' . $this->image;    
    }
 
    private function getBaseImagePath(){
        return Yii::app()->request->baseUrl . '/' . $this->path . '/';
    }  
}
/**
 * @property string $image
 */
class News extends CActiveRecord 
{
    protected $path = 'upload/images/news';
 
    public function getImageUrl(){
        return $this->getBaseImagePath() . $this->image;    
    }
 
    public function getImageThumbUrl(){
        return $this->getBaseImagePath() . 'small_' . $this->image;    
    }
 
    private function getBaseImagePath(){
        return Yii::app()->request->baseUrl . '/' . $this->path . '/';
    }    
}

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

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

/**
 * @property string $image
 */
abstract class ImageModel extends CActiveRecord 
{
    protected $path = '';
 
    public function getImageUrl(){
        return $this->getBaseImagePath() . $this->image;    
    }
 
    public function getImageThumbUrl(){
        return $this->getBaseImagePath() . 'small_' . $this->image;    
    }
 
    private function getBaseImagePath(){
        return Yii::app()->request->baseUrl . '/' . $this->path . '/';
    }  
}
class Post extends ImageModel 
{
    protected $path = 'upload/images/posts';
}
class News extends ImageModel 
{
    protected $path = 'upload/images/news';
}

Тогда эти методы появятся в каждой унаследованной от класса ImageModel модели. Задача решена.

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

$category = Category::model()->findByAttributes(array('alias'=>$alias));

мы можем добавить в модель категории метод findByAlias():

$category = Category::model()->findByAlias($alias);

А если у нас иерархические категории, то и findByPath():

$path = 'programming/flash/games';
$category = Category::model()->findByPath($path);

Аналогично оба метода можно вынести в базовый класс CategoryModel и унаследовать нужные от него.

А теперь представим ситуацию, что в категории интернет-магазина тоже нужно загружать изображения. Мы могли бы это осуществить, унаследовав модель категории магазина от двух классов сразу: от ImageModel и CategoryModel, но PHP прямым образом не позволит нам этого сделать.

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

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

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

Перенос кода модели в поведение

Как мы упоминали, в Yii имеются три класса для поведений. Чаще используются универсальный CBehavior и унаследованный от него CActiveRecordBehavior.

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

class ImageBehavior extends CActiveRecordBehavior
{
    public $imagePath = '';
    public $imageField = '';
 
    public function getImageUrl(){
        return $this->getBaseImagePath() . $this->owner->{$this->imageField};    
    }
 
    public function getImageThumbUrl(){
        return $this->getBaseImagePath() . 'small_' . $this->owner->{$this->imageField};    
    }
 
    private function getBaseImagePath(){
        return Yii::app()->request->baseUrl . '/' . $this->imagePath . '/';
    }  
}

Обратите внимание, что $this в поведении ссылается на сам объект поведения. Чтобы обратиться к модели, к которой привязано поведение, нужно использовать $this->owner или $this->getOwner().

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

Поведения у нас теперь есть, теперь мы можем «прицепить» их к любой нашей модели:

class Post extends CActiveRecord 
{
    public function behaviors(){
        return array(
            'imageBehavior' => array(
                'class' => 'ImageBehavior',
                'imagePath' => 'upload/images/posts',
                'imageField' => 'image',
            ),
        );
    }
}
class Category extends CActiveRecord 
{
    public function behaviors(){
        return array(
            'imageBehavior' => array(
                'class' => 'ImageBehavior',
                'imagePath' => 'upload/images/categories',
                'imageField' => 'image',
            ),
            'categoryBehavior' => array(
                'class' => 'CategoryBehavior',
                'aliasField' => 'alias',
            ),
        );
    }
}

Имена поведений в строках

'imageBehavior' => array(...),
'categoryBehavior' => array(...),

чисто случайны. Их можно называть как угодно.

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

$model = Post::model()->findByPk($id);
 
echo $model->getImageUrl();
echo $model->imageBehavior->getImageUrl();

Теперь рассмотрим процесс обработки событий модели CActiveRecord в поведении.

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

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

Пусть у нас есть модель с кодом для загрузки файлов:

class Post extends CActiveRecord 
{
    protected $filePath = 'upload/files';
 
    public function rules(){
        return array(
            array('file', 'file', 'types'=>'doc,xls,pdf', 'allowEmpty'=>true, 'safe'=>false),
        );
    }
 
    protected function beforeSave(){
        if (parent::beforeSave())
        {
            if ($file = CUploadedFile::getInstance($this, 'file')){
                $this->deleteFile();
                $file->saveAs($this->filePath . '/' . $file->name);
                $this->file = $file->name;
            }
            return true;
        }
        return false;
    }
 
    protected function beforeDelete(){
        if (parent::beforeDelete())
        {
            $this->deleteFile();
            return true;
        }
        return false;
    }
 
    public function deleteFile(){
        unlink($this->filePpath . '/' . $this->file);
        $this->file = '';
    }    
 
    public function getFileUrl(){
        return Yii::app()->request->baseUrl . '/' . $this->filePath . '/' . $this->file;
    }  
}

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

class CActiveRecordBehavior extends CModelBehavior
{
    ...
 
    public function events()
    {
        return array_merge(parent::events(), array(
            'onBeforeSave'=>'beforeSave',
            'onAfterSave'=>'afterSave',
            'onBeforeDelete'=>'beforeDelete',
            'onAfterDelete'=>'afterDelete',
            'onBeforeFind'=>'beforeFind',
            'onAfterFind'=>'afterFind',
        ));
    }
 
    protected function beforeSave($event)
    {
    }
 
    protected function afterSave($event)
    {
    }
 
    ...
}

Чобы выполнить какой-либо процесс при наступлении определённого события, нам достаточно перекрыть нужный метод.

Мы можем безболезненно перенести наш код в отдельный класс поведения. При этом учтём, что вместо $this нужно для обращения к самой модели использовать $this->owner. В отличие от методов модели, в поведении нам не нужно возвращать true из методов beforeSave() и beforeDelete(). Также эти методы в поведении должны быть публичными:

class FileBehavior extends CActiveRecordBehavior 
{
    public $filePath = '';
    public $fileField = 'file';
 
    public function beforeSave($event){
        if ($file = CUploadedFile::getInstance($this->owner, $this->fileField)){
            $this->deleteFile();
            $file->saveAs($this->filePath . '/' . $file->name);
            $this->owner->{$this->fileField} = $file->name;
        }
    }
 
    public function beforeDelete($event){
        $this->deleteFile();
    }
 
    public function deleteFile(){
        unlink($this->filePath . '/' . $this->owner->{$this->fileField});
        $this->owner->{$this->fileField} = '';
    }
 
    public function getFileUrl(){
        return Yii::app()->request->baseUrl . '/' . $this->filePath . '/' . $this->owner->{$this->fileField};
    }  
}

Теперь код нашей модели стал таким:

class Post extends CActiveRecord 
{
    protected $filePath = 'upload/files';
 
    public function rules(){
        return array(
            array('file', 'file', 'types'=>'doc,xls,pdf', 'allowEmpty'=>true, 'safe'=>false),
        );
    }
 
    public function behaviors(){
        return array(
            'fileBehavior' => array(
                'class' => 'FileBehavior',
                'filePath' => 'upload/images/posts',
                'fileField' => 'file',
            ),
        );
    }    
}

В момент подключения поведения к компоненту (в нашем случае к модели) внутри него вызывается метод attach(), в который передаётся сама модель параметром $owner. Этот метод можно переопределить и произвести в нём, например, динамическое добавление правил валидации в модель для поля file. Именно эти процессы производятся в полноценном поведении для загрузки файлов рецепта из официального руководства.

Подключение поведений к произвольным компонетам

Мы научились прикреплять поведения к моделям CActiveRecord. Аналогично используя тот же метод behaviors() мы можем подключать их и к компонентам, производным от классов CController и CFormModel.

Например, в вышеприведённом уроке мы подключили поведение к базовому контроллеру, чтобы во всех контроллерах стал доступным метод decodeWidgets().

Похожий функционал реализует и класс CApplicationComponent, от которого наследуются компоненты нашего приложения. Вместо метода behaviors() он содержит свойство behaviors, которому можно присвоить массив поведений прямо в конфигурационном файле:

class MyBehavior extends CBehavior
{
    public function updateLoginAt()
    {
        if ($this->owner->id)
            User::model()->updateByPk($this->owner->id, array('last_login_at'=>time()));   
    }
}
return array(
    'components' => array(    
        ...        
        'user' => array(
            'allowAutoLogin' => true,
            'loginUrl' => array('/site/login'),
            'behaviors' => array (
                'myBehavior' => array(
                    'class' => 'MyBehavior',
                ),
            ),
        ),
    ),
);

Теперь все методы класса MyBehavior станут доступными внутри экземпляра класса CWebUser, то есть мы в контроллере или в любом другом месте можем вызвать:

Yii::app()->user->updateLoginAt();

и это сработает.

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

return array(
    'components'=>array(...),
 
    'behaviors' => array (
        'myBehavior' => array(
            'class' => 'MyBehavior',
        ),
    )
);

Это понадобилось нам, например, для подключения маршрутов модулей в рамках события onBeginRequest приложения. По аналогии с исходным кодом CActiveRecordBehavior мы должны указать отслеживаемые события в перекрытом методе events() и вписать свой код в метод beginRequest():

class MyBehavior extends CBehavior
{
    public function events()
    {
        return array_merge(parent::events(),array(
            'onBeginRequest'=>'beginRequest',
        ));
    }
 
    public function beginRequest($event)
    {
        ...
    }
}

Теперь наш код будет выполняться перед процессом разбора текущего URL.

Динамическое подключение поведений

Все рассматриваемые нами примеры реализовывали статическое подключение поведений (их список жёстко прописывался в коде). При создании экземпляра компонента автоматически создаются и экземпляры всех поведений.

Например, вызов

$posts = Post::model()->findAll()

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

В этом ничего страшного нет, так как класс ImageBehavior достаточно «лёгкий» и его методы для модели нужны всегда.

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

Для этих целей можно напрямую использовать методы класса CComponent:

// добавление нескольких поведений
attachBehaviors($behaviors)
 
// удаление всех добавленных
detachBehaviors()
 
// добавление одного
attachBehavior($name, $behavior)
 
// удаление какого-либо одного
detachBehavior($name)
 
// включение всех добавленных
enableBehaviors()
 
// отключение всех добавленных
disableBehaviors()
 
// включение одного
enableBehavior($name)
 
// отключение одного
disableBehavior($name)

Пусть у нас есть поведение для фильтрации текста:

class MyBehavior extends CActiveRecordBehavior
{
    public function beforeSave($event){
        $model = $this->getOwner();
        $model->text = $this->processText($model->text);
    }
 
    private function processText($text){
        return ...;
    }
}

Мы не будем его описывать в модели:

class Post extends СActiveRecord {
 
}

Вместо этого мы модифицируем экшен создания записи:

class PostController extends Ccontroller
{
    public function actionCreate()
    {
        $model = new Post();
 
        if (isset($_POST['Post']))
        {  
            $model->attributes = $_POST['Post'];
 
            $model->attachBehavior('myBehavior', array(
                'class' => 'MyBehavior',
            ));
 
            $success = $model->save();
 
            $model->detachBehavior('myBehavior');
 
            if ($success){
                Yii::app()->user->setFlash('post-form', 'Сохранено');
                $this->redirect($model->url);
            }
        }
 
        $this->render('create', array('model'=>$model));
    }
}

Здесь мы вручную прикрепили к модели поведение, сохранили запись и сразу же удалили это поведение по его имени. Аналогично нужно изменить actionUpdate().

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

Достоинства использования поведений

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

Также поведения удобны и для дистрибьюции расширений. Например, большое полноценное поведение DCategoryBehavior содержит около двадцати методов, а поведение для интеграции форума phpBB активно использует события beforeSave и beforeDelete модели User. Такие объёмы кода логичнее распространять как сейчас отдельными библиотеками-поведениями и одним движением подключать парой строк к любой модели, чем прилагать здоровую инструкцию с текстом «откройте данный файл, скопируйте все имеющиеся там методы в свою модель, переименуйте такие-то параметры на имена аналогичных полей вашей модели, создайте в модели метод beforeSave и напишите там...».

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

Поведения для моделей:

Поведения для контроллера

Вы можете сделать у себя любые другие.

Предположим, что нам нужно создать на сайте новый раздел «Фильмы». Там должны быть сущности:

  • фильмы (с описанием и картинкой),
  • герои (с описанием и картинкой, множественный выбор),
  • категории (вложенность, множественный выбор),
  • жанры (множественный выбор),

Если раньше нам пришлось бы прописывать десятки одинаковых методов в каждой модели, то теперь достаточно просто сгенерировать через Gii модели Film и FilmHero, FilmGenre, FilmCategory, добавить связи и перечислить внутри них нужные поведения: для фильтрации описания, загрузки изображения, поддержки вложенных категорий, множественного выбора.

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

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

UPD: Провели отдельный вебинар про поведения в Yii2.

Комментарии

 

Стас

Браво, Дмитрий! Спасибо за полноценное описание и качественные примеры. Наконец то ясность использования стала очевидной. Я бы сказал это единственный качественный труд столь насущной темы - что в нашем сегменте, что у иностранцев. Много перелопатил в поисках ответов, но ничего не найдя вернулся к "beforeSave" и им подобным.

Мне кажется, теперь после выхода данной статьи в инете появятся клоны по теме поведений, надеюсь хоть в душе спасибо Вам скажут.. :))

Ответить

 

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

Я и не против того, чтобы клоны целого моего сайта появлялись для разных языков и фреймворков ))

Ответить

 

Стас

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

Ответить

 

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

Конкретно интересуют геттеры и сеттеры в Yii? Или вообще общие примеры использования getXxx и setXxx и __get и __set в разных языках?

Ответить

 

Стас

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

Ответить

 

Саня

Спасибо, мегаполезная статья. Да и весь ваш блог - зачитаешься)

Ответить

 

Александр

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

Радует то, что вы не только про поведения расписали, но и про геттеры, просто класс! Если будет время напишите про связи (relation, один-ко-многим, многие-ко-многим, при каких ситуациях, что лучше использовать и когда).

Ответить

 

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

Понятие «связь» идёт из теории реляционных баз данных. Различные ORM разных фреймворков предоставляют похожие инструменты для работы с ними. Самоучитель по тем же ActiveRecord, Doctrine или Propel вводной информации по основам SQL и основам PHP не содержат, и для разработчика желательно уже уметь работать со связями на уровне SQL, а именно различать их типы и вручную строить запросы с JOIN.

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

Ответить

 

Алексей

Присоединяюсь к благодарностям! Доступным, человеческим языком всё описано, разложено по полочкам. Есть приятные "отходы от темы".

Конкретно по этой статье тоже большая благодарность! В некоторые вещи внесена ясность!

Ответить

 

Александр

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

Ответить

 

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

Может напишу. Добавлю пока в список тем на будущее.

Ответить

 

solo – google.com

Афтар, возьмите коды те что вы выложили и проверьте там куча ашибак.

Ответить

 

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

Исправил пару опечаток. Если найдёте ещё, то сообщите.

Ответить

 

solo – google.com

return this->getBaseImagePath() . 'small_' . $this->owner->{$this->imageField}; ага но еще забыли

Ответить

 

anton44eg

Автор, проверьте свой комментарий, в нем куча ошибок.

Ответить

 

fatalick

Спасибо, здорово написано, все понятно!

Ответить

 

Илья

Спасибо за познавательную статью!

Ответить

 

xar

Есть ли способ найти все модели которые используют определенное поведение ?

Ответить

 

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

Как наиболее оптимальное решение на ум приходит подключение интерфейса-маркера одновременно с поведением и перебор классов:

class Post extends CActiveRecord implements IUseMyBehavior {
    ....
}

$usesMyBhavior = array();
foreach ($allMyModelClasses as $class){
    if (is_subclass_of($class, 'IUseMyBehavior')
        $usesMyBhavior[] = $class;
}

или проходить по массиву $model->behaviors() и проверять их классы:

$usesMyBhavior = array();
foreach ($allMyModelClasses as $class){
    $model = new $class;
    foreach ($model->behaviors() as $behavior) {
        if ($behavior['class'] = 'MyBehavior')
            $usesMyBhavior[] = $class;
    }
}

Классы можно получить, например, взяв список файлов в папках models.

Но это статические варианты. Если поведения подключается динамически, то можно использовать «утиную» типизацию. А именно создавать экземпляр и проверять, есть ли в нём какое-либо поле, имеющееся в поведении:

$usesMyBhavior = array();
foreach ($allMyModelClasses as $class){
    $model = new $class;
    if ($model->canGetProperty(mySecretProperty))
        $usesMyBhavior[] = $class;
}

Также можно вызывать $model->asa('myBehavior'), но имя поведения пишется произвольно и может не совпадать с именем класса.

Ответить

 

Андрей Долгополов

можно поставить IDE по поиску найти имя класса - выдаст все модели контроллеры и компоненты где этот класс упоминается :)

Ответить

 

Vladislav Lisovenko

Дмитрий, спасибо Вам за статью, очень ценный труд, прочитал и с первого раза всё стало понятно, всё очень доступно и просто объяснено, примеры просто отличные.

Ответить

 

Николай

Спасибо за статью. Замечательный блог. У вас появился еще один постоянный читатель.
Еще раз спасибо!

Ответить

 

Дмитрий

Дмитрий, спасибо за Ваш блог. Очень много полезной информации на пути изучения Yii. Жду с нетерпением новых статей)

Ответить

 

Игорь Сапегин – www.sapegin.in

Здравствуйте, Дмитрий.
Спасибо за статью!

Подскажите, как можно определить наличие метода (например "methodExample") , если указанный метод присоединяется с помощью поведения (например "MethodExampleBehavior") ?

method_exists($object, "methodExample"); // не работает
Ответить

 

Игорь Сапегин – www.sapegin.in

Единственный способ, который я нашел, такой:

// проверка
$object->__isset('methodExample');
/** Behavior */
public function methodExample(){
   // code
}
// Заглушка для CComponent->__isset
public function getMethodExample(){
   return $this->methodExample()
}

Но это криво.

Ответить

 

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

Видимо это не предусмотрено. Можно было бы получить список поведений $_m из CComponent и самому проходить по нему с помощью method_exists() для вашего вопроса или get_class() для определения наличия определённого поведения. Но, увы, это поле приватное и геттера не имеет. Так что даже «костыль» с isset($object->methodExample()) подойдёт.

Ответить

 

Игорь – www.y-wave.ru

А можно behaviors вставить в CFormModel?

Ответить

 

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

Да. Также, как и в CActiveRecord модель в методе behaviors().

Ответить

 

Паел

Скажите, я правильно понял, что "поведение" вызывается до применения фильтров в контроллере? Т.е. поведением я при определенном условии смогу переопределить массив возвращаемый accessRules().
Задача состоит в том, что бы динамически определять разрешения в зависимости от того в какой группе пользователь состоит + какой контроллер используется.

Ответить

 

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

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

class Controller extends CController {
    public function behaviors() {
        return array(...);
    }
    public function accessRules() {
        return $this->myAccessRulesFromBehavior();
    }
}
Ответить

 

Pavel

Спасибо. Я правда реализовал немного по другому. От обобщающего подхода пришлось отказаться и по этому в каждом контроллере сделал так:

public function accessRules() {
    return $this->LoadUserPriveleges(__CLASS__, get_class_methods(__CLASS__));
}
public function behaviors() {
    return array(
        'BehoviorAccessRules' => array (
            'class' => 'BehoviorAccessRules',
            ....
        )
    );
}


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

Ответить

 

Pavel

забыл добавить, что конечно же при обработке данных в поведении все "зарезервированные" если можно так сказать методы выбрасываются

Ответить

 

Иван

Отлично описано! Спасибо!

Ответить

 

Антон

Спасибо, очень погла статья!

Ответить

 

Юрий Жупиков

Отличная статья!

Ответить

 

Дмитрий

Спасибо за отличное разьяснение.

Ответить

 

Сергей

В строчке
if ($file = CUploadedFile::getInstance($this->owner, $this->fileField))){

Лишняя ")"

Ответить

 

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

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

Ответить

 

Олег

Cпасибо, очень доходчиво, продолжайте!

Ответить

 

Alex D

Спасибо большое за внятное разъяснение!

Благодаря вам, я написал поведение, которое для меня по кр. мере и думаю для многих будет крайне полезным - большинству словян надо хранить даты и вытягивать даты в нашем формате а не западном - проблема типов DATE в MySQL решалась разными способами, но поведением, думаю получается наиболее элегантно:

<?php
class autoDBDatesBehavior extends CActiveRecordBehavior
{
    public function beforeSave($event) {
        foreach($this->owner->metadata->tableSchema->columns as $columnName => $column){
            if ($column->dbType === 'date')
                 $this->owner->$columnName = date('Y-m-d', strtotime($this->owner->$columnName));
        }
    }

    public function afterFind($event) {
        foreach($this->owner->metadata->tableSchema->columns as $columnName => $column){
            if ($column->dbType === 'date'){
                $this->owner->$columnName = Yii::app()->dateFormatter->format('dd.MM.yyyy',$this->owner->$columnName);
            }
        }
    }
}

теперь в любой модели, где есть DATE поля, просто прицепите:

public function behaviors() {
    return array(
        'autoDBDatesBehavior' => array('class' => 'AutoDBDatesBehavior',),
    );
}

и все волшебно заработает "как надо" :)

Пожалуйста, пересмотрите код и предложите "улучшения" или укажите на ошибки если есть.

Ответить

 

Alex D

Небольшой UPDATE:
чтобы сохранялись "пустые" (NULL) даты (если необходимо стирать иногда дату из Input) и правильно их обрабатывать пришлось добавить еще по одной строке в каждый их методов поведения (сразу после if ($column->dbType === 'date')):

{
if (empty($this->owner->$columnName)) {$this->owner->$columnName = null; continue; } 
...
}

и

{
if (empty($this->owner->$columnName)) continue;   
...
}
Ответить

 

Владимир Любарь

Дмитрий, здравствуйте!

Не совсем понял как использовать в классе поведения метод beforeValidate().

В контроллере примерно так:

$model = Model::model()->findByPk($id);
$model->attributes = Yii::app()->request->getPost('Model');
if($model->validate() && $model->save())
{
      //....
}

При этом методы поведения beforeSave(), afterSave() вызываются, также вызывается afterValidate(), но вот beforeValidate() не вызывается

Ответить

 

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

А Вы не перекрывали метод beforeValidate в самой модели?

Ответить

 

Владимир Любарь

Да, забыл вызвать parent::beforeValidate() в beforeValidate() модели.....
Большое спасибо! Надо больше спать :)

Ответить

 

Алексей

Супер статья, спасибо Дмитрий!

Ответить

 

Andrey

Спасибо!!!

У меня вопрос - чем отличается CActiveRecordBehavior от СBehavior?

Ответить

 

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

Различаются только набором отслеживаемых событий. CModelBehavior наследуется от CBehavior, но содержит ещё и встроенные обработчики afterConstruct, beforeValidate и afterValidate. В CActiveRecordBehavior аналогично добавлены события из CActiveRecord.

Ответить

 

Саша

Вижу живое общение на тему поведения в yii. Дмитрий, может вы сможете помочь. Я расширяю модель своим поведением, т.е. появляется некий метод. Но вот беда, я не могу задать модели-родителю свойства из под поведения, т.е. хочу чтобы в модели-owner е появился аттрибут. Поведения типа CActiveRecordBehavior

Ответить

 

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

Просто добавьте публичное поле в класс поведения или сделайте геттер+сеттер. Если хотите добавить в rules модели свой атрибут, то это можно сделать в метода attach():

class MyBehavior
{
    public $value;

    public function attach($owner)
    {
        $validator = new CSafeValidator();
        $validator->attributes = array('value');
        $owner->getValidatorList()->add($validator);
        parent::attach($owner);
    }
}

Это если я правильно понял ваш вопрос.

Ответить

 

Саша

Это в случае с валидатором, а что делать чтобы присвоить просто свойства модели, которых у нее нет?

 public function attach($owner)
    {
        
        $owner->attributes = array('calls'=>0,'rejects'=>0);
        parent::attach($owner);
    }


Так? Чтобы в итоге потом можно было обратиться к ним:
$model->calls;

Ответить

 

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

Тогда просто вот так:

class MyBehavior
{
    public $calls = 0;
    public $rejects = 0;
}

И эти свойства появятся в модели.

Ответить

 

Саша

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

Ответить

 

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

Очерёдность видна в методе __get($name) класса CActiveRecord и потом в аналогичном класса CComponent.

Ответить

 

Andrey

Добрый день,

подскажите пожалуйста, допустим есть несколько поведений, подключенных к одной модели, у каждого есть свой AfterSave и BeforeSave , AfterDelete и т.д. Допустим такие же методы лежат и в самой модели, как выявить к примеру порядок вызовов этих after Действий?

Ответить

 

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

Поведения подключаются в том порядке, в котором они перечислены в массиве внутри behaviors().

Допустим, что в модель Вы добавили метод:

protected function afterSave() {
    ...
    parent::afterSave()
}

Вы выполняете какие-то свои действия и через parent вызываете CActiveRecord::afterSave, который выглядит так:

protected function afterSave() {
    if($this->hasEventHandler('onAfterSave'))
        $this->onAfterSave(new CEvent($this));
}

public function onAfterSave($event) {
    $this->raiseEvent('onAfterSave',$event);
}

и который, как мы видим, запускает выполнение события 'onAfterSave'. Поведения при подключении подписывается на нужные им события (добавляют свои методы-обработчики в массив CComponent::_e), а метод CComponent::raiseEvent обходит этот массив циклом foreach и запускает каждый обработчик.

Так что в начале фреймворком запускается метод afterSave в модели. Если Вы его переопределили, то запускается именно ваш метод. А потом Вы сами вызываете parent::afterSave() и он уже запускает обработчики во всех поведениях по порядку подключения этих поведений.

Ответить

 

Andrey

Есть ли принципиальные отличия для статических методов, находящихся в поведении от обычных?

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

Call to undefined method Product::staticFunc()

Ответить

 

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

Для перехвата статических методов нужно использовать магический метод __callStatic, а в CComponent есть только __call для нестатических методов. Так что можете либо добавить в модель метод __callStatic (но в нём всё равно нужно будет создавать экземпляр модели, чтобы как-то добраться до её поведений), либо просто сделать ваш метод нестатическим.

Ответить

 

Andrey

Спасибо!

Ответить

 

BSChehsir

Для добавления слушателя событий делать так

class HistoryBehavior extends CActiveRecordBehavior
{
    public function events()
    {
        $this->owner->attachEventHandler('onAfterFind', ['History', 'logAfterFind']);
        $this->owner->attachEventHandler('onAfterSave', ['History', 'logAfterSave']);
        return parent::events();
    }
}

для замены слушателей события - так

class HistoryBehavior extends CActiveRecordBehavior
{
    public function events()
    {
        return array_merge(parent::events(), [
            'onAfterFind'  => ['History', 'logAfterFind'],
            'onBeforeSave' => ['History', 'logAfterSave'],
        ]);
    }
}

для добавления в момент подключения

    public function attach($owner)
    {
        parent::attach($owner);
        $owner->attachEventHandler('onAfterFind', ['History', 'logAfterFind']);
        $owner->attachEventHandler('onAfterSave', ['History', 'logAfterSave']);
    }

я правильно понял?

Ответить

 

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

Да, можно так.

Ответить

 

Андрей – 100banknot.ru

Дмитрий, большое спасибо, было очень полезно прочитать Вашу статью

Ответить

 

Владимир Атанов

Здорово, полезная статься

Ответить

 

Александер

пытался прочитать на смартфоне 4", обрезаются вставки кода (

Ответить

 

Максат

Отличнейшая статья!

Ответить

 

Андрей

Добрый день, Дмитрий,

Подскажите, Как в yii скормить модели картинки, без формы?

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

Если использовать следующую конструкцию:

@file_put_contents($pathLocale, file_get_contents($pathRemote));

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

Я бы хотел, чтобы картинки при $model->save() загружались таким же образом, как это происходит при загрузке элемента через форму. Т.к. отрабатываются поведение для основной картинки и поведение для доп. изображений ( в частности метод поведений afterSave())..

Сами данные успешно сохраняются, но вот картинки не подкидываются. Может неправильно прописал данные в $_FILES? OS linux mint

Пробовал так:

''''
           $this->uploadImages($images, $bust);
            //  My::printArr($_FILES);
            //  My::printArr($_POST);
            if(!$model) {
                $model  =   new Objects();
            }
            $model->setAttributes($excel, true);
            if(!$model->save())
                echo 'Ошибка сохранения '. My::printArr($model->printErrors());
}

private function uploadImages($images, $path) {
        // foreach ((array)$_POST["prev_image_ids"] as $md5) {
        if(!$images){
            return;
        }
        foreach ($images as $image) {
            $md5    =   md5(microtime()); // basename($md5);
            $imagePath  =   $path . '/' . $image;
            $tmpName     =   "/tmp/{$md5}"; //"./already-uploaded/{$md5}";
            //  $img        =   'http://static2.t-ru.org/logo/logo.gif';
            $getInfo    =   getimagesize($imagePath);
            // My::printArr($getInfo);
            //  header('Content-type: ' . $getInfo['mime']);
            //  readfile($img);
            // die();
            $_FILES['Objects']["pictures"][]   =   [
                "tmp_name"  =>  $imagePath, // $tmpName,
                "size"      =>  filesize($imagePath),   // $tmpName
                "type"      =>  $getInfo['mime'],       // image/png
                "name"      =>  $image,                 // Screenshot_20161117_141028.png
                "error"     =>  UPLOAD_ERR_OK,
            ];
        }
        $_FILES['Objects']["photo"]    =   $_FILES['Objects']["pictures"][0];
    }
Ответить

 

Григорий Степенко

Дмитрий, добрый день. Увидел битую ссылку в статье
... ни на странице об использовании, ни в теме о создании /a расширений нет ...
Можно заменить .ru на .com и будет работать.
Спасибо за статью

Ответить

 

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

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

Ответить

 

des

а поведение перезаписывает уже существующие методы?

Ответить

 

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

Нет.

Ответить

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

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


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





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