CActiveDataProvider with custom value - php

In my Yii project I need to search and sort data by some custom value. I have 'users' table and I need every User instance to have month_profit property which should combine SQL data + lots of my own calculations. At the moment I have in my User model:
public $month_profit;
public function search($pageSize = 10, $defaultOrder = '`t`.`reg_date` DESC')
{
$criteria = new CDbCriteria;
$criteria->with = array('money');
$requests_table = Requests::model()->tableName();
$requests_count_sql = "(SELECT COUNT(*) FROM $requests_table rt WHERE rt.partner_id = t.id) ";
$referrals_table = Referrals::model()->tableName();
$referrals_count_sql = "(SELECT COUNT(*) FROM $referrals_table reft WHERE reft.user_id = t.id) ";
$referrals_payed_sql = "(SELECT COUNT(*) FROM $referrals_table reft WHERE reft.user_id = t.id AND reft.status = 'Оплачено') ";
//$month profit_sql = ???;
$criteria->select = array(
'*',
$requests_count_sql . "as requests_count",
$referrals_count_sql . "as referrals_count",
$referrals_payed_sql . "as referrals_payed_count",
$month_profit_sql . "as month_profit",
);
$criteria->compare($requests_count_sql, $this->requests_count);
$criteria->compare($referrals_count_sql, $this->referrals_count);
$criteria->compare($referrals_payed_sql, $this->referrals_payed_count);
$criteria->compare($month_profit_sql, $this->month_profit);
$criteria->compare('t.id', $this->id);
$criteria->compare('t.reg_date', $this->reg_date, true);
$criteria->compare('username', $this->username, true);
$criteria->compare('password', $this->password, true);
$criteria->compare('site', $this->site, true);
$criteria->compare('status', $this->status, true);
return new CActiveDataProvider(get_class($this), array(
'criteria' => $criteria,
'pagination' => array( 'pageSize' => $pageSize ),
'sort' => array(
'defaultOrder' => $defaultOrder,
'attributes' => array(
'id' => array(
'asc' => '`t`.`id` ASC',
'desc' => '`t`.`id` DESC',
),
'email' => array(
'asc' => '`t`.`email` ASC',
'desc' => '`t`.`email` DESC',
),
'requests_count' => array(
'asc' => 'requests_count ASC',
'desc' => 'requests_count DESC',
),
'referrals_count' => array(
'asc' => 'referrals_count ASC',
'desc' => 'referrals_count DESC',
),
'referrals_payed_count' => array(
'asc' => 'referrals_payed_count ASC',
'desc' => 'referrals_payed_count DESC',
),
'money' => array(
'asc' => 'money.profit',
'desc' => 'money.profit DESC',
),
'fullProfit' => array(
'asc' => 'money.full_profit',
'desc' => 'money.full_profit DESC',
),
'*',
),
)
));
}
E.g. I have a relation in my User model:
public function relations()
{
return array(
'clients' => array( self::HAS_MANY, 'Referrals', 'user_id'),
Let's say my month_profit will be equal: count of User's clients registered in last 30 days * 150. I need to somehow pass this data to search() and create CDbCriteria to sort users by month_profit. Is this even real? :) Should I create another function to calculate everything and then pass to search()? All my tries followed to failure so far.

I suppose you need something like the following, based on this guide http://www.yiiframework.com/wiki/319/searching-and-sorting-by-count-of-related-items-in-cgridview
First make a statistical query in relations at your User model:
'month_profit' => array(self::STAT, 'User', 'id', 'select'=>'COUNT(*) * 150', 'condition'=>'DATE_SUB(CURDATE(), INTERVAL 30 DAY) <= reg_date'),
Then mark "month_profit" attribute as safe in search scenario in rules.
public function rules() {
return array(
...
array('username, ... , month_profit', 'safe', 'on' => 'search' ),
);
}
Add all these where each one is needed in User search() method:
$user_table = User::model()->tableName();
$month_profit_sql = "(SELECT COUNT(*) FROM user_table WHERE DATE_SUB(CURDATE(), INTERVAL 30 DAY) <= reg_date)";
$criteria->select = array(
'*',
$requests_count_sql . "as requests_count",
$referrals_count_sql . "as referrals_count",
$referrals_payed_sql . "as referrals_payed_count",
$month_profit_sql . "as month_profit",
);
...
$criteria->compare($month_profit_sql, $this->month_profit);
return new CActiveDataProvider(get_class($this), array(
'criteria' => $criteria,
'pagination' => array( 'pageSize' => $pageSize ),
'sort' => array(
'defaultOrder' => $defaultOrder,
'attributes' => array(
'id' => array(
'asc' => '`t`.`id` ASC',
'desc' => '`t`.`id` DESC',
),
...
'month_profit' => array(
'asc' => 'month_profit ASC',
'desc' => 'month_profit DESC',
),
'*',
),
)
));
Finally, modify your grid:
$this->widget('zii.widgets.grid.CGridView', array(
'dataProvider' => $model->search(),
'filter' => $model,
'columns' => array(
'username',
... ,
'month_profit',
array(
'class'=>'CButtonColumn',
),
),
));
Please, let me know if that worked.
EDIT
If that doesnt' work try it without the statistical query.

Related

Single Pagination on multiple models in cakephp

My requirement is to search on 2 different tables : Vets and Clinics. There might be relation between them i.e result should fetch clinics have 'a' in name and vets having 'a' in them. Vets might be related to clinics or might not. Currently I'm doing the following. Is there any method to avoid running 2 queries which can also help me use the cakephp pagination helper?
$this->paginate = array(
'Vet' => array(
'conditions' => $conditions,
'fields' => array('Vet.id', 'Vet.name', 'Vet.professionnal_address', 'phone_number', 'Vet.email', 'Vet.type', 'Vet.latitude', 'Vet.longitude','Vet.city','Vet.clinic_id','Vet.zipcode'),
'joins' => array(
array(
'table' => 'vet_appointment_types',
'alias' => 'VetAppointmentType',
'type' => 'LEFT',
'conditions' => array(
'Vet.id = VetAppointmentType.vet_id',
)
)
),
'limit' => $limit,
'group' => array(
'Vet.id'
),
'order' => array(
'Vet.name' => 'ASC'
)
),
'Clinic' => array(
'conditions' => $conditions1,
'fields' => array('Clinic.*'),
'limit' => $limit,
'order' => array(
'Clinic.name' => 'ASC'
)
)
);
$results = $this->paginate('Vet');
$results2 = $this->paginate('Clinic');
I tried by extending the default pagination component in a non database model and using union but the database structure in a bit complex so can't use union. Also, i think implementing a temporary table based model would be an option, but since it'll be used for searching, so how exactly to go about implementing it, I'm unable to think. Any help would be +1'd ;)
As per the suggestion, I accomplished it by using database views. Below is the code. Any improvement suggestions is highly appreciated.
// \app\Model\Search.php
App::uses('ConnectionManager', 'Model');
App::uses('AppModel', 'Model');
App::uses('CakeLog', 'Log');
class Search extends AppModel {
public function __construct() {
parent::__construct();
$connection = ConnectionManager::getDataSource('default');
$return = $connection->execute("CREATE OR REPLACE VIEW `search` AS
SELECT Vet.id, (CONCAT( `Vet`.`fname` , ' ', `Vet`.`lname` )) AS name, 'vet' AS TYPE , Vet.latitude, Vet.longitude, Vet.zipcode, Vet.speciality FROM `db`.`vets` AS `Vet`
UNION
SELECT Clinic.id, Clinic.name, 'clinic' AS TYPE , Clinic.lat, Clinic.long, Clinic.zipcode, Clinic.address FROM `db`.`clinics` AS `Clinic`");
$return = ($return == true)?'Search View created successfully on '.date('Y-m-d H:i:s'):$return;
CakeLog::write('search', $return);
}
public $useTable = 'search';// This model does not use a database table
public $primaryKey = 'id'; // Define primary key
public $useDbConfig = 'default'; // Define db
}
This is the model.Everytime user searches anything, when the model is loaded in controller, view is created/replaced so that updated values can be fetched. It can be used as any other model in cakephp. It also supports virtualFields as well. To build your query for the database view you can also use the query builder which i used as follows.
$joins = array(
array(
'table' => 'vet_appointment_types',
'alias' => 'VetAppointmentType',
'type' => 'LEFT',
'conditions' => array(
'Vet.id = VetAppointmentType.vet_id',
)
)
);
$dbo = $this->Vet->getDataSource();
$subQuery = $dbo->buildStatement(
array(
'fields' => array('Vet.id', 'Vet.name'),
'table' => $dbo->fullTableName($this->Vet),
'alias' => 'Vet',
'limit' => $limit,
'group' => array(
'Vet.id'
),
'order' => array(
'Vet.name' => 'ASC'
),
'offset' => null,
'joins' => $joins,
'conditions' => $conditions
),
$this->Vet
);
$query = $subQuery;
$query .= ' UNION ';
$dbo = $this->Clinic->getDataSource();
$subQuery = $dbo->buildStatement(
array(
'fields' => array('Clinic.id', 'Clinic.name'),
'table' => $dbo->fullTableName($this->Clinic),
'alias' => 'Clinic',
'limit' => $limit,
'conditions' => $conditions1,
'limit' => $limit,
'order' => array(
'Clinic.name' => 'ASC'
),
'offset' => null
),
$this->Clinic
);
$query .= $subQuery;
print_r($query);

CGridView column based on relation displays incorrect data

I am currently working on a web application (written in PHP, based on Yii), which, among other things, lets you connect different price values for products. Each product can have multiple prices, but the system is built in a way that I can easily (and correctly) determine the type of the price field - so while each product can have multiple price fields, it can only have one of each type of price fields.
The part where I stuck is when I have to display the stored value of these fields in a list, and order the list view by them - I can display them correctly and sort them by one column, but as soon as I try to make the list to be sortable by all of the columns (not at the same time, of course), the rows start to display the wrong values.
Here are the relevant pieces of code:
In the model, at relations:
'productPriceConnects' => array(Product::HAS_MANY, 'ProductPriceConnect', 'product_id'),
'price1' => array(Product::HAS_ONE, 'ProductPriceConnect', 'product_id',
'joinType' => 'LEFT JOIN',
'on' => 'priceBeszerzesi.product_price_field_id=:product_price_field_id AND priceBeszerzesi.active = 1',
'params' => array(':product_price_field_id' =>ProductPriceField::model()->findByAttributes(array('active' => 1, 'type' => 1, 'category' => 6))->id),
'alias' => 'priceBeszerzesi',
),
'price2' => array(Product::HAS_ONE, 'ProductPriceConnect', 'product_id',
'joinType' => 'LEFT JOIN',
'on' => 'priceEladasi.product_price_field_id=:product_price_field_id AND priceEladasi.active = 1',
'params' => array(':product_price_field_id' => ProductPriceField::model()->findByAttributes(array('active' => 1, 'type' => 1, 'category' => 1))->id),
'alias' => 'priceEladasi'
),
'price3' => array(Product::HAS_ONE, 'ProductPriceConnect', 'product_id',
'joinType' => 'LEFT JOIN',
'on' => 'priceAkcios.product_price_field_id=:product_price_field_id AND priceAkcios.active = 1',
'params' => array(':product_price_field_id' => ProductPriceField::model()->findByAttributes(array('active' => 1, 'type' => 1, 'category' => 5))->id),
'alias' => 'priceAkcios'
),
In the model, at search:
...
$criteria->with('price1', 'price2', 'price3);
...
$criteria->compare('price1.price', $this->beszerzesi, true);
$criteria->compare('price2.price', $this->eladasi, true);
$criteria->compare('price3.price', $this->akcios, true);
...
$sort->attributes = array(
'price1' =>array(
'asc' => 'priceBeszerzesi.price ASC',
'desc' => 'priceBeszerzesi.price DESC',
),
'price2' =>array(
'asc' => 'priceEladasi.price ASC',
'desc' => 'priceEladasi.price DESC',
),
'priceAkcios' =>array(
'asc' => 'price3.price ASC',
'desc' => 'price3.price DESC',
),
'*'
);
...
return new CActiveDataProvider($this, array(
'criteria' => $criteria,
'sort' => $sort,
'pagination' => array(
'pageSize' => 10,
),
)
);
The columns' data in gridview:
'eladasi' => array(
'name' => 'price2',
'header' => 'Eladási ár',
'type' => 'raw',
'headerHtmlOptions' => array('class' => 'auto-width text-center'),
'value' => '!empty($data->price2->price) ? $data->price2->price == "" ? "-" : $data->price2->price : "-"',
'htmlOptions' => array('class' => 'border-right auto-width text-center'),
),
'akcios' => array(
'name' => 'priceAkcios',
'header' => 'Akciós',
'value' => '!empty($data->price3->price) ? $data->price3->price == "" ? "-" : $data->price3->price : "-"',
'headerHtmlOptions' => array('class' => 'auto-width text-center'),
'htmlOptions' => array('class' => 'border-right auto-width text-center'),
),
'beszerzesi' => array(
'name' => 'price1',
'header' => 'Beszerzési ár',
'type' => 'raw',
'value' => '!empty($data->price1->price) ? $data->price1->price == "" ? "-" : $data->price1->price : "-"',
'headerHtmlOptions' => array('class' => 'auto-width text-center'),
'htmlOptions' => array('class' => 'border-right auto-width text-center'),
),
This code makes it possible to sort the list by all three of the relation-dependent columns, but every column displays the same value - the value of the last relation in the with array. If the last element in the array is price3, then the columns display the value of the price3 relation.
When I remove all relation names from the with array expect one, I can sort the list by that column, but not the others.
My question is this:
Is there any way to
1) surely add any number of relations to a model, connecting to the same db field, but depending on conditions,
2) and display these values WHILE enabling sorting based on them?
Find solution below:
I created these tables at my system and used your relation and gridview code. I made some changes in that code and now in below code searching and sorting is working perfectly.
I defined three variable in model class i.e.
public $beszerzesi;
public $eladasi;
public $akcios;
Then I changed the param's name in relation arraye used with left join. This was the main issue in your code. You used same name for parameters i.e. :product_price_field_id I assigned different name for each parameters. While yii prepare sql query it will replace the parameters which are assigned to query. In your case it was replacing same value for all three parameters.
Also i made some changes in sort and compare attributes passed in CActiveDataProvider. You can find all changes in below model file.
Product.php
<?php
/**
* This is the model class for table "product".
*
* The followings are the available columns in table 'product':
* #property integer $id
* #property string $name
*/
class Product extends CActiveRecord
{
public $beszerzesi;
public $eladasi;
public $akcios;
/**
* #return string the associated database table name
*/
public function tableName()
{
return 'product';
}
/**
* #return array validation rules for model attributes.
*/
public function rules()
{
// NOTE: you should only define rules for those attributes that
// will receive user inputs.
return array(
array('name', 'required'),
array('name', 'length', 'max' => 100),
// The following rule is used by search().
// #todo Please remove those attributes that should not be searched.
array('id, name, beszerzesi, eladasi,akcios', 'safe', 'on' => 'search'),
);
}
/**
* #return array relational rules.
*/
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'productPriceConnects' => array(Product::HAS_MANY, 'ProductPriceConnect', 'product_id'),
'price1' => array(Product::HAS_ONE, 'ProductPriceConnect', 'product_id',
'joinType' => 'LEFT JOIN',
'on' => 'priceBeszerzesi.product_price_field_id=:product_price_field_id1 AND priceBeszerzesi.active = 1',
'params' => array(':product_price_field_id1' => ProductPriceField::model()->findByAttributes(array('active' => 1, 'type' => 1, 'category' => 6))->id),
'alias' => 'priceBeszerzesi',
),
'price2' => array(Product::HAS_ONE, 'ProductPriceConnect', 'product_id',
'joinType' => 'LEFT JOIN',
'on' => 'priceEladasi.product_price_field_id=:product_price_field_id2 AND priceEladasi.active = 1',
'params' => array(':product_price_field_id2' => ProductPriceField::model()->findByAttributes(array('active' => 1, 'type' => 1, 'category' => 1))->id),
'alias' => 'priceEladasi'
),
'price3' => array(Product::HAS_ONE, 'ProductPriceConnect', 'product_id',
'joinType' => 'LEFT JOIN',
'on' => 'priceAkcios.product_price_field_id=:product_price_field_id3 AND priceAkcios.active = 1',
'params' => array(':product_price_field_id3' => ProductPriceField::model()->findByAttributes(array('active' => 1, 'type' => 1, 'category' => 5))->id),
'alias' => 'priceAkcios'
),
);
}
/**
* #return array customized attribute labels (name=>label)
*/
public function attributeLabels()
{
return array(
'id' => 'ID',
'name' => 'Name',
);
}
/**
* Retrieves a list of models based on the current search/filter conditions.
*
* Typical usecase:
* - Initialize the model fields with values from filter form.
* - Execute this method to get CActiveDataProvider instance which will filter
* models according to data in model fields.
* - Pass data provider to CGridView, CListView or any similar widget.
*
* #return CActiveDataProvider the data provider that can return the models
* based on the search/filter conditions.
*/
public function search()
{
// #todo Please modify the following code to remove attributes that should not be searched.
$criteria = new CDbCriteria;
$criteria->with = array('price1', 'price2', 'price3');
$criteria->compare('id', $this->id);
$criteria->compare('name', $this->name, true);
$criteria->compare('priceBeszerzesi.price', $this->beszerzesi, true);
$criteria->compare('priceEladasi.price', $this->eladasi, true);
$criteria->compare('priceAkcios.price', $this->akcios, true);
// $criteria->attributes = ;
return new CActiveDataProvider($this, array(
'criteria' => $criteria,
'sort' => array(
'attributes' => array(
'beszerzesi' => array(
'asc' => 'priceBeszerzesi.price',
'desc' => 'priceBeszerzesi.price DESC',
),
'eladasi' => array(
'asc' => 'priceEladasi.price',
'desc' => 'priceEladasi.price DESC',
),
'akcios' => array(
'asc' => 'priceAkcios.price',
'desc' => 'priceAkcios.price DESC',
),
'*'
)
),
'pagination' => array(
'pageSize' => 10,
),
)
);
}
/**
* Returns the static model of the specified AR class.
* Please note that you should have this exact method in all your CActiveRecord descendants!
* #param string $className active record class name.
* #return Product the static model class
*/
public static function model($className = __CLASS__)
{
return parent::model($className);
}
}
gridview code
<?php $this->widget('zii.widgets.grid.CGridView', array(
'id' => 'product-grid',
'dataProvider' => $model->search(),
'filter' => $model,
'columns' => array(
'id',
'name',
'eladasi' => array(
'name' => 'eladasi',
'header' => 'Eladási ár',
'type' => 'raw',
'headerHtmlOptions' => array('class' => 'auto-width text-center'),
'value' => '!empty($data->price2->price) ? $data->price2->price == "" ? "-" : $data->price2->price : "-"',
'htmlOptions' => array('class' => 'border-right auto-width text-center'),
),
'akcios' => array(
'name' => 'akcios',
'header' => 'Akciós',
'value' => '!empty($data->price3->price) ? $data->price3->price == "" ? "-" : $data->price3->price : "-"',
'headerHtmlOptions' => array('class' => 'auto-width text-center'),
'htmlOptions' => array('class' => 'border-right auto-width text-center'),
),
'beszerzesi' => array(
'name' => 'beszerzesi',
'header' => 'Beszerzési ár',
'type' => 'raw',
'value' => '!empty($data->price1->price) ? $data->price1->price == "" ? "-" : $data->price1->price : "-"',
'headerHtmlOptions' => array('class' => 'auto-width text-center'),
'htmlOptions' => array('class' => 'border-right auto-width text-center'),
),
array(
'class' => 'CButtonColumn',
),
),
)); ?>
You can found step by step guide for the searching and sorting on relation data at Searching and sorting by related model in CGridView | Wiki | Yii PHP Framework

