I can't paginate my results if I access them from a Routed url. These are the routes that I'm using:
// NEWS
Router::connect('/news.rss', array('controller' => 'posts', 'action' => 'index', 'ext' => 'rss'));
Router::connect('/news/*', array('controller' => 'posts', 'action' => 'index'));
Router::connect('/:lang/posts/*', array('controller' => 'posts', 'action' => 'index'));
I know that in the last route I'm not passing the :lang parameter, but if I pass it:
Router::connect('/:lang/news/*', array('controller' => 'posts', 'action' => 'index'), array('lang' => $regex['lang'], 'pass' => array('lang')));
It does not work either.
If I try to access the url /news/page:2 it will show me the results from the first page. I printed out $this->params to see if it takes the page number correctly, and, in first instance, it does:
Array
(
[lang] => ca
[named] => Array
(
[page] => 2
)
[pass] => Array
(
)
[controller] => posts
[action] => index
[plugin] =>
[url] => Array
(
[ext] => html
[url] => ca/posts/page:2
)
[form] => Array
(
)
[...]
)
This part of the array (I've ommited some parts that I'll show you later) is the same if I access /news/page:2 and /posts/index/page:2, but if you take a look to this part:
Array
(
[...]
[paging] => Array
(
[Post] => Array
(
[page] => 1
[current] => 3
[count] => 3
[prevPage] =>
[nextPage] =>
[pageCount] => 1
[defaults] => Array
(
[limit] => 3
[step] => 1
[order] => Post.created DESC
[conditions] => Array
(
[Post.active] => 1
[Post.page] =>
[0] => Post.public_date <= NOW()
)
)
[options] => Array
(
[page] => 1
[limit] => 3
[order] => Post.created DESC
[conditions] => Array
(
[Post.active] => 1
[Post.page] =>
[0] => Post.public_date <= NOW()
)
)
)
)
You can see that it doesn't take the page number correctly. But if I access from /posts/index/page:2 it takes the number well and pagination works.
If only it were pretty URLs do not bother me, but considering that the site is multilingual, I need at least that works if I access /en/posts/index/page:2 (or /en/news/page:2)...
Here is my full routes.php file:
http://pastebin.com/th4hLZNz
Anybody has an idea of what is occurring?
Thanks in advance
Finally I've found the solution. Actually it was easy and the biggest problem was that I did not focus it properly. I focused on the routes thinking I did something wrong, but the problem was the controller.
Here was the problem:
$this->paginate['conditions'] = array(
'Post.active' => true,
'Post.page' => $page,
'Post.public_date <= NOW()');
$this->paginate['group'] = 'Post.id';
The group (in convination with the conditions) doesn't allow the paginateCount function to count the results well. So I've created my own paginateCount function on Post's Model missing the group condition:
/**
* This method fixes the count query
* when there's a GROUP BY on it
*
* #return integer
*/
public function paginateCount($conditions = null, $recursive = 0, $extra = array())
{
$parameters = compact('conditions', 'recursive');
$params = array();
if (isset($extra['group']))
{
$params = array_merge(array(
'fields' => array(
'COUNT(DISTINCT(' . $this->alias . '.' . $this->primaryKey . ')) AS `count`'
)
), $parameters);
}
else
{
$params = array_merge($parameters, $extra);
}
return $this->find('count', $params);
}
And now it seems to work well
Related
I have a 2 CakePHP models, Articles and Categories, attached using a hasAndBelongsToMany relationship, like so:
$category = new Category();
$category->bindModel(array('hasAndBelongsToMany' => array(
'Article' => array(
'className' => 'Article',
'joinTable' => 'articles_categories',
'foreignKey' => 'category_id',
'associationForeignKey' => 'article_id',
'fields' => 'id,name'
))));
$this->set('categories', $category->find('all',
array(
'fields' => 'id,name'
)));
...but then, when I print out $categories, I get the following:
Array
(
[0] => Array
(
[Category] => Array
(
[id] => 31
[name] => category name
[article_count] => 1
)
[Article] => Array
(
[0] => Array
(
[id] => 1445
[name] => article name
[teaser] =>
[author_id] => 3
[ArticlesCategory] => Array
(
[id] => 6634
[article_id] => 1445
[category_id] => 31
[author_id] => 3
[created] => 2014-03-10 12:27:26
[modified] => 2014-03-10 12:27:26
)
)
)
)
I really don't need the [ArticlesCategory] member of [Article]. This just leads me back to information I already have. I tried limiting recursion but that didn't help.
How would I get rid of this?
You have two options:
[1] Reduce recursive value to 0 (CakePHP default: 1)
$category->recursive = 0;
$category->find('all',
array(
'fields' => 'id,name'
))
[2] Start using ContainableBehaviour (my own preference), which gives you more control over model data retrieved
Add following to AppModel for App-wide coverage:
public $actsAs = array('Containable');
The find method becomes:
$category->find('all',
array(
'fields' => 'id, name',
'contain' => 'Article'
))
That should do it!
I have a model, Comment, that belongsto Module, Photo, Review, Article, and User. In turn, each of those models havemany Comment.
User belongsto Avatar and Avatar hasmany User
In the code below I successfully retrieve the latest 5 comments regardless of what model they come from (Photo, Article, Review) and I display some information about the User (username, online status).
$recentComments = $this->find('all',
array(
'limit' => '5',
'order' => array('Comment.id' => 'desc'),
'conditions' => array(
'Comment.page_id' => $page_id
),
'contain' => array(
'Photo' => array(
'fields' => array('Photo.id'),
'conditions' => array(
'Comment.module_id' => 8
),
),
'Review' => array(
'fields' => array('Review.title', 'Review.id'),
'conditions' => array(
'Comment.module_id' => 2
),
),
'Article' => array(
'fields' => array('Article.title', 'Article.id'),
'conditions' => array(
'Comment.module_id' => 3
),
),
'Module' => array(
'fields' => array('Module.model', 'Module.post_title'),
),
'User' => array(
'fields' => array('User.username', 'User.online'),
),
)
));
Unfortunately there are some issues with this. As you can see above I don't contain User->Avatar and all expected data is retrieved successfully.
$recentComments = Array
(
[0] => Array
(
[Comment] => Array
(
[id] => 179
[page_id] => 2
[module_id] => 3
[page] =>
[user_id] => 29
[post_id] => 9
[title] => test comment for article 9
[content] => here is the content for the comment
[created] => 2013-04-24 00:00:00
[redeemed] => 0
[status] => 0
)
[User] => Array
(
[username] => bowlerae
[online] => 0
)
[Module] => Array
(
[model] => Article
[post_title] => title
)
[Article] => Array
(
[title] => Test title for article 9
[id] => 9
[likes] => 0
[dislikes] => 0
[comments] => 2
)
[Photo] => Array
(
[id] =>
)
[Review] => Array
(
[title] =>
[id] =>
)
)
However, if I DO contain User->Avatar like this...
'User' => array(
'fields' => array('User.username', 'User.online'),
'Avatar' => array(
'fields' => array('Avatar.file')
)
),
Then the recursion basically becomes unlimited, ALL models related to Comment, User, Module, Article, Photo and Review are also retrieved which is A LOT.
Can anyone explain why this is happening? I will be happy to submit more code from some of the models if needed but I don't see any issues there.
Take a look at another example that works successfully...Below I am retrieving the 5 most recent articles. All information including the user Avatar is successfully retreived.
$this->set('articles_sidebar', ClassRegistry::init('Article')->find('all',
array(
'limit' => '5',
'order' => array('Article.id' => 'desc'),
'conditions' => array('
Article.page_id' => $page_id
),
'contain' => array(
'User' => array(
'fields' => array('User.username', 'User.online'),
'Avatar' => array(
'fields' => array('Avatar.file')
)
),
)
)));
Please note these two finds are performed in the AppController in the beforeRender given that $page_id > 0. $page_id is set in whatever the current controller is. I know people would probably ask about it but that's not what the issue is as I mentioned that example 2 retrieving the recent articles currently works.
EDIT: I discovered that it has something to do with my afterfind callback in the Article model. Is there a way I can tweak the queries in the afterfind and/or the $recentComments query so that they still work without breaking my contain? I don't need the likes, dislikes or comments virtual fields in my $recentComments query which is why they are not listed as one of the contained fields.
function afterFind($results, $primary = false) {
parent::afterFind($results, $primary);
foreach($results as $key => $val){
if (isset($val['Article']['id'])){
$results[$key]['Article']['likes'] = $this->Like->find('count', array('conditions' => array('Like.post_id' => $results[$key]['Article']['id'], 'Like.module_id' => 3, 'Like.status' => 0)));
$results[$key]['Article']['dislikes'] = $this->Like->find('count', array('conditions' => array('Like.post_id' => $results[$key]['Article']['id'], 'Like.module_id' => 3, 'Like.status' => 1)));
$results[$key]['Article']['comments'] = $this->Comment->find('count', array('conditions' => array('Comment.post_id' => $results[$key]['Article']['id'], 'Comment.module_id' => 3, 'Comment.status < 2')));
}
} // end for each
return $results;
} // end afterfind
My guess is your problem is this:
When using ‘fields’ and ‘contain’ options - be careful to include all
foreign keys that your query directly or indirectly requires.
Make sure you're including whatever field(s) are used to join User and Avatar models.
(read here)
For now I changed (in the Article model afterFind)
if (isset($val['Article']['id']))
to
if (isset($val['Article']['id']) && $val == "article")
Seems to be working in current controller but need further testing. Is there a better solution?
I have array element stored in $sal to put at the start of an array $fields['billing']. i am using array_unshift for this purpose.
$sal = array(
'label' => __('Tratamiento', 'woocommerce'),
'placeholder' => _x('', 'placeholder', ''),
'required' => 0,
'clear' => true,
'class' => array('form-row form-row-wide'),
'type' => 'select',
'options' => array(
'Señor' => __('Señor', 'woocommerce' ),
'Señora' => __('Señora', 'woocommerce' ),
'Señorita'=> __('Señorita', 'woocommerce')
)
);
array_unshift($fields['billing'] , $sal);
array_unshift adding element at the start of array at 0 key index. after print_r i see:
[0] => Array
(
[Label] => Treatment
[Placeholder] =>
[Required] => 0
[Clear] => 1
[Class] => Array
(
[0] => form-row-row-wide form
)
[Type] => select
[Options] => Array
(
[Lord] => Lord
[Lady] => Lady
[Ms.] => Miss
)
)
My problem is only that i just want to change the key value from [0] to ['saluation'], i can simply do that with:
$fields['billing']['saluation'] = $fields['billing'][0];
unset($fields['billing'][0]);
but i also want it at the start of array. i tried many techniques but still unable to figure this out.
this is actully woocommerce fields arrays which i am dealing with.
I just solved it by array_merge() function.
$fields['billing']= array_merge(array('saluation' => $sal), $fields['billing']);
view/plans/index.ctp
<?php debug($plans); ?>
controllers/plans_controller.php (index function)
function index() {
$plansC = $this->Plan->find('all',
array('contain' => array('PlanDetail' => array('fields' => array('id',
'effective_date',
'expiration_date',
'active',
'name',
'plan_type_id',
'max_benefit',
'deductible',
'preventive',
'basic',
'major',
'ortho',
'company_id',
'plan_type_id',
'plan_detail_note_id'),
'Company' => array('fields' => array(
'id',
'company_logo_url'
)),
'PlanType' => array('fields' => array(
'id',
'name'
))
))));
debug($plansC);
$this->set('plans',$this->paginate($planC));
Here is a sample index() debug record from the plans_controller.php (as you can see all data is being contained properly using containable):
[0] => Array
(
[Plan] => Array
(
[id] => 7
[created] => 2011-01-10 14:11:40
[modified] => 2011-02-03 18:35:29
[plan_detail_id] => 32
[monthly_cost] => 25.49
[dental_cost] => 0.00
[age_id] => 2
[applicant_id] => 1
[state_id] => 1
)
[PlanDetail] => Array
(
[id] => 32
[effective_date] => 2011-01-10 14:07:00
[expiration_date] => 2011-01-10 14:07:00
[active] => 1
[name] => Classic 1500
[plan_type_id] => 2
[max_benefit] => 0.00
[deductible] => $75/year
[preventive] => 90%
[basic] => 75%
[major] => 50%
[ortho] =>
N/A
[company_id] => 4
[plan_detail_note_id] => 9
[Company] => Array
(
[id] => 4
[company_logo_url] => nationwide_logo.png
)
[PlanType] => Array
(
[id] => 2
[name] => Indemnity
)
)
)
The containable data is not being passed. Only data associated with Plan and PlanDetail (no deeper relations than PlanDetail such as Company or Plan type), but the debug in the index controller shows all data being passed! But none of this data is making it into the view debug???
Does anyone know if this is a bug with containable?
You must either paginate or find, you can't paginate the found data array. Pagination is retrieving data from the database. Replace your find call with a paginate call. Save the result of the paginate call in a variable and debug it, your problem is there, not in the set call.
After 8 hours pain, I solved it : ) and I added pagination to boot. I ended up using compact() in place of the baked set().
function index() {
//$this->Plan->find('all'); not needed
$this->paginate['Plan'] = array('contain' => array('PlanDetail' => array('fields' => array('id',
'effective_date',
'expiration_date',
'active',
'name',
'plan_type_id',
'max_benefit',
'deductible',
'preventive',
'basic',
'major',
'ortho',
'application_url',
'company_id',
'plan_type_id',
'plan_detail_note_id'),
'Company' => array('fields' => array(
'id',
'company_logo_url'
)),
'PlanType' => array('fields' => array(
'id',
'name'
))
)));
$plans = $this->paginate('Plan');
$this->set(compact('plans'));
}
I had a recent question here regarding Finding records via conditions in habtm. Now, I could search for posts within a category I searched for.
Now, my questions is how to I retrieve the categories of each post. Sometimes a post has one or more categories in this query:
$this->set('posts', $this->Category->find(
'first',
array(
'conditions' => array(
'Category.uri' => $uri
),
'contain' => array('Post')
)
));
I'd imagine something like this:
$this->set('posts', $this->Category->find(
'first',
array(
'conditions' => array(
'Category.uri' => $uri
),
'contain' => array('Post' => array(
'contain' => 'Category'
))
)
));
Here's what my models look like.
// Category Model
class Category extends AppModel {
var $name = 'Category';
var $hasAndBelongsToMany = array(
'Post' => array(
'className' => 'Post'
)
);
var $actsAs = array('Containable');
}
// Post Model
class Post extends AppModel {
var $name = 'Post';
var $hasAndBelongsToMany = array(
'Category' => array(
'className' => 'Category'
)
);
var $actsAs = array('Containable');
var $virtualFields = array(
'date_posted' => 'DATE_SUB(Post.created, INTERVAL 7 DAY)'
);
}
A sample data would be like this:
categories
id name
1 holidays
2 destinations
posts
id title
1 I am a post
2 I am another post
categories_posts
post_id category_id
1 1
2 2
2 1
I am retrieving posts from holidays.
Array
(
[Category] => Array
(
[id] => 3
[name] => holidays
[uri] => holidays
[created] => 2010-11-25 20:43:03
[modified] => 2010-11-25 20:43:03
)
[Post] => Array
(
[0] => Array
(
[id] => 1
[title] => I am a post
),
[1] => Array
(
[id] => 2
[title] => I am a another post
)
)
)
The problem is that 1 of the posts are in two categories. I'd like to have that information also available.
Close. Try this:
$this->set('posts', $this->Category->find(
'first',
array(
'conditions' => array(
'Category.uri' => $uri
),
'contain' => array('Post' => array('Category'))
)
));
Tangent
Writing Controller code like this has problems:
It will bloat your controller methods and make your controllers less readable.
It prevents you from re-using business logic among your different controllers.
You can satisfactorily correct these problems by moving your find call to a model method:
// Categories Controller
$this->set('posts', $this->Category->get_posts_with_cats($uri));
// Category Model
function get_posts_with_cats($uri) {
$this->find('first', array(
'conditions' => array('Category.uri' => $uri),
'contain' => array('Post' => array('Category'))
));
}
This makes your code More Awesome:
The controller is nice and Skinny, and super readable.
Now you can call the same method from any associated model. So, for instance, in your PostsController you could call: $this->Post->Category->get_posts_with_cats($uri);. Your code is now DRY.
Tangent #2
After you learn how to nest containable models like I've shown you, you may be tempted in the future to do something like this:
$this->find('first', array(
'conditions' => array('Category.uri' => $uri),
'contain' => array(
'Post' => array(
'Category' => array(
'Tag' => array('Author')
)
)
)
));
The good news? You will get the desired model data from the above method (as long as you've defined all of these relationships).
The terrible news? CakePHP doesn't know how to optimize the queries for deep associations like this. So, as your database grows, you'll end up with dozens (if not hundreds (if not thousands)) of SELECT queries to the database, effectively bringing your application to a crawl (if not a halt.)
The good folk that design CakePHP are working on a better containable behavior for CakePHP 2.0, but you'll have to wait until then to get this level of precision.
I'm leaving my other answer as-is because it contains a lot of good info for those who may stumble upon it. As far as solving your problem more elegantly? You'll need to flip the find on it's head and search from the Post model with an ad-hoc join, like this:
// From the CategoriesController
$this->Category->Post->find('all', array(
'joins' => array(
array(
'table' => 'categories_posts',
'alias' => 'CategoryFilter',
'type' => 'inner',
'conditions' => array(
'CategoryFilter.post_id = Post.id'
)
),
array(
'table' => 'categories',
'alias' => 'CategoryUriFilter',
'type' => 'inner',
'conditions' => array(
'CategoryUriFilter.id = CategoryFilter.category_id'
)
)
),
'conditions' => array(
'CategoryUriFilter.uri' => $uri
),
'contain' => array('Category')
));
The array returned will be formatted a bit differently, but it should contain all of the data you were looking for.
I actually did this to get what I want:
function getPostsFromCategory($uri) {
return $this->populatePosts($this->find(
'first',
array(
'conditions' => array(
'Category.uri' => $uri
),
'contain' => array('Post' => array('Category'))
)
));
}
function populatePosts($categoryPosts) {
$posts = $categoryPosts['Post'];
$count = count($categoryPosts['Post']);
for ($i = 0; $i < $count; $i++) {
$categories = $this->Post->find('first', array('conditions' => array('Post.id' => $categoryPosts['Post'][$i]['id']),
'contains' => array('Category')));
// unset($categories['Post']);
foreach ($categories['Category'] as $cat) {
$categoryPosts['Post'][$i]['Categories'][] = $cat;
}
unset($categories);
}
return $categoryPosts;
}
I really don't know if this can be done in a more CakePHP-y way though:
I returns something like this:
Array
(
[Category] => Array
(
[id] => 3
[name] => festivals
[uri] => festivals
[created] => 2010-11-25 20:43:03
[modified] => 2010-11-25 20:43:03
)
[Post] => Array
(
[0] => Array
(
[id] => 28
[title] => A Post
[Categories] => Array
(
[0] => Array
(
[id] => 3
[name] => festivals
[uri] => festivals
[created] => 2010-11-25 20:43:03
[modified] => 2010-11-25 20:43:03
)
[1] => Array
(
[id] => 4
[name] => destinations
[uri] => destinations
[created] => 2010-11-28 13:04:52
[modified] => 2010-11-28 13:04:52
)
)
)
)