Let's pretend I'm working on a magazine, where a Category (like "sport", "art" an so on) can contain several Articles. Therefore I want to extract all articles for a specific category. In Phalcon I usually do:
$category = \Models\Category::findFirst(array(
'conditions' => 'id = ?1',
'bind' => array(1 => $id)
));
Then:
foreach ($category->Article as $article) {
// do something with $article
}
It works great, but I would like to sort those Articles - say - date wise, ascending. How could I accomplish that?
You should use get prefix in your for..loop statement, so your code should like this :
foreach ($category->getArticle(array('order' => 'date DESC')) as $article) {
// do something with $article
}
The main Docs explains more examples.
Try it & get your results.
M2sh answer is all important and actual, I just will post a secondary way, just using model designed for articles:
$page = 5; // eg. 5th page of results
$limit = 100; // eg. 100 results per page
$articles = \Models\Articles::find(array(
'conditions' => 'category_id = :cid:',
'bind' => array(
'cid' => $id
),
'order' => 'date DESC, id DESC',
'limit' => $limit,
'offset' => $page * $limit
));
It's possible to use such set of parameters in M2sh way aswell.
One more for completeness. I will borrow from yergo to illustrate the differences:
$page = 5;
$limit = 100;
$articles = \Models\Articles::query()
->where('category_id= :cid:', array('cid' => $id)
->orderBy('date DESC, id DESC')
->limit($limit)
->offset($page * $limit)
->query();
Related
I have a find method that uses a DISTINCT clause to get results from my Model. The Controller code looks like below
$options = array(
'limit' => 10,
'fields' => array(
'DISTINCT id', 'title',
),
'contain' => array(
'Dealer' => array('id'),
),
'paramType' => 'querystring'
);
$this->Paginator->settings = $options;
$cars = $this->Paginator->paginate('Car'); // returns 6 distinct rows
The above query return 6 distinct rows and 12 total rows. So when I am displaying, the screen shows 6 distinct rows
However in the View, when I use
echo $this->Paginator->param('count'); // returns 12
I get a count of 12
I checked the SQL Log and noticed that the count query is not using the distinct clause. Any idea how I can override the Paginator count query to use the DISTINCT clause?
Found the solution,
In controller add distinct as an array parameter with other pagination options. So if I was trying to retrieve a list of Cars in my inventory with 10 cars at a time, the options would have a DISTINCT clause in the fields parameter and a separate parameter called distinct would also be added as shown below
$options = array(
'conditions' => $conditions,
'joins' => $joins,
'limit' => 10,
'fields' => array(
'DISTINCT Car.id', 'title', 'user_id'),
'contain' => array(
'Dealer' => array('id'),
),
'paramType' => 'querystring',
'distinct' => 'Car.id'
);
$this->Paginator->settings = $options;
$cars = $this->Paginator->paginate('Car');
In Model, use the below function to override the original paginateCount method
public function paginateCount($conditions = null, $recursive = 0, $extra = array()) {
$parameters = compact('conditions', 'recursive');
if (isset($extra['distinct'])) {
$parameters['fields'] = 'DISTINCT ' . $extra['distinct'];
$count = $this->find('count', array_merge($parameters, $extra));
} else {
// regular pagination
$count = $this->find('count', array_merge($parameters, $extra));
}
return $count;
}
No change in View
I have a table called items and a table called item_pics.
item_pics has an item_id, file_name and a rank field (among others).
What I'm looking for is for each item my index page's $items array to contain the file_name from the item_pics matching the item's item_id with the lowest rank. So I can access like (or something like) this in my Items/index.ctp:
foreach ($items as $item):
$img = $item['Item']['ItemPic']['file_name'];
...
I'm pretty new to CakePHP, this is my first project. I thought that this within the Item model would cause item_pics data to be pulled (although I figured all related item_pics for each item would get pulled rather than just the one with the lowest rank):
public $hasMany = array(
'ItemPic' => array(
'className' => 'ItemPic',
'foreignKey' => 'item_id',
'dependent' => false
)
}
but I can see that no item_pics data is loaded (at the bottom of items/index):
SELECT `Item`.`id`, `Item`.`title`, `Item`.`description`, `Item`.`created`, `Item`.`modified`, `Item`.`type`, `Project`.`id`, `Project`.`item_id`, `Project`.`title`, `Project`.`description`, `Project`.`rank`, `Project`.`created`, `Project`.`modified`
FROM `laurensabc`.`items` AS `Item`
LEFT JOIN `laurensabc`.`projects`
AS `Project`
ON (`Project`.`item_id` = `Item`.`id`)
WHERE `Item`.`type` IN (1, 2)
LIMIT 20
also, while I would like projects to be joined in the view pages, I don't really need them in the index page.
I've done some searching and haven't been able to find exactly what I'm looking for. I suppose I could do a query within the index view item loop, but I'm trying to make sure I do things the right way... the CakePHP way. I assume I need to change something about my model relationships but I haven't had any luck.
CakePHP - Associations - HasMany, this makes it seem like I could order by rank and limit 1. But this didn't work... and even if it did, I wouldn't want that to affect the view pages but rather just the index page.
My Controller looks like this:
public function index($type = null) {
$this->Item->recursive = 0;
$conditions = array();
if ($type == "sale") {
$conditions = array(
"Item.type" => array(self::FOR_SALE, self::FOR_SALE_OR_RENT)
);
} else if ($type == "rent" ) {
$conditions = array(
"Item.type" => array(self::FOR_RENT, self::FOR_SALE_OR_RENT)
);
} else {
$conditions = array("Item.type !=" => self::HIDDEN);
}
$paginated = $this->Paginator->paginate($conditions);
debug($paginated);
$this->set('items', $paginated);
$this->set('title', ($type == null ? "Items for Sale or Rent" : "Items for " . ucwords($type)));
}
I have also tried this on my controller, but it doesn't seem to do anything either:
$this->paginate = array(
'conditions' => $conditions,
'joins' => array(
array(
'alias' => 'ItemPic',
'table' => 'item_pics',
'type' => 'left',
'conditions' => array('ItemPic.item_id' => 'Item.id'),
'order' => array('ItemPic.rank' => 'asc'),
'limit' => 1
)
)
);
$paginated = $this->paginate($this->Item);
First, set containable behavior in AppModel (or if you don't want it on each model, put it on Item model):
public $actsAs = array('Containable');
Then, on your find query:
$items = $this->Item->find('all', array(
'contain' => array(
'ItemPic' => array(
'fields' => array('file_name'),
'order' => 'rank',
'limit' => 1
)
)
));
Then the result array you can access it like:
foreach ($items as $item):
$img = $item['ItemPic']['file_name'];
Edit: Then you should put it on the paginate query:
$this->paginate = array(
'conditions' => $conditions,
'contain' => array(
'ItemPic' => array(
'fields' => array('file_name'),
'order' => 'rank',
'limit' => 1
)
)
);
In this case, I would probably order by rank and limit 1 as you said, and make that a dynamic association just for the index page (See http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#creating-and-destroying-associations-on-the-fly). So use $this->Item->bindModel(array('hasMany' => array('ItemPic' => $options))); (which I believe should replace your current settings for HasMany ItemPic, but you may have to unbindmodel first)
Associations created through bindModel will go through for the next query only, then it'll revert to your normal settings, unless you specifically set an option to keep using the new association.
As for why it's not getting ItemPics with Items, or why trying to order by rank and limit 1 didn't work for you, I can't really say without seeing more of your code.
I've got a fairly modified pagination query using a number of Joins etc - but for some reason the paginator->counter() never matches the results from the count query.
You can see it in action at http://dev.qreer.com/ - by choosing various options on the LHS navigation, the query output is below and the paginator count appears to be pretty random.
Any idea where I can start looking to debug this?
In the Jobs Controller:
$this->paginate = $this->Job->paginateParams($data);
$jobs = $this->paginate('Job');
$this->set(compact('jobs'));
In the Model:
function paginateParams($data = null){
//lots of joins + conditions
return array('contain' => $contain, 'recursive' => 0,'joins' => $joins, 'conditions' => $conditions, 'group' => 'Job.id', 'order' => $order);
}
Sample Join (there's inner joins for all the join tables and data tables):
array(
'table' => 'education_backgrounds',
'alias' => 'EducationBackground',
'type' => 'INNER',
'conditions' => array('EducationBackgroundsJobs.education_background_id = EducationBackground.id'),
),
Sample Condition:
'EducationBackground.name' => array('Aerospace Engineering');
It's because of the group by I found a workaround. I'd love to put the link but i've lost it, so i'll post the code:
public function paginateCount($conditions = null, $recursive = 0, $extra = array()) {
$parameters = compact('conditions', 'recursive');
if (isset($extra['group'])) {
$parameters['fields'] = $extra['group'];
if (is_string($parameters['fields'])) {
// pagination with single GROUP BY field
if (substr($parameters['fields'], 0, 9) != 'DISTINCT ') {
$parameters['fields'] = 'DISTINCT ' . $parameters['fields'];
}
unset($extra['group']);
$count = $this->find('count', array_merge($parameters, $extra));
} else {
// resort to inefficient method for multiple GROUP BY fields
$count = $this->find('count', array_merge($parameters, $extra));
$count = $this->getAffectedRows();
}
} else {
// regular pagination
$count = $this->find('count', array_merge($parameters, $extra));
}
return $count;
}
I added it in the app_model and it works fine for me :)
Hope this helps
Edited: I found the link =)
http://wiltonsoftware.com/posts/view/custom-group-by-pagination-and-a-calculated-field
Figured it out:
The paginator counter relies on $this->find('count') to return an integer of the total of the results, which for some reason, doesn't like the 'group' parameter. So following the Custom Query Pagination (which also recommends at the bottom of the page to do the count yourself for any custom / modified pagination) - I added the following to my model:
function paginateCount(){
$params = Configure::read('paginate.params');
$params['fields'] = 'DISTINCT (Job.id)';
unset($params['group']);
unset($params['contain']);
unset($params['order']);
return $this->find('count', $params);
}
This overwrites the value with the correct one and it all seems to be working perfectly.
Bearing in mind I've added Configure::write('paginate', array('params' => $this->paginate['Job'])); to my controller so I can access the pagination parameters.
function paginateCount($conditions = null, $recursive = 0,$extra)
{
$db = $this->getDataSource();
$sql = $db->buildStatement(
array(
'fields' => array('DISTINCT Gdi.id'),
'table' => $db->fullTableName($this),
'alias' => 'Gdi',
'limit' => null,
'offset' => null,
'joins' => isset($extra['joins'])?$extra['joins']:null,
'conditions' => $conditions,
'order' => null,
'group' =>isset($extra['group'])?$extra['group']:null
),
$this
);
$this->recursive = $recursive;
$results = $this->query($sql);
return count($results);
}
Just add this function in your model and change the field name and model name. Using this function you can customize you own query count.
Am I doing this right? I went to look at some old PHP code w/ MySQL and I've managed to get it to work, however I'm wondering if there's a much "cleaner" and "faster" way of accomplishing this.
First I would need to get the total number of "documents"
$total_documents = $collection->find(array("tags" => $tag,
"seeking" => $this->session->userdata('gender'),
"gender" => $this->session->userdata('seeking')))->count();
$skip = (int)($docs_per_page * ($page - 1));
$limit = $docs_per_page;
$total_pages = ceil($total_documents / $limit);
// Query to populate array so I can display with pagination
$data['result'] = $collection->find(array("tags" => $tag,
"seeking" => $this->session->userdata('gender'),
"gender" => $this->session->userdata('seeking')))->limit($limit)->skip($skip)->sort(array("_id" => -1));
My question is, can I run the query in one shot? I'm basically running the same query twice, except the second time I'm passing the value to skip between records.
-- New code ...
Ok, unless someone knows of another way to do this (if it's possible), I'm going to say it's not doable. With that said, I changed the way I run my queries through mongodb, which yielded better looking code. ;-) I was trying to minimize the trips to the DB, but oh well hopefully this doesn't take a performance hit. My other attempt was to count the number of elements in the array, but quickly found out that wouldn't work since the $limit & $skip parameters would give ITS total number of docs.
$skip = (int)($docs_per_page * ($page - 1));
$limit = $docs_per_page;
$query = array("loc" => array('$near' => array('lat' => $latitude, 'lon' => $longitute) ),
"tags" => $tag, "seeking" => $this->session->userdata('gender'),
"gender" => $this->session->userdata('seeking'));
$fields = array("username", "zipcode", "tags", "birth_date");
$total_documents = $collection->find($query, array("_id"))->count();
$data['result'] = $collection->find($query, $fields)->limit($limit)->skip($skip);
Since the result of find()->limit()->skip() is a Mongo_Cursor you don't have to execute the actual query twice.
The following should work as well :
$skip = (int)($docs_per_page * ($page - 1));
$limit = $docs_per_page;
$query = array("loc" => array('$near' => array('lat' => $latitude, 'lon' => $longitute) ),
"tags" => $tag, "seeking" => $this->session->userdata('gender'),
"gender" => $this->session->userdata('seeking'));
$fields = array("username", "zipcode", "tags", "birth_date");
$cursor = $collection->find($query, $fields)->limit($limit)->skip($skip);
$total_documents = $cursor->count();
$data['result'] = $cursor;
btw I first misread your question, I thought you didn't know about limit & skip.
Yes you are doing right.
And you can run query in one shot.
Here is a paging example:
function printStudents(pageNumber, nPerPage) {
print("Page: " + pageNumber);
db.students.find().skip((pageNumber-1)*nPerPage).limit(nPerPage).forEach( function(student) { print(student.name + "<p>"); } );
}
Reference: Advanced Queries - MongoDB: http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-{{skip%28%29}}
I have a user model which gives me latest users as output. How can I limit the record to just output me 200 records instead of all the users in database?
According to the documentation, the second argument to the find() method is a $params array.
One of the possible values to pass in this array is a limit key. So you could do the following:
$users = $this->User->find('all', array('limit' => 200));
"i have it like array('limit' => 21, 'page' => 1) for paging 21 users in one page.. if i change the limit there to 200 then it paginates 200 users in one page only...in this case how to limit along with proper pagination?? – Anonymous May 14 '09 at 7:22"
yes you can use the cakePHP pagination helper as someone has mentioned. But there may be some cases where you want to do your own pagination or just limit the number of records retrieved per call. For what it's worth here's how I handled one such situation.
Say for example you want to retrieve a certain number of records per page, Then:
$start = 0; -> this is in order to start retrieving records starting from the first one. If you need to say for example, start from the 31st, then $start = 30;
So,
$start = 0;
$length = 20; // we are going to retrieve 20 records starting from the first record
And the code will be something like:
// To retrieve a number of Products per page
$products = $this->Product->find('all', array(
'order' => 'product_number ASC',
'limit' => $start.','.$length,
'recursive' => -1
)
);
Don't paginate with find().
Cake Pagination: http://book.cakephp.org/2.0/en/core-libraries/components/pagination.html
array(
'conditions' => array('Model.field' => $thisValue), //array of conditions
'recursive' => 1, //int
//array of field names
'fields' => array('Model.field1', 'DISTINCT Model.field2'),
//string or array defining order
'order' => array('Model.created', 'Model.field3 DESC'),
'group' => array('Model.field'), //fields to GROUP BY
'limit' => n, //int
'page' => n, //int
)
Limit * page = 200 set your values according to your comfortable view in pages. This might help
You can also try this out
$results = $this->Model->find('all',
array('limit'=>10,
'order'=>array('date DESC')));
open the model file of user and do as follows:
you will need to change the 'limit' property in the relationship variable named
var $hasMany = array( 'Abus' =>
array('className' => 'Abus',
'foreignKey' => 'user_id',
'dependent' => false, 'conditions'
=> '',
'fields' => '', 'order' => '', 'limit' => '200', 'offset'
=> '', 'exclusive' => '', 'finderQuery' => '',
'counterQuery' => '' ) );
OR you can also try this out...
in your users controller set the $paginate to like this.
var $paginate = array('limit' => 200);
The records will be limited to 200 now wherever you use paginate.