# زبان های اسکریپتی > PHP > Yii Framework >  ترفندهای بهینه سازی کد در فریمورک Yii

## MMSHFE

توی این تاپیک میخوایم هر تکنیکی که بنظرمون میاد برای بهینگی کد توی فریمورک Yii مناسبه مطرح کنید. حتماً میدونید که بخاطر ساختار داخلی فریمورک و معماریهای بکار رفته در داخلش، یکسری جاها اصول بهینگی توی کدنویسی فریمورک Yii با خود زبان PHP خام فرق میکنه. تجربیاتتون رو با دوستانتون به اشتراک بگذارین.

----------


## MMSHFE

اولین مورد رو خودم میگذارم تا استارت بخوره این تاپیک. خیلیها توی یک پروژه برای اینکه فرضاً یک کاری رو روی تمام رکوردهای یک جدول انجام بدیم، از این روش مرسوم در خود PHP استفاده میکنیم:
foreach(Posts::model()->findAll('confirmed=1') as $post) {
    // ...
}
خوب حالا ایرادش چیه؟ اینه که در ابتدای حلقه تمام رکوردهای جدول Posts بصورت شئ با تمام attributeها و... میاد توی حافظه و فرض کنید توی یک هاست اشتراکی با محدودیت حافظه میخواین 10 هزار رکورد که هرکدوم 10 تا فیلد دارن و فرضاً فیلد body برخی از اونها حدود 3-2 کیلوبایته پردازش بشه. اینجا نه تنها به سرور فشار میاد بلکه ممکنه حتی سایت شما رو Suspend کنن یا خطای Maximum memory allocated و... بگیرین. تازه الان ما از with و... استفاده نکردیم و هیچ Join و... در کار نیست وگرنه احتمالاً مسئول هاست شخصاً میومد کتک هم میزد ما رو!!!
حالا راه حل چیه؟
$id = 0;
while($post = Posts::model()->find('id>:id AND confirmed=1', array(':id'=>$id))) {
    $id = $post->id;
    // ...
}
چیزی که مسلمه اینه که رکوردی با id صفر نداریم و از 1 شماره میخوره رکوردها. خوب ما اول میایم شرط میگذاریم id بزرگتر از صفر رو پیدا کنه (بخاطر find بجای findAll فقط یک رکورد استخراج میشه) و بعد از استخراج، id اون رو بعنوان شرط استخراج رکورد بعدی درنظر میگیریم. اینطوری وقتی دیگه رکوردی نباشه، از حلقه میایم بیرون ولی هربار فقط یک رکورد توی حافظه است.
امیدوارم به دردتون بخوره.
-----
نکته مهم: این تکنیک وقتی مفیده که مصرف حافظه براتون مهمتر از سرعت باشه چون سرعتش تاحدودی کمتر از استخراج یکجای اطلاعات با یک کوئری هست ولی خوب بعضی وقتها بخصوص توی هاستهای اشتراکی حافظه خیلی کمی در اختیارتون قرار میگیره و حجم اطلاعات شما هم بالاست. اینجاست که این تکنیک به دادتون میرسه.

----------


## MMSHFE