Yii Framework Undefined Offset: 0

I am trying to use CGridView with custom query, and trying to build a very simple with no sorting and stuff.
My View contains simple CGridView
$this->widget('zii.widgets.grid.CGridView', array(
'dataProvider'=>$dataProvider,
));
And my controller passes the $dataProvider to the view
$count=Yii::app()->db->createCommand('SELECT COUNT(*) FROM ( ' . $query . ' ) as count')->queryScalar();
$dataProvider=new CSqlDataProvider($query, array(
'keyField' => false,
'totalItemCount'=>$count,
'pagination'=>array(
'pageSize'=>10,
),
));
I don't have a keyField therefore I have set it to false. Moreover, I have tried printing out data using var_dump, data is present in the variable, but still I get this undefined offset error.
You need to set the mapping for sorting.
/*
Query results
array(
array(
'id' => 1,
'username' => 'username',
'email' => 'email'
),
...
)
*/
return new CSqlDataProvider($query, array(
'keyField' => 'id', //required, any field from query results
'totalItemCount'=> $count,
'pagination' => array(
'pageSize' => 10
),
'sort' => array(
'defaultOrder' => array(
'username' => CSort::SORT_DESC,
),
'attributes' => array(
'username',
'email',
),
),
));
//grid.columns
array(
array(
'name' => 'id' //WO sort
),
array(
'name' => 'username', //with sort (isset in dp.sort.attributes)
),
)
you have to provide keyfield other than false, change keyfield primary key of your query Table

