CakePHP 3 Paginator using FIELD() in ORDER clause - mysql

I have a question regarding the Paginator Component of CakePHP3 (3.0.13). I'm using the MYSQL function FIELD() to order my data, like that:
$this->paginate = array_merge_recursive([
'conditions' => [
'zip IN' => $zips
],
'order' => [
'FIELD(zip, '.rtrim(implode(',', $zips), ',').')',
'ispro' => 'desc',
],
$this->paginate
]);
When I apply this to my $this->paginate() it wouldn't be recognized as long as the query param sort is set. To avoid this I remove the sort in my request with:
if (isset($this->request->query['sort'])) {
unset($this->request->query['sort']);
}
This is working, but I was wondering if there is a better solution, maybe in the paginator component itself, which I haven't found yet.

you can sort your query before you paginate it. Your sort conditions will come before the one appended from the paginator component
so you can do
$dentists = $this->Dentists->find()
->order([
'FIELD(zip, '.rtrim(implode(',', $zips), ',').')',
'ispro' => 'desc'
]);
$this->paginate = array_merge_recursive([
'conditions' => [
'zip IN' => $zips
],
$this->paginate
]);
$dentists = $this->paginate($dentists);

Related

Setting a default value on join table from another table when model is edited or created

Hoping someone may be able to point me in the right direction.
I have a app that consists of (among other things) Recommendations and Assessments. They are joined with a join table that includes extra fields that I would like to update but am struggling to figure out how.
As you can see above, when I create a Reccommendation, I set the following fields:
default_user_impact
default_business_impact
default_deployment_complexity
default_criticality
Now when I create a new Assessment or edit one that has not got any Recommendations linked the Assessment saves fine because nothing is needing to be written to the join table.
When I try to edit an Assessment to include one or more Recommendations, the app tries to write the link to the join table and fails because the user_impact, business_impact, deployment_complexity and criticality fields aren't specified - perfectly normal because I have set the fields to required in MySQL right? The error I get in CakePHP is
SQLSTATE[HY000]: General error: 1364 Field 'user_impact' doesn't have a default value
What I want to be able to do is at the time of editing or creating an Assessment is to use the values in the Recommendations table to populate the corresponding join table entries. Any ideas how to go about this?
So as an example:
user_impact = default_user_impact
business_impact = default_business_impact
deployment_complexity = default_deployment_complexity
criticality = default_criticality
The reason I want to do this is so that I can have the Recommendations set with values for those fields, and then if a user wants to run an assessment and they want to adjust the values just for their own assessment then it won't impact others etc.
Here is my AssessmentsTable association.
$this->belongsToMany('Recommendations', [
'foreignKey' => 'assessment_id',
'targetForeignKey' => 'recommendation_id',
'joinTable' => 'assessments_recommendations',
'through' => 'assessments_recommendations',
]);
Here is my RecommendationsTable association.
$this->belongsToMany('Assessments', [
'foreignKey' => 'recommendation_id',
'targetForeignKey' => 'assessment_id',
'joinTable' => 'assessments_recommendations',
'through' => 'assessments_recommendations',
]);
Here is my AssessmentsRecommendations association:
$this->belongsTo('Assessments', [
'foreignKey' => 'assessment_id',
'joinType' => 'INNER',
]);
$this->belongsTo('Recommendations', [
'foreignKey' => 'recommendation_id',
'joinType' => 'INNER',
]);
This is what my AssessmentsController edit function looks like:
public function edit($id = null)
{
$assessment = $this->Assessments->get($id, [
'contain' => ['Recommendations'],
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$assessment = $this->Assessments->patchEntity($assessment, $this->request->getData(), ['associated'=>['Recommendations._joinData']]);
if ($this->Assessments->save($assessment, ['associated' => ['Recommendations._joinData']])) {
$this->Flash->success(__('The assessment has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The assessment could not be saved. Please, try again.'));
}
$clients = $this->Assessments->Clients->find('list', ['limit' => 200]);
$recommendations = $this->Assessments->Recommendations->find('list', ['limit' => 200]);
$this->set(compact('assessment', 'clients', 'recommendations'));
}
Now when I've added the beforeSave function to the AssessmentsRecommendationsTable I see the following error:
Argument 2 passed to App\Model\Table\AssessmentsRecommendationsTable::beforeSave() must be an instance of App\Model\Table\EntityInterface, instance of Cake\ORM\Entity given, called in /var/www/html/csa-portal/vendor/cakephp/cakephp/src/Event/EventManager.php on line 310
Any help would be much appreciated.
First, the associations you are using are wrong. It should be like this
For AssessmentsTable
$this->hasMany('AssessmentsRecommendations', [
'foreignKey' => 'assessment_id'
]);
For RecommendationsTable
$this->hasMany('AssessmentsRecommendations', [
'foreignKey' => 'recommendation_id'
]);
For AssessmentsRecommendationsTable
$this->belongsTo('Assessments', [
'foreignKey' => 'assessment_id',
'joinType' => 'INNER',
]);
$this->belongsTo('Recommendations', [
'foreignKey' => 'recommendation_id',
'joinType' => 'INNER',
]);
Now for the default values, you have to user beforeSave in you AssessmentsRecommendationsTable.php file.You can modify your data as per your need here before the save.
public function beforeSave(Event $event, EntityInterface $entity, \ArrayObject $options)
{
if ($entity->isNew()) { // Returns true when you add new record
$recommendation = TableRegistry::getTableLocator()->get('Recommendations')->get($entity->recommendation_id);
$entity->user_impact = $recommendation->default_user_impact;
$entity->business_impact = $recommendation->default_business_impact;
$entity->deployment_complexity = $recommendation->default_deployment_complexity;
$entity->criticality = $recommendation->default_criticality;
}
}
I have never used belongsToMany, if the associations works for you then ignore the association part.
Have you considered writing a Rule to handle this?
https://book.cakephp.org/3/en/orm/validation.html#applying-application-rules

Yii2 Pagination + PrettyURL Cannot Find site/index

I have pagination setup in site/index, with pretty url working. But my site/index is hidden by either the rewrite engine of Apache, or by UrlManager. In any case, my index page address is simply "X.COM' and pagination wishes to redirect a page change to "X.COM/index?PAGINATIONQUERY", so it always returns a 404.
Example Pagination Request (Returns 404):
x.com/index?page=2&per-page=12
Here is my UrlManager
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
// '<alias:\w+>' => 'site/<alias>',
'<action:\w+>' => 'site/<action>',
],
],
How would I either remove the 'index' portion from pagination requests, or allow myself to see /index in Url again?
Thank you!
Edit:
This is my Index action
public function actionIndex()
{
$query = Shout::find()->orderBy(['id' => SORT_DESC]);
$countQuery = $query->count();
$pagination = new Pagination(['totalCount' => $countQuery, 'pageSize' => 12]);
$shouts = $query->offset($pagination->offset)
->limit($pagination->limit)
->all();
return $this->render('index', [
'shouts' => $shouts,
'pagination' => $pagination,
]);
}
Use Yii2 Gii for Generating Crud Modules with Inbuilt Pagination & Searching.
Yii2 Gii - https://www.yiiframework.com/doc/guide/2.0/en/start-gii
Make Sure Pjax enable when create crud with Gii.

