Yii2 : how to cache active data provider? - php

In my PostSearch model I have this code :
public function search($params)
{
$query = Post::find()->where(['status' => 1]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
'sort'=> ['defaultOrder' => ['id' => SORT_DESC]],
'pagination' => [
'pageSize' => 10,
]
]);
if (!($this->load($params) && $this->validate())) {
return $dataProvider;
}
$query->andFilterWhere([
'id' => $this->id,
'status' => $this->status,
]);
$query->andFilterWhere(['like', 'title', $this->title])
->andFilterWhere(['like', 'text', $this->text]);
return $dataProvider;
my try, instead of above line return $dataProvider, would be this block of code:
$dependency = [
'class' => 'yii\caching\DbDependency',
'sql' => 'SELECT MAX(updated_at) FROM post',
];
$result = self::getDb()->cache(function ($db) {
return $dataProvider;
}, 3600, $dependency);
return $result
I would like to cache the result returned by ADP, based on the updated_at field. I mean I want to serve data from cache until some change is made. My code does not work, I mean caching is not applied at all. What I am doing wrong, and is it possible to do this on ADP ? Thanks

It has little use caching the data provider after instantiating, since it's not actually doing any selecting on the database until it has been prepared. So you would actually be caching an empty object instance like it is now.
If you have a very large set of records, call the dataProviders' prepare() in advance in the cache:
self::getDb()->cache(function ($db) use ($dataProvider) {
$dataProvider->prepare();
}, 3600, $dependency);
return $dataProvider;
This will actually cache whatever queries the dataProvider runs ,so the next time they will be fetched from the query cache. This should result in what you are looking for.
If you have a finite amount of records, caching them all at once could also work:
$key = 'MyCachedData'; // + Data uniquely referring to your search parameters
$cache = \Yii::$app->cache;
$dataProvider = $cache->get($key);
if (!$dataProvider) {
$dependency = \Yii::createObject([
'class' => 'yii\caching\DbDependency',
'sql' => 'SELECT MAX(updated_at) FROM post',
]);
$dataProvider = new \yii\data\ArrayDataProvider;
$dataProvider->allModels = $query->all();
$cache->set($key, $dataProvider, 3600, $dependency)
}
return $dataProvider;
Obviously this is less than ideal for larger datasets, but it depends on what you are looking for.

Related

Yii2 GridView Filters behaving not working properly

Yii2 Gridiview filter not working properly. Selecting one filter have impact on other filters. Changing one filter (dropdown) auto-select the values of other filters (dropdowns). This problem also exists in URL as well, changing one filter appends the other filters in URL as well and result shown as combined. but in reality only one filter should be applied which is being changed.
// Search Model, adding dummy table names
public function search($params)
{
$query = Model::find()->with('model_b');
if (empty($params['sort'])) {
$query->orderBy("group, " . Model::getSortByType() . ', "title"');
}
$dataProvider = new ActiveDataProvider([
'query' => $query,
'sort' => [
'attributes' => [
'code',
'title',
'updated_at'
]
]
]);
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
// grid filtering conditions
$query->andFilterWhere([
'type' => $this->type,
'price_type' => $this->price_type,
'status' => $this->status,
'terms_related' => $this->terms_related,
'required' => $this->required,
'group' => $this->group,
]);
$query->andFilterWhere(['ilike', 'title', $this->title]);
$query->andFilterWhere(['is_qr' => $this->is_qr]);
return $dataProvider;
}
//Controller
public function actionIndex()
{
$searchModel = new ModelSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render("index", [
"searchModel" => $searchModel,
"dataProvider" => $dataProvider,
]);
}
// In View, the filter I change
[
'attribute' => 'is_qr',
'format' => 'boolean',
'filter' => [1 => 'TRUE', 0 => 'FALSE'],
'content' => function ($service) { return ((int) $service->is_qr === 1) ? 'TRUE' : 'FALSE'; }
],
// the filter being changed with above filter
[
'attribute' => 'terms_related',
'filter' => array(0 => 'FALSE', 1 => 'TRUE'),
'content' => function ($service) { return ((int) $service->terms_related === 1) ? 'TRUE' : 'FALSE'; }
]
Observations:
Consider I have 5 filters in a GridView.
Action 1: I changed a filter, only that filter is applied first time but after page reload, other filters are being populated with values with "0". Because on selecting one filter, all filters are being pushed in URL with empty values other than selected one. And filters with empty values are being applied to rest of the filters with "0" value
Problem
The problem is, once I select a filter, gridview sends all possible filters in URL. The filters I did not select, have empty values.
Yii::$app->request->queryParams
This has all filters and the filters other than I selected have empty values, and
$this->load($params);
in search() deals empty values as 0. So, filters that I have not touched are being populated with "0" value.
I have found the solution, it is a custom solution but works for me.
I created a Trait
trait ParamsTrimable
{
public function trimParams($params, $modelClass)
{
$modelClass = basename(str_replace('\\', '/', $modelClass));
if ($params[$modelClass]) {
$params[$modelClass] = array_filter($params[$modelClass], function ($value) {
return ($value !== '');
});
}
return $params;
}
}
And before
$this->load($params);
I called trait's function i.e.
$params = $this->trimParams($params, static::class);
$this->load($params);
Reason behind the trait solution is, this problem may occur in other listings as well. To fix, we only need to use trait and call the function to remove empty values from params.

