تالار گفتمان nCIS.ir

نسخه‌ی کامل: ترفندهای بهینه سازی کد در فریمورک Yii 1.1
شما در حال مشاهده نسخه آرشیو هستید. برای مشاهده نسخه کامل کلیک کنید.
توی این تاپیک میخوایم هر تکنیکی که بنظرمون میاد برای بهینگی کد توی فریمورک Yii مناسبه مطرح کنید. حتماً میدونید که بخاطر ساختار داخلی فریمورک و معماریهای بکار رفته در داخلش، یکسری جاها اصول بهینگی توی کدنویسی فریمورک Yii با خود زبان PHP خام فرق میکنه. تجربیاتتون رو با دوستانتون به اشتراک بگذارین.
اولین مورد رو خودم میگذارم تا استارت بخوره این تاپیک. خیلیها توی یک پروژه برای اینکه فرضاً یک کاری رو روی تمام رکوردهای یک جدول انجام بدیم، از این روش مرسوم در خود 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 اون رو بعنوان شرط استخراج رکورد بعدی درنظر میگیریم. اینطوری وقتی دیگه رکوردی نباشه، از حلقه میایم بیرون ولی هربار فقط یک رکورد توی حافظه است.
امیدوارم به دردتون بخوره.
-----
نکته مهم: این تکنیک وقتی مفیده که مصرف حافظه براتون مهمتر از سرعت باشه چون سرعتش تاحدودی کمتر از استخراج یکجای اطلاعات با یک کوئری هست ولی خوب بعضی وقتها بخصوص توی هاستهای اشتراکی حافظه خیلی کمی در اختیارتون قرار میگیره و حجم اطلاعات شما هم بالاست. اینجاست که این تکنیک به دادتون میرسه.
حتماً تا حالا براتون پیش اومده که فرضاً بخواین فقط ایمیل کاربرانی که فعال شدن رو داشته باشین. راه حلی که اکثر افراد میرن اینه:
$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 ذکر شده رو نشون میده.
خب خیلی از دوستان با توجه با اینکه تا حالا چندین مورد ازم سوال کردن این ذهنیت رو پیدا کردن که وقتی صحبت از "سطح دسترسی" میشه،باید 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 رو داره ؟ خریداره یا نه ؟ اگر بود ادامه کار رو میره وگرنه با یک صفحه خالی رو به رو میشه [عکس: 124.gif]
حالا برید کنترلر،ویو ها و ... مخصوص هر ماژول رو بسازید و برنامه رو توسعه بدید.
فایل 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();'),
بهتره وقتی به فیلدهای خاصی احتیاج ندارین، اونها رو بارگذاری نکنید.
مثال:
// 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',
));
شاید براتون پیش اومده باشه که بخواین کوئریهایی که به دیتابیس میره رو لاگ کنید. خوب خیلی راحت میشه با این کد توی فایل 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();
       }
   }
}

فکر میکنم به حد کافی کدها واضح هستن و نیازی به توضیح اضافه نیست.