Multiple categories in Url

I want to create links something like that:
http://example.com/cat1/itemname-1
http://example.com/cat1/cat2/itemname-2
http://example.com/cat1/cat2/cat3/itemname-3
http://example.com/cat1/cat2/cat3/[..]/cat9/itemname-9
How rule looks like in yii2 UrlManager and how to create links for this?
Url::to([
'param1' => 'cat1',
'param2' => 'cat2',
'param3' => 'cat3',
'slug' => 'itemname',
'id' => 3
]);
Above code is really bad for multiple category params.
I add that important is only last param it means ID.
Controller looks like that:
public function actionProduct($id)
{
echo $id;
}
The below url rule would to this trick but you have to build the "slug" with the categories within your controller:
'rules' => [
['route' => 'module/controller/product', 'pattern' => '<slug:(.*)+>/<id:\d+>', 'encodeParams' => false],
]
Generate the Url:
yii\helpers\Url::toRoute(['/module/controller/product', 'slug' => 'cat1/cat2/cat3', 'id' => 1])
The output would be:
example.com/cat1/cat2/cat3/1

yii CActiveDataProvider with limit and pagination

I'm trying to select few rows from the table and render it in multiple pages (pagination).
This is a code in the model:
return new CActiveDataProvider('Downloads',
array(
'criteria' => array(
'select' => 'download_id,title,thumb_ext',
'order' => 'download_id DESC',
'limit' => $count,
),
'pagination' => array('pageSize' => 5,),
)
);
In the view I display it using CGridView:
$this->widget('zii.widgets.grid.CGridView', array(
'dataProvider'=>$dataProvider,
'columns' => array('download_id', 'title', 'thumb_ext'),
));
The problem is that CActiveDataProvider ignores limit of criteria and returns all rows of table...
Thanks.
I'm not positive... but I think Yii uses the LIMIT clause to do SQL result pagination, so it will overwrite/replace your LIMIT clause. You can check this by turning on the CWebLogRoute log route to see exactly what SQL is being executed.
I'm not quite sure how this is supposed to work, anyway. What is the LIMIT clause you added for? If you are paginating anyway, why not let the user paginate through all the records? The solution is probably to change your criteria to get rid of the LIMIT clause.
Are you trying to set the number of results per page? You already have the pageSize set to 5 though...
One other thing to try that might do what you want, is to check out the base Pager class, CPagination. With CLinkPager, it might let you paginate more creatively than the CListPager you are using with the CGridView.
Good luck!
I had the same problem and I found a solution as follows:
return new CActiveDataProvider('Downloads',
array(
'criteria' => array(
'select' => 'download_id,title,thumb_ext',
'order' => 'download_id DESC',
),
'pagination' => array('pageSize' => 5,),
'totalItemCount' => $count,
)
);
and set CGridView to pagination is hidden:
$this->widget('zii.widgets.grid.CGridView', array(
'dataProvider'=>$dataProvider,
'enablePagination' => false,
'columns' => array('download_id', 'title', 'thumb_ext'),
));
One of the basic features of a data provider is that you can override the calculation of the amount of rows by specifying a "totalItemCount" in the config. Normally (I haven't tested it) this also works for the ActiveDataProvider:
return new CActiveDataProvider('Downloads',
array(
'criteria' => array(
'select' => 'download_id,title,thumb_ext',
'order' => 'download_id DESC',
),
'pagination' => array('pageSize' => 5,),
'totalItemCount' => $count,
)
);

Cakephp model associastion

I have thee following simple model:
Item belongsTo CatalogItem
CatalogItem hasMany Item, and belongsTo Section
Section hasMany CatalogItem
I'm trying to get counts of items, grouped by catalogitem, for a certain section-
the equivalent of:
SELECT catalogitem.id, count(*) FROM section LEFT JOIN catalogitem ON section.id=catalogitem.section_id LEFT JOIN item ON item.catalogitem_id=catalogitem.id WHERE section.id=5 GROUP BY catalogitem.id
So simple in sql, yet I can't get it to work with cake models. Can anyone point as to how to do it with cake models, using the model->find?
I can't get it to group by correctly or join correctly on 3 tables :(
Edit:
highly prefer to get the info in single query
Here's a longer way, "cakeish" way:
class Item extends AppModel
{
/* snip */
var $virtualFields = array('item_count' => 'count(Item.id)');
function getCountForSection($sectionId)
{
$ca = $this->Catalogitem->find
(
'all',
array
(
'fields' => array('Catalogitem.id'),
'conditions' => array('Catalogitem.section_id' => $sectionId),
'recursive' => -1
)
);
$ca = Set::extract('/Catalogitem/id', $ca);
$ret = $this->find
(
'all',
array
(
'fields' => array('Item.catalogitem_id', 'item_count'),
'conditions' => array('Item.catalogitem_id' => $ca),
'group' => array('Item.catalogitem_id'),
'recursive' => -1
)
);
return $ret;
}
}
Then simply use it in your controller:
$ret = $this->Item->getCountForSection(1);
debug($ret);
How does it work:
Define a virtual field (cake 1.3+ only AFAIK) which will count items
Fetch all the Catalogitems belonging to a Section you're interested in
Use Set::extract() to get the Catalogitems in a simple array
Use the array of Catalogitems to filter Items while counting and grouping them
NB: You don't seem to be using Cake's naming conventions in your database. This may hurt you.
Sorry, in my first answer I somehow missed your GROUP BY requirement, which was the whole point of the question, I now realize. I haven't used this yet, but I came across it recently, and it looks like it might accomplish what you are looking for: Linkable Behavior.
http://planetcakephp.org/aggregator/items/891-linkable-behavior-taking-it-easy-in-your-db
Like Containable, but works with only right and left joins, produces much more compact queries and supports GROUP BY.
http://github.com/rafaelbandeira3/linkable
#azv
Would this work for you:
$section_id = 5;
$fields = array('CatalogItem.id as CatalogItemId', 'count(*) AS SectionCount');
$conditions = array('Section.id' => $section_id);
$joins = array(
array('table' => 'catalogitem',
'alias' => 'CatalogItem',
'type' => 'LEFT',
'conditions' => array('Section.id' => 'CatalogItem.section_id')
),
array('table' => 'item',
'alias' => 'Item',
'type' => 'LEFT',
'conditions' => array('Item.catalogitem_id' => 'CatalogItem.id')
));
$data = $this->Section->find('all',
array('fields' => $fields,
'conditions' => $conditions,
'joins' => $joins,
'group' => 'CatalogItem.id',
'recursive' => -1)
);
// access your data values
foreach ($data['Section'] as $i => $datarow) {
$catalogitem_id = $datarow['CatalogItemId'];
$section_count = $datarow['SectionCount'];
}
This way you are explicitly setting your joins and doing it all in one query. See here for more info on joins in Cake:
http://book.cakephp.org/view/1047/Joining-tables
Hope this helps. All the best,
-s_r