Complex Database Queries in yii2 with Active Record

TL;DR
I have a query that works in RAW SQL but i have had little success recreating it with query builder or active record.
I am working on a web application based off of the yii2 advanced application template. I have written a database query and implemented it with findbysql() returning the correct records but am having trouble translating this into active record.
I originally wanted to allow the user to modify (filter) the results by means of a search form(user & date), however i have since realized that implementing filters on the gridview with active records would be smoother.
I have gotten simple queries to work however am unsure of how to implement one with this many joins. Many examples used sub queries but my attempts failed to return any records at all. I figured before I attempt filters i need to transcribe this query first.
videoController.php
public function actionIndex()
{
$sql = 'SELECT videos.idvideo, videos.filelocation, events.event_type, events.event_timestamp
FROM (((ispy.videos videos
INNER JOIN ispy.cameras cameras
ON (videos.cameras_idcameras = cameras.idcameras))
INNER JOIN ispy.host_machines host_machines
ON (cameras.host_machines_idhost_machines =
host_machines.idhost_machines))
INNER JOIN ispy.events events
ON (events.host_machines_idhost_machines =
host_machines.idhost_machines))
INNER JOIN ispy.staff staff
ON (events.staff_idreceptionist = staff.idreceptionist)
WHERE (staff.idreceptionist = 182)
AND (events.event_type IN (23, 24))
AND (events.event_timestamp BETWEEN videos.start_time
AND videos.end_time)';
$query = Videos::findBySql($sql);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}
Failed Attempt
public function actionIndex()
{
$query = Videos::find()
->innerJoin('cameras', 'videos.cameras_idcameras = cameras.idcameras')
->innerJoin('host_machines', 'cameras.host_machines_idhost_machines = host_machines.idhost_machines')
->innerJoin('events', 'events.host_machines_idhost_machines = host_machines.idhost_machines')
->innerJoin('staff', 'events.staff_idreceptionist = staff.idreceptionist')
->where('staff.idreceptionist = 182')
->andWhere(['events.event_type' => [23,24]])
->andwhere(['between', 'events.event_timestamp', 'videos.start_time', 'videos.end_time']);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}
Portion of View
<?= GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'idvideo',
'event_type',
'event_timestamp',
'filelocation',
//['class' => 'yii\grid\ActionColumn'],
],
]); ?>
Please let me know if i need to be more specific or include any additional information.
Thanks ahead
i will assume, based on the question you asked here you liked in comments that you provided the entire query
(no other fields, that you took out just to show sample code)
therefore, if you only need only the fields specified in SELECT statement, you can optimize your query quite a bit:
first off, you're joining with host_machines only to link cameras and events, but have the same key host_machines_idhost_machines on both, so that's not needed, you can directly:
INNER JOIN events events
ON (events.host_machines_idhost_machines =
cameras.host_machines_idhost_machines))
secondly, the join with ispy.staff, the only used field is idreceptionist in WHERE clause, that field exists in events as well so we can drop it completly
the final query here:
SELECT videos.idvideo, videos.filelocation, events.event_type, events.event_timestamp
FROM videos videos
INNER JOIN cameras cameras
ON videos.cameras_idcameras = cameras.idcameras
INNER JOIN events events
ON events.host_machines_idhost_machines =
cameras.host_machines_idhost_machines
WHERE (events.staff_idreceptionist = 182)
AND (events.event_type IN (23, 24))
AND (events.event_timestamp BETWEEN videos.start_time
AND videos.end_time)
should output the same records as the one in your question, without any identitcal rows
some video duplicates will still exists due to one to many relation between cameras and events
now to the yii side of things,
you have to define some relations on the Videos model
// this is pretty straight forward, `videos`.`cameras_idcameras` links to a
// single camera (one-to-one)
public function getCamera(){
return $this->hasOne(Camera::className(), ['idcameras' => 'cameras_idcameras']);
}
// link the events table using `cameras` as a pivot table (one-to-many)
public function getEvents(){
return $this->hasMany(Event::className(), [
// host machine of event => host machine of camera (from via call)
'host_machines_idhost_machines' => 'host_machines_idhost_machines'
])->via('camera');
}
the VideoController and the search function itself
public function actionIndex() {
// this will be the query used to create the ActiveDataProvider
$query =Video::find()
->joinWith(['camera', 'events'], true, 'INNER JOIN')
->where(['event_type' => [23, 24], 'staff_idreceptionist' => 182])
->andWhere('event_timestamp BETWEEN videos.start_time AND videos.end_time');
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}
yii will treat each video as a single record (based on pk), that means that all video duplicates are
removed. you will have single videos, each with multiple events so you wont be able to use 'event_type'
and 'event_timestamp' in the view but you can declare some getters inside Video model to show that info:
public function getEventTypes(){
return implode(', ', ArrayHelper::getColumn($this->events, 'event_type'));
}
public function getEventTimestamps(){
return implode(', ', ArrayHelper::getColumn($this->events, 'event_timestamp'));
}
and the view use:
<?= GridView::widget([
'dataProvider' => $dataProvider,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'idvideo',
'eventTypes',
'eventTimestamps',
'filelocation',
//['class' => 'yii\grid\ActionColumn'],
],
]); ?>
edit:
if you want to keep the video duplicates, declare the two columns from events inside Video model
public $event_type, $event_timestamp;
keep the original GridView setup, and add a select and indexBy this to the query inside VideoController:
$q = Video::find()
// spcify fields
->addSelect(['videos.idvideo', 'videos.filelocation', 'events.event_type', 'events.event_timestamp'])
->joinWith(['camera', 'events'], true, 'INNER JOIN')
->where(['event_type' => [23, 24], 'staff_idreceptionist' => 182])
->andWhere('event_timestamp BETWEEN videos.start_time AND videos.end_time')
// force yii to treat each row as distinct
->indexBy(function () {
static $count;
return ($count++);
});
update
a direct staff relation to Video is currently somewhat problematic since that is more than one table away from it.
there's an issue about it here
however, you add the staff table by linking it to the Event model,
public function getStaff() {
return $this->hasOne(Staff::className(), ['idreceptionist' => 'staff_idreceptionist']);
}
that will allow you to query like this:
->joinWith(['camera', 'events', 'events.staff'], true, 'INNER JOIN')
Filtering will require some small updates on the controller, view and a SarchModel
here's a minimal implementation:
class VideoSearch extends Video
{
public $eventType;
public $eventTimestamp;
public $username;
public function rules() {
return array_merge(parent::rules(), [
[['eventType', 'eventTimestamp', 'username'], 'safe']
]);
}
public function search($params) {
// add/adjust only conditions that ALWAYS apply here:
$q = parent::find()
->joinWith(['camera', 'events', 'events.staff'], true, 'INNER JOIN')
->where([
'event_type' => [23, 24],
// 'staff_idreceptionist' => 182
// im guessing this would be the username we want to filter by
])
->andWhere('event_timestamp BETWEEN videos.start_time AND videos.end_time');
$dataProvider = new ActiveDataProvider(['query' => $q]);
if (!$this->validate())
return $dataProvider;
$this->load($params);
$q->andFilterWhere([
'idvideo' => $this->idvideo,
'events.event_type' => $this->eventType,
'events.event_timestamp' => $this->eventTimestamp,
'staff.username' => $this->username,
]);
return $dataProvider;
}
}
controller:
public function actionIndex() {
$searchModel = new VideoSearch();
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render('test', [
'searchModel' => $searchModel,
'dataProvider' => $dataProvider,
]);
}
and the view
use yii\grid\GridView;
use yii\helpers\ArrayHelper;
echo GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
'idvideo',
'filelocation',
[
'attribute' => 'eventType', // from VideoSearch::$eventType (this is the one you filter by)
'value' => 'eventTypes' // from Video::getEventTypes() that i suggested yesterday
// in hindsight, this could have been named better, like Video::formatEventTypes or smth
],
[
'attribute' => 'eventTimestamp',
'value' => 'eventTimestamps'
],
[
'attribute' => 'username',
'value' => function($video){
return implode(', ', ArrayHelper::map($video->events, 'idevent', 'staff.username'));
}
],
//['class' => 'yii\grid\ActionColumn'],
],
]);
My recommendation would be to have 2 queries. The first one to get the ids of the videos that fit your search, the second query theone that uses those ids and feeds your $dataProvider.
use yii\helpers\ArrayHelper;
...
public function actionIndex()
{
// This is basically the same query you had before
$searchResults = Videos::find()
// change 'id' for the name of your primary key
->select('id')
// we don't really need ActiveRecord instances, better use array
->asArray()
->innerJoin('cameras', 'videos.cameras_idcameras = cameras.idcameras')
->innerJoin('host_machines', 'cameras.host_machines_idhost_machines = host_machines.idhost_machines')
->innerJoin('events', 'events.host_machines_idhost_machines = host_machines.idhost_machines')
->innerJoin('staff', 'events.staff_idreceptionist = staff.idreceptionist')
->where('staff.idreceptionist = 182')
->andWhere(['events.event_type' => [23,24]])
->andwhere(['between', 'events.event_timestamp', 'videos.start_time', 'videos.end_time'])
// query the results
->all();
// this will be the query used to create the ActiveDataProvider
$query = Videos::find()
// and we use the results of the previous query to filter this one
->where(['id' => ArrayHelper::getColumn($searchResults, 'id')]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
return $this->render('index', [
'dataProvider' => $dataProvider,
]);
}

Site and its behaviors do not have a method or closure named "orderBy"

while i am fetching this records then getting this error how to solve it ?
public function actionIndex()
{
$query = Site::model();
$pagination = new CPagination([
'defaultPageSize' => 5,
'totalCount' => $query->count(),
]);
$countries = $query->orderBy('name')
->offset($pagination->offset)
->limit($pagination->limit)
->all();
return $this->render('view', [
'countries' => $countries,
'pagination' => $pagination,
]);
}
you should distinguish between yii1 and yii2.from your codes,you used yii1 and yii2 together.

Sorting calculated fields in Yii2 (Grid View)

i am need to sort some fields (asc,desc) in GridView, but same fields are calculated. Look at code below:
SearchModel:
class ObjectSearch extends Object {
use SearchModelTrait;
public function rules()
{
return [
['id', 'integer', 'min' => 1],
];
}
public function search($params)
{
$this->company_id = \Yii::$app->user->identity->companyId;
$query = Object::find()->where(['company_id' => $this->company_id]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => false,
]);
$dataProvider->setSort([
'attributes' => [
'id',
'name',
'lastReportResult' => [
'asc' => ['lastReportResult' =>SORT_ASC ],
'desc' => ['lastReportResult' => SORT_DESC],
'default' => SORT_ASC
],
'reportPercentDiff'
]
]);
if (!($this->load($params,'ObjectSearch') && $this->validate())) {
return $dataProvider;
}
$this->addCondition($query, 'id');
return $dataProvider;
}
Methods in Object model:
public function getLastReportResult()
{
$lastReport = $this->getLastReport();
$message = 0;
if (!empty($lastReport)) {
$statistic = new ReportStatistic($lastReport);
$message = $statistic->getPercent();
}
return $message;
}
/**
* #return int
*/
public function getReportPercentDiff()
{
$lastReport = $this->getLastReport();
$message = 0;
if (!empty($lastReport)) {
$statistic = $lastReport->getReportDiff();
if (!empty($statistic['diff'])) {
$message = $statistic['diff']['right_answers_percent_diff'];
} elseif (!empty($statistic['message'])) {
$message = $statistic['message'];
}
}
return $message;
}
So, by this methods, i am calculating a values of two fields, which are need's sorting. This way doesn't working, i have a Database Exception, because object table hasn't this fields. exception
How to do sorting of this fields ?
Update: I am the author of this answer and this answer is not accurate. Preferred way is to use database view
Add two public properties to ObjectSearch.php and mark it as safe
class ObjectSearch extends Object {
use SearchModelTrait;
public $lastReportResult, $reportPercentDiff;
public function rules()
{
return [
['id', 'integer', 'min' => 1],
[['lastReportResult', 'reportPercentDiff'], 'safe']
];
}
public function search($params)
{
$this->company_id = \Yii::$app->user->identity->companyId;
$query = Object::find()->where(['company_id' => $this->company_id]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
'pagination' => false,
]);
$dataProvider->setSort([
'attributes' => [
'id',
'name',
'lastReportResult' => [
'asc' => ['lastReportResult' =>SORT_ASC ],
'desc' => ['lastReportResult' => SORT_DESC],
'default' => SORT_ASC
],
'reportPercentDiff' => [
'asc' => ['reportPercentDiff' =>SORT_ASC ],
'desc' => ['reportPercentDiff' => SORT_DESC],
'default' => SORT_ASC
],
]
]);
if (!($this->load($params,'ObjectSearch') && $this->validate())) {
return $dataProvider;
}
$this->addCondition($query, 'id');
return $dataProvider;
}
Then in index.php (view file in which you are having grid view) add lastReportResult and reportPercentDiff in array of all attributes (list of all attributes ob Object model)
...
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' => $searchModel,
'columns' => [
['class' => 'yii\grid\SerialColumn'],
// your other attribute here
'lastReportResult',
'reportPercentDiff',
['class' => 'yii\grid\ActionColumn'],
],
]); ?>
...
For more info you can visit Kartik's blog at Yii
Though this is an old thread, stumbled upon this and tried to find other method to achieve sorting of purely calculated field to no avail... and this post unfortunately is not an answer as well... It just that I feel the need to post it here to give a heads up to those that still looking for the solution so as not to scratch their heads when trying the solution given and still fail.
The given example from documentation or referred links as far as I have tested only works if you have a column within the database schema (whether in the main table or the related tables). It will not work if the virtual attribute/calculated field you create is based on calculating (as an example multiplication of 2 column on the table)
e.g:
table purchase: | purchase_id | product_id | quantity |
table product: | product_id | unit_price |
then, if we use a virtual attribute 'purchase_total' for model 'purchase' which is the multiplication of quantity and unit_price (from the join table of purchase and product on product_id), eventually you will hit an error saying 'purchase_total' column can not be found when you tried to sort them using the method discussed so far.