how to apply sorting on model method in CGridView Yii

I have User model which contain a function that calculates the average revenue per user. Now i want apply sorting in CGridView on the column which is associated with getAvgRevenue() function. While license is relation in the User model.
Here is my code,
public class User{
$user_id;
$email;
public function getAvgRevenue(){
$count = 0;
$revenue = 0;
foreach ($this->license as $license){
$revenue += $license->price;
$count++;
}
if($count!= 0){
$averageRevenue = $revenue/$count;
return $averageRevenue;
}
else{
return null;
}
}
}
In Controller
$modelUser = new CActiveDataProvider('User', array(
'sort' => array(
'attributes' => array(
'user_id',
'email',
'averagerevenue'
),
'defaultOrder' => array(
'user_id' => CSort::SORT_ASC,
),
),
));
In view
<?php $this->widget('zii.widgets.grid.CGridView', array(
'id' => 'user-grid',
'dataProvider' => $modelUser,
'columns' => array(
array(
'header' => 'User ID',
'name' => 'user_id',
),
array(
'header' => 'Email',
'name' => 'email',
),
array(
'header'=>'Average Revenue',
'value'=>'$data->averagerevenue',
),
)
));
?>
Sorting is applicable on user_id and email but Average Revenue column is not sortable. how to specify model method in sort() of CActiveDataprovider
Please help me to solve the problem.
Try this:
$modelUser = new CActiveDataProvider('User', array(
'sort' => array(
'attributes' => array(
'user_id',
'email',
'avgRevenue' //here's the change for you
),
'defaultOrder' => array(
'user_id' => CSort::SORT_ASC,
),
),
));
And your gridview column should be:
array(
'header'=>'Average Revenue',
'value'=>'avgRevenue',
),
and you can read more info on it over here:
http://www.yiiframework.com/wiki/167/understanding-virtual-attributes-and-get-set-methods/