حتماً تا حالا براتون پیش اومده که فرضاً بخواین فقط ایمیل کاربرانی که فعال شدن رو داشته باشین. راه حلی که اکثر افراد میرن اینه:
$emails = array();
foreach(Users::model()->findAll('active=1') as $user) {
    $emails[] = $user->email;
}
حالا اگه فیلدهایی که میخواین استخراج بشه بیشتر بود چی؟ قطعاً دردسرتون بیشتر میشه و تازه این روش مثل مطلب شماره 2 همین تاپیک، مشکل مصرف زیاد حافظه رو داره. اما روشی که من بعنوان جایگزین میخوام معرفی کنم چیه؟
توی پوشه protected/components این کد رو به اسم ActiveRecord.php ذخیره کنید:
class ActiveRecord extends CActiveRecord
{
    public function findColumn($columns, $condition = '', $params = null)
    {
        if($condition instanceOf CDbCriteria) {
            $criteria = $condition;
        }
        else {
            $criteria = new CDbCriteria;
            if(is_array($condition)) {
                foreach($condition as $k => $v) {
                    $criteria->{$k} = $v;
                }
            }
            else {
                $criteria->condition = $condition;
                $criteria->params = $params;
            }
            $criteria->select = $columns;
        }
        $data = array();
        $cols = array_map('trim', explode(',', $columns));
        foreach($this->findAll($criteria) as $model) {
            $row = array();
            foreach($cols as $col) {
                $row[$col] = $model->attributes[$col];
            }
            $data[] = $row;
        }
        return $data;
    }
}
حالا کافیه هر مدلی که میخواین قابلیت جدید findColumn رو داشته باشه، بجای CActiveRecord خود فریمورک از ActiveRecord ما مشتق بشه. برای مثال:
class Users extends ActiveRecord { ... }
و حالا چطور از این متد استفاده میکنیم؟ خیلی ساده است:
print_r(Users::model()->findColumn('email', 'active=1');
میتونید چند ستون رو هم داشته باشین:
print_r(Users::model()->findColumn('username,password', 'active=:active', array(':active'=>1)));
تازه میتونید از تمام قابلیتهای Criteria هم استفاده کنید:
$criteria = new CDbCriteria;
$criteria->addSearchCondition('name', 'ali');
foreach(Users::model()->findColumn('email', $criteria) as $user) {
    echo '<p>' . $user['email'] . '</p>' . PHP_EOL;
}
الان این دستورات به راحتی به شما آدرس ایمیل کاربرانی که توی فیلد name اونها کلمه ali ذکر شده رو نشون میده.

----------


## alireza.stack

> اولین مورد رو خودم میگذارم تا استارت بخوره این تاپیک. خیلیها توی یک پروژه برای اینکه فرضاً یک کاری رو روی تمام رکوردهای یک جدول انجام بدیم، از این روش مرسوم در خود PHP استفاده میکنیم:
> foreach(Posts::model()->findAll('confirmed=1') as $post) {
>     // ...
> }
> حالا راه حل چیه؟
> $id = 0;
> while($post = Posts::model()->find('id>:id AND confirmed=1', array(':id'=>$id))) {
>     $id = $post->id;
>     // ...
> }


لزوما نمیشود گفت که روش دوم بهتر است؟! شما حاضرید ۱۰ هزار بار درخواستتون به سمت سرور Database برود و برگردد؟ چگونه از هزینه های query Planner و query Opimizer و بسیاری از سطوح مختلف فیلترها چشم پوشی کردید؟ 

*راه حل:* من اگر قرار بود این سیستم رو هندل کنم داده ها رو بصورت Big Chunks میخوندم مثلا هر درخواست هزارتا رکورد فچ شود (نه خیلی کم تا ۱۰ هزار بار درخواست بفرستم و نه خیلی زیاد که سرور دان شه) و این درخواستها رو در سیستم key value storage مانند redis ذخیره میکردم و برای دفعات بعدی از آنجا می خواندم.

----------


## MMSHFE

ببینید مسئله اینه که خیلی وقتها یادمون میره که سرور دیتابیس و وب سرور هر دو روی یک سیستم درحال اجرا هستن و درخواستهای مکرر فرستادن به دیتابیس، مسئله خاصی ایجاد نمیکنه (چه ازنظر مصرف ترافیک و چه ازنظر بار گذاشتن روی پردازنده) و ازطرفی راه حلی که من ارائه کردم هم با توجه به امکانات عادی و پیشفرض موجود بود که همون دیتابیس عادی MySQL و وب سرور Apache و مفسر PHP با تنظیمات پیشفرض هست. ضمناً میشه این روش رو با راهکار شما ترکیب کرد و برای مثال، چنین نتیجه ای بدست آورد:
$id = 0;
while($posts = Posts::model()->findAll(array('condition'=>'id>:id AND confirmed=1','params'=>array(':id'=>$id),'limit'=>  100)) {
    $id = end($posts)->id;
    foreach($posts as $post) {
        // ...
    }
}

----------


## alireza.stack

> ببینید مسئله اینه که خیلی وقتها یادمون میره که سرور دیتابیس و وب سرور هر دو روی یک سیستم درحال اجرا هستن و درخواستهای مکرر فرستادن به دیتابیس، مسئله خاصی ایجاد نمیکنه (چه ازنظر مصرف ترافیک و چه ازنظر بار گذاشتن روی پردازنده) و ازطرفی راه حلی که من ارائه کردم هم با توجه به امکانات عادی و پیشفرض موجود بود که همون دیتابیس عادی MySQL و وب سرور Apache و مفسر PHP با تنظیمات پیشفرض هست. ضمناً میشه این روش رو با راهکار شما ترکیب کرد و برای مثال، چنین نتیجه ای بدست آورد:
> $id = 0;
> while($posts = Posts::model()->findAll(array('condition'=>'id>:id AND confirmed=1','params'=>array(':id'=>$id),'limit'=>  100)) {
>     $id = end($posts)->id;
>     foreach($posts as $post) {
>         // ...
>     }
> }



روش بهتری شد. منظورم به هیچ وجه بار ترافیکی over network بین بانک اطلاعاتی و سرور نیست. اونکه اصلا به حساب نمیاد برای ما. منظورم پردازشهای داخل خود MySQL هست.

----------


## salehforum

ممنون بابت مطالب خوبتون آقای شهرکی اما من با آقای alireza.stack موافقم در مورد تعداد درخواست های بالا به پایگاه، زیاد جالب نیس این تعداد در خواست به پایگاه داده

----------


## MMSHFE

یک سؤال دارم: فرض کنید یک هاست اشتراکی داریم که سایتمون روشه و 1000 کاربر هم داریم. حالا تصور کنید در صفحه مشخصات کاربران، میخواین اطلاعات این 1000 کاربر بارگذاری بشه و یکسری مشخصاتشون رو نشون بدیم (هیچ Join هم درکار نیست). چنین فرضی محتمل بنظر میرسه دیگه نه؟ 1000 کاربر چیزی نیست که بخوایم بخاطرش سرور اختصاصی بگیریم. حالا فرض کنید جدول اطلاعات کاربران حدود 20 فیلد داره که 10 تاش متنیه و هرکدوم حدود 100 کارکتر محتوا داره بطور متوسط (یک سایت شبکه اجتماعی یا دوستیابی رو فرض کنید). خوب در چنین حالتی در هر درخواست 1 مگابایت حافظه مصرف میشه که اگه 1000 بازدیدکننده همزمان باشه میشه 1 گیگابایت حافظه مصرفی در هر لحظه.

من نگفتم روش من ایده آله، گفتم در شرایطی که *با محدودیت حافظه مواجه هستیم*، میتونیم از این روش برای حل مشکل استفاده کنیم. بهرحال باید تا جایی که میتونیم از امکانات موجود استفاده کنیم و بعد تصمیم به ارتقاء سرور بگیریم. من خودم شخصاً برای یک هاست 1000 کاربره سرور اختصاصی یا VPS نمیگیرم چون ارزش نداره و هزینه هاش در نمیاد. بقیه رو نمیدونم. تصمیم با خودتونه که مصرف حافظه براتون مهمتره یا سرعت یا تعداد اتصالها به دیتابیس.

----------


## salehforum

آقای شهرکی حرف شما درسته اما فکر نمی کنم نمایش و دریافت این همه داده از جدول کار درستی باشه برای نمایش در یک صفحه
نمایش هزار کاربر در یک صفحه؟

حرف شما برای مواردی مثل Cron Job ها که مجبوریم برای تمام کاربر ها اجراش کنیم و رم هم واقعا پایینه درست هست. اما همون تراکنش و درخواست به تعداد زیاد آمار question های پایگاه رو به شدت میبره بالا.

----------


## MMSHFE

یه شبکه اجتماعی رو درنظر بگیرین که قراره پروفایل اعضا رو نشون بده. در حداقل حالتها اطلاعات 100 کاربر نشون داده میشه. اسکریپتی مثل Sharetronix رو درنظر بگیرین یا PHPFox و... درسته که شاید فقط عکس کاربر نشون داده بشه ولی بهرحال کل مدل داره تو حافظه میاد. البته راه حلهای دیگری هم هست مثل همون ترفند دوم که گفتم چطور فقط ستون خاصی استخراج بشه ولی این روش ساده ترین روش برای بهینه سازیه بخصوص وقتی به اکثر فیلدها نیاز داشته باشیم. به نوعی میشه Pagination در سطح مدل درنظر گرفتش.

----------


## Veteran

خب خیلی از دوستان با توجه با اینکه تا حالا چندین مورد ازم سوال کردن این ذهنیت رو پیدا کردن که وقتی صحبت از "سطح دسترسی" میشه،باید RBAC رو پیاده سازی کنند.اما این RBAC نیاز نیست برای هر چیزی پیاده بشه،و اصلا توصیه هم نمیشه.زمانی شما باید به سراغ این معماری برید که با سطح های مختلف و در اون سطح ها کاربران و باز با دسترسی های متفاوت و گروه های زیادی سرو کار دارید.تا بعدا بشه براحتی و بدون دستکاری ساختار دیتابیس،سطح،گروه و ... اضافه بکنیذ.
اما خیلی وقتا میشه با دادن یک مشخصه به کاربر،اون رو در سطح های مختلف قرار داد.مثلا یک سیستم رو در نظر بگیرید با سطح های
مدیریت کل سیستمفروشندهخریدار 
خب اگر بخوایم در این سیستم،امکانات هر سطح رو در اختیار فقط همون سطح قرار بدیم باید مشخص کنیم که چه کاربرانی جزء کدوم یک از سطح های بالا هستند.
جدول کاربران رو با این ساختار در نظر بگیرید

CREATE TABLE IF NOT EXISTS `tbl_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) COLLATE utf8_bin NOT NULL,
  `email` varchar(255) COLLATE utf8_bin NOT NULL,
  `password` varchar(255) COLLATE utf8_bin NOT NULL,
  `last_login_time` datetime DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `create_user_id` int(11) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `update_user_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_bin AUTO_INCREMENT=2 ;

این جدول هیچگونه مشخصه ایی نداره که بخوایم بفهمیم کاربر در چه سطحی قرار داره.پس نیاز داریم یک فیلد دیگه به این جدول اضافه بکنیم

ALTER TABLE `tbl_user` ADD `level` TINYINT( 1 ) NOT NULL AFTER `last_login_time` ;

خب الان به این شکل عمل میکنیم :
مدیریت کل سیستم،سطح 1فروشنده سطح 2خریدار سطح 3 
یعنی کاربرانی که در دیتابیس،فیلد level اونها
 1 هست،در نقش مدیریت کل سیستم
2 هست، در نقش فروشنده
3 هست،در نقش خریدار
موقعه ثبت نام هم فقط سطح رو در سطح 2 و 3 قرار بدید و اون رو در Rule قرار بدید تا کاربری ثبت نام نکنه با سطح 1،سطح 1 رو به صورت دستی ایجاد کنید.
حالا باید بیایم امکانات هر سطح رو فقط در اختیار کاربران همون سطح قرار بدیم.پس چیزی که لازم دارم اینکه در زمان لاگین اطلاعات کاربر لاگین شده رو ذخیره کنیم.
میایم به این روش غمل میکنیم(البته روش های دیگه ایی هم هست)
اگر کاربر تونست لاگین بکنه در کامپوننت userIdentity، رکورد پیدا شده که یک شی Active Record هست رو در یک سشن ذخیره میکنیم
$this->setState('userInfo', $user);
این user$ اینه :

        $user = Users::model()->find('LOWER(username)=?', array (
                strtolower($this->username) 
        ));

حالا چطور امکانات هر سطح رو داشته باشیم و در اختیارشون قرار بدیم ؟ یکی از روش های خیلی خوب اینکه شما برای هر سطح یک ماژول بسازید
پس برید و هرچندتا سطح که دارید،به تعداد با اسم های مشخص ماژولشون رو بسازید و بعد از ساخت نیاز دارید اونرو به برنامه معرفی کنید.بعد برید به فایل main.php جایی که ماژول Gii معرفی شده،بعد از اون ماژول هارو معرفی کنید،یعنی اسم ماژول هارو قرار بدید ( Module ID )
اینم نمونه :

        'modules' => array (
                // uncomment the following to enable the Gii tool
             /*   'gii' => array (
                        'class' => 'system.gii.GiiModule',
                        'password' => '123',
                        // If removed, Gii defaults to localhost only. Edit carefully to taste.
                        'ipFilters' => false 
                ),*/
                'developer',
                'user',
                'Admin' 
        ),

خب این ماژول ها خودشون یک برنامه های کاملی هستند به عنوان زیر برنامه،پس نگران تداخل نباشید.توجه داشته باشید که مدل هایی که در اپلیکیشن اصلی در فولر models قرار دارند در ماژول هم قابل دسترسی هستند.
هر ماژول هم به صورت پیشفرض یک فایل داره که در ریشه قرار داره کلاسی هم نام با اسم ماژول وجود داره که در اون یک متدهست به این شکل :

    public function beforeControllerAction($controller, $action)
    {
        if(parent::beforeControllerAction($controller, $action))
        {
            // this method is called before any module controller action is performed
            // you may place customized code here
            return true;
        }
        else
            return false;
    }

این متد در هربار فراخوانی ماژول اجرا میشه و ما میتونیم چک کنیم که اگر کاربر سطح دسترسی داشت،ادامه کار رو بره.وگرنه نتونه.
به این شکل

    public function beforeControllerAction ( $controller, $action ) {
        if(parent::beforeControllerAction($controller, $action)) {
            if(Yii::app()->user->isGuest == false) {
                if(Yii::app()->user->userInfo->level==3)
                    return true;
            }else{
                Yii::app()->controller->redirect('/signin');
            }
        }
        else
            return false;
    }


اگر این ماژول رو ماژول خریدار فرض کنیم،اومدیم قبل از هرکاری چک کردیم که کاربر فعلی سطح 3 رو داره ؟ خریداره یا نه ؟ اگر بود ادامه کار رو میره وگرنه با یک صفحه خالی رو به رو میشه  :قهقهه: 
حالا برید کنترلر،ویو ها و ... مخصوص هر ماژول رو بسازید و برنامه رو توسعه بدید.
اگر جایی مشکل داشتید،بفرماید تا بیشتر توضیح بدیم و اگر اساتید گرامی متوجه مشکلی شدند،خوشحال میشم مارو از اون اگاه کنند.

----------


## Tarragon

تشکر می کنم از vetern جان.
یک نکته که من تو کارم استفاده می کنم اینجوره که مثلا توی یه بازی آنلاین داریم بازیکن، مولتی هانتر (‌فقط نامه هارو جواب می ده و می تونه اکانت بازیکن های دیگه رو هم ببینه) و ادمین که کل امکانات رو داره. من اینجوری استفاده کردم که ادمین ۹ هستش ، کسی که لوگین نکرده ۱ ، بازیکن ۲ و مولتی هانتر ۸.
و اگر بقیه هم بونم به نسبت بین اینا تقسیم می کنیم بعدش مثلا برای controller قسمت مشاهده امکانات ادمین اونایی که level بیشتر از ۸ دارن و برای مثلا دیدن لیست بازیکنان اونایی که بیشتر از ۱ هستند.
الان تو مثال بالا تمام امکانات فرشنده تمام امکانات خریدار رو داشته باشه و ادمین هم تمامی امکانات. اینجوری باعث می شه که کد کمتری بنویسیم و دیگه لازم نیست برای هر کدوم سطع دسترسی مجزا مشخص کنیم. 
البته اینم بگم بعضی وقتا باید به بعضی rule ها دسترسی موازی بدیم که بعضیا این امکان رو داشته باشد و بعضیا یه چیزای دیگه داشته باشند اون وقت از روش شما استفاده می شه.

----------


## MMSHFE

فایل index.php توی ریشه سایتتون رو باز  کنید و اولش این کد رو بگذارین:
ob_start('ob_gzhandler');
و آخرش هم این کد رو بنویسید:
ob_end_flush();
اینطوری هم از بافر خروجی استفاده میشه و هم بخاطر استفاده  از gzip محتوای اسکریپت شما فشرده میشه و برای کلاینت ارسال میشه و مرورگر  کلاینت اسکریپت رو از حالت فشرده خارج میکنه (همه مرورگرها این فشرده سازی  رو میشناسن). با این روش صفحه ای که در حالت عادی ممکنه 100 کیلوبایت حجم  داشته باشه، با 2 تا 3 کیلوبایت حجم دریافت میشه. اینطوری هم توی مصرف  ترافیک سایتتون صرفه جویی میشه و هم سرعت بارگذاری صفحات بالا میره.

----------


## rezaonline.net

> فایل index.php توی ریشه سایتتون رو باز  کنید و اولش این کد رو بگذارین:
> ob_start('ob_gzhandler');
> و آخرش هم این کد رو بنویسید:
> ob_end_flush();
> اینطوری هم از بافر خروجی استفاده میشه و هم بخاطر استفاده  از gzip محتوای اسکریپت شما فشرده میشه و برای کلاینت ارسال میشه و مرورگر  کلاینت اسکریپت رو از حالت فشرده خارج میکنه (همه مرورگرها این فشرده سازی  رو میشناسن). با این روش صفحه ای که در حالت عادی ممکنه 100 کیلوبایت حجم  داشته باشه، با 2 تا 3 کیلوبایت حجم دریافت میشه. اینطوری هم توی مصرف  ترافیک سایتتون صرفه جویی میشه و هم سرعت بارگذاری صفحات بالا میره.


خود وبسرورها بخصوص nginx با فعال کردن gzip برای فرمت text/html اینکارو انجام میدن :)

----------


## MMSHFE

متأسفانه اکثر هاستها (بخصوص ایرانی) اینکار رو انجام نمیدن و اگه انجام میدن هم فقط روی محتواست و بافر خروجی فعال نمیشه و درنتیجه مشکل عدم امکان تغییر هدرها باقی میمونه. با این روش که گفتم هم از مزیت بافر استفاده میشه و هم خروجی فشرده میشه.

----------


## arta.nasiri

> فایل index.php توی ریشه سایتتون رو باز  کنید و اولش این کد رو بگذارین:
> ob_start('ob_gzhandler');
> و آخرش هم این کد رو بنویسید:
> ob_end_flush();
> اینطوری هم از بافر خروجی استفاده میشه و هم بخاطر استفاده  از gzip محتوای اسکریپت شما فشرده میشه و برای کلاینت ارسال میشه و مرورگر  کلاینت اسکریپت رو از حالت فشرده خارج میکنه (همه مرورگرها این فشرده سازی  رو میشناسن). با این روش صفحه ای که در حالت عادی ممکنه 100 کیلوبایت حجم  داشته باشه، با 2 تا 3 کیلوبایت حجم دریافت میشه. اینطوری هم توی مصرف  ترافیک سایتتون صرفه جویی میشه و هم سرعت بارگذاری صفحات بالا میره.


تو فایل کانفیگ هم به این شکل انجام میگیره

'onBeginRequest'=> create_function('$event', 'return ob_start("ob_gzhandler");'),
	'onEndRequest'=> create_function('$event', 'return ob_end_flush();'),

----------


## MMSHFE

بهتره وقتی به فیلدهای خاصی احتیاج ندارین، اونها رو بارگذاری نکنید.
مثال:
// instead of Users::model()->findAll('confirmed=1');
$users = Users::model()->findAll(array('select'=>'id,name,family','conditi  on'=>'confirmed=1'));
مثالی از استفاده همراه با relationها (Eager Mode) :
// instead of Users::model()->with('posts')->findAll('confirmed=1');
$users = Users::model()->with(array(
    'posts'=>array(
        'select'=>'title,body',
    ),
))->findAll(array(
    'select'=>'id,name,family',
    'condition'=>'confirmed=1',
));

----------


## MMSHFE

شاید براتون پیش اومده باشه که بخواین کوئریهایی که به دیتابیس میره رو لاگ کنید. خوب خیلی راحت میشه با این کد توی فایل config/main.php لاگ دیتابیس رو فعال کنید:
'log'=>array(
    'class'=>'CLogRouter',
    'routes'=>array(
        array(
            'class'=>'CFileLogRoute',
            'levels'=>'error, warning',
        ),
        array(
            'class'=>'CFileLogRoute',
            'levels'=>'trace',
            'categories'=>'system.db.*',
            'logFile'=>'sql.log',
        ),
    ),
),
اما مسئله اصلی اینه که این فایل لاگ که ساخته میشه، خیلی هم خوانا و قابل فهم نیست و ازطرفی ممکنه شما فقط برای یک جدول خاص این لاگ رو لازم داشته باشین. حتی ممکنه ساختار دلخواه خودتون رو برای لاگ بخواین ایجاد کنید. اینجور مواقع، استفاده از این تکنیک میتونه به شما کمک کنه:
1- یک پوشه به اسم logs داخل protected بسازین و مجوز نوشتن بهش بدین.
2- توی مدلتون (من فرض کردم مدل Users هست) این کدها رو اضافه کنید:
class Users extends CActiveRecord
{
    private $_oldAttributes;
    
    public function afterFind()
    {
        $this->_oldAttributes = $this->attributes;
    }
    
    public function beforeSave()
    {
        if($this->scenario == 'update' && $this->attributes !== $this->_oldAttributes) {
             if($fp = fopen(Yii::app()->basePath . '/logs/users.log', 'a')) {
                fwrite($fp, 'Time: ' . date('Y/m/d - H:i:s') . PHP_EOL;
                fwrite($fp, 'Old Attributes:' . PHP_EOL;
                foreach($this->_oldAttributes as $name => $value) {
                    fwrite($fp, $name . ' => ' . $value . PHP_EOL);
                }
                fwrite($fp, str_repeat('-', 10) . PHP_EOL;
                fwrite($fp, 'New Attributes:' . PHP_EOL;
                foreach($this->ttributes as $name => $value) {
                    fwrite($fp, $name . ' => ' . $value . PHP_EOL);
                }
                fwrite($fp, str_repeat('=', 10) . PHP_EOL;
                fclose($fp);
            }
            return parent::beforeSave();
        }
    }
}
فکر میکنم به حد کافی کدها واضح هستن و نیازی به توضیح اضافه نیست.

----------