Yii2 only display data where posted on date is less than or equal to current datetime

I have a field within my database called publish_on_date where I store the date the post should become active.
Now I am unsure on how to add the query to my dataprovider/search model.
So I need to add a query that will query the display date and check if it is less than or equal to now (now being the current date/server date).
Here is my current search functon
public function search($params, $pageSize = 3, $published = false)
{
$query = Article::find();
// this means that editor is trying to see articles
// we will allow him to see published ones and drafts made by him
if ($published === true)
{
$query->where(['status' => Article::STATUS_PUBLISHED]);
$query->orWhere(['user_id' => Yii::$app->user->id]);
}
$dataProvider = new ActiveDataProvider([
'query' => $query,
'sort'=> ['defaultOrder' => ['id' => SORT_DESC]],
'pagination' => [
'pageSize' => $pageSize,
]
]);
if (!($this->load($params) && $this->validate())) {
return $dataProvider;
}
$query->andFilterWhere([
'id' => $this->id,
'user_id' => $this->user_id,
'status' => $this->status,
'category' => $this->category,
]);
$query->andFilterWhere(['like', 'title', $this->title])
->andFilterWhere(['like', 'summary', $this->summary])
->andFilterWhere(['like', 'content', $this->content]);
return $dataProvider;
}
Just add someplace another condition with
$query->andWhere('publish_on_date <= NOW()');
You can add this when you first create the query
$query = Article::find()->andWhere('publish_on_date <= NOW()');
or later down the page

Categories