How to make CGridView column sortable using CSqldataprovider?

How to make CGridView columns sortable (On clicking column title) using CSqldataprovider.
In controller
$sql = "select id ,name, address
from User
where city = 'ABC' ";
$rawData = Yii::app()->db->createCommand($sql);
return $allMovies = new CSqlDataProvider($rawData, array(
'keyField' => 'id',
'sort'=>array(
'attributes'=>array(
'id', 'name', 'address',
),
),
'pagination' => array(
'pageSize' => 10,
),
));
In view
$this->widget('zii.widgets.grid.CGridView', array(
'id' => 'all_movies',
'dataProvider' => $allMoviesStats,
'columns' => array(
'id',
'name',
'address',
'city'
)
)
);?>
It is giving error
"error":{"code":99,"text":"Property \"CGridView.sort\" is not defined.
You need to create object for CDbCriteria and Sort class and try to use as below and change the code for your requirement.
public function search()
{
$criteria=new CDbCriteria;
$criteria->condition="active=1";
if($this->name=="Enter Country Name" || $this->name=='') {
$this->name='';
} else {
$this->name=$this->name;
}
$criteria->compare('name',$this->name,true);
$sort = new CSort;
$sort->defaultOrder = 'id DESC';
$sort->attributes = array(
'name' => array(
'asc' =>'name',
'desc' =>'name DESC',
),
...
... // attributes to sort
);
return new CActiveDataProvider('Country', array( //Country is nothing but you model class name
'criteria' =>$criteria,
'sort' => $sort,
'pagination'=>array('pageSize'=> 10),
));
}

Categories