I have a photoblog built on CakePHP 2.0 with a data structure that looks like:
POSTS <-habtm-> TAGS <-habtm-> IMAGES
I am building an AJAX-based feature to find all blog posts and images that match a given tag. Page 1 of Posts and page 1 of Images are loaded into adjacent panels when a tag is first selected. After that, they can be paged through independently. For the most part this is working fine, except when I am fetching the initial pages of data.
I am using paginate() twice in the first action -- once to get my Posts and a second time to get the Images. The problem is that the conditions I assign to paginate() for the second model in the sequence are completely ignored. Individually they both work fine, and switching their order has confirmed it's a sequence-dependent problem for me, rather than restricted to one of the models or the other.
I've searched to see if anyone else has encountered similar problems in the past, but this is either an unusual design choice on my part or I'm not finding the right search query.
My basic $paginate array is declared as follows in my TagsController.php:
public $paginate = array(
"PostsTag" => array(
"limit" => 4,
"order" => "Post.id DESC",
"contain" => array(
"Tag",
"Post" => array("fields" => array(
"id", "title", "created"))
),
"group" => array("Post.id")
),
"ImagesTag" => array(
"limit" => 4,
"order" => "Image.id DESC",
"contain" => array(
"Tag",
"Image" => array("fields" => array(
"id", "title", "url", "created", "gallery"))
),
"group" => array("Image.id")
)
);
From my main search action I call two private functions:
$posts = $this->post_pagination($tagIds);
$images = $this->image_pagination($tagIds);
which add the limiting conditions to $paginate and look like this:
private function post_pagination($tags, $page = 1) {
$this->paginate['PostsTag']['conditions'] = array(
"status" => 1,
"OR" => array("tag_id" => $tags)
);
$this->paginate['PostsTag']['page'] = $page;
return $this->paginate("PostsTag");
}
private function image_pagination($tags, $page = 1) {
$this->paginate['ImagesTag']['conditions'] = array(
"gallery" => 1,
"OR" => array("tag_id" => $tags)
);
$this->paginate['ImagesTag']['page'] = $page;
return $this->paginate("ImagesTag");
}
Cake is respecting limit, order, contain, etc. without issue, but drops the ball on conditions specifically for whichever model I try to paginate over second. It feeds me back the first 4 results ordered properly, but completely unfiltered. I do not think my somewhat complicated conditions are at fault either -- as long as I don't break syntax, I can type completely random strings into conditions for the second paginate() and get back identical results.
Any help or suggestions are greatly appreciated.
[edit] Here is an SQL dump of the second paginate() query:
SELECT `PostsTag`.`id`, `PostsTag`.`post_id`, `PostsTag`.`tag_id`,
`Tag`.`id`, `Tag`.`name`, `Post`.`id`, `Post`.`title`, `Post`.`created`
FROM `posts_tags` AS `PostsTag`
LEFT JOIN `tags` AS `Tag` ON (`PostsTag`.`tag_id` = `Tag`.`id`)
LEFT JOIN `posts` AS `Post` ON (`PostsTag`.`post_id` = `Post`.`id`)
WHERE 1 = 1
GROUP BY `Post`.`id`
ORDER BY `Post`.`id`
DESC LIMIT 4
As you can see, Cake is generating a WHERE 1 = 1 in place of my conditions.
DEAR PEOPLE FROM THE FUTURE: Here's what we've figured out so far...
OP is correct that YourController::$paginate is only fed into the PaginatorComponent once. If you need to call YourController::paginate() again with different options, you'll need to unload the component first, e.g.:
$this->Components->unload('Paginator');
Then, the next time you call YourController::paginate(), it will reload whatever's in the YourController::$paginate property.
So upon some more poking around I discovered the following:
Any alterations made to $paginate after an initial paginate() call is made are not carried through to the Paginator component. This applies to conditions, order, limit, etc.
So doing this:
$this->paginate['<model1>']['conditions'] = array( ... );
$model1Results = $this->paginate("<model1>");
$this->paginate['<model2>']['conditions'] = array( ... );
$model2Results = $this->paginate("<model2>");
Will return results for <model1> that obey the new conditions/order/limit/whatever you've applied, but your results for <model2> will be based on the original conditions defined for it in $paginate. Your controller will see the updates to $paginate just fine, but it appears $paginate can only be grabbed by Paginator once.
The workaround I have found is to make any and all changes to $paginate BEFORE the first paginate() call, so:
$this->paginate['<model1>']['conditions'] = array( ... );
$this->paginate['<model2>']['conditions'] = array( ... );
$model1Results = $this->paginate('<model1>');
$model2Results = $this->paginate('<model2>');
I've been poking around in PaginatorComponent.php to figure out why things work this way, and any further insight would, of course, be appreciated.
Related
Hello I am having a tought time figuring out how to use arrays in cakephp. right now i have a view with 2 columns, active and startYear. i need to grab the start years for all of the columns in the view and sho i have this code.
public function initialize(array $config)
{
$this->setTable('odb.SchoolYear');
}
controller
public function index()
{
$deleteTable = $this->loadModel('DeletedTranscripts');
$this->$deleteTable->find('all', array(
'conditions' => array(
'field' => 500,
'status' => 'Confirmed'
),
'order' => 'ASC'
));
$this->set('startYear',$deleteTable );
}
once i have the array captured and put into lets say startYear can in input a statement like this into my dropdown list to populate it?
<div class="dropdown-menu">
<a class="dropdown-item" href="#"><?= $delete->startYear; ?></a>
</div>
i have been looking for answers for quite awhile any help would be awesome.
Couple of things:
Loading Tables in CakePHP
For this line:
$deleteTable = $this->loadModel('DeletedTranscripts');
While you can get a table this way, there's really no reason to set the return of loadModel to a variable. This function sets a property of the same name on the Controller, which almost correctly used on the next line. Just use:
$this->loadModel('DeletedTranscripts');
Then you can start referencing this Table with:
$this->DeletedTranscripts
Additionally, if you're in say the DeletedTranscriptsController - the corresponding Table is loaded for you automatically, this call might be unnecessary entirely.
Getting Query Results
Next, you're close on the query part, you've can start to build a new Query with:
$this->DeletedTranscripts->find('all', array(
'conditions' => array(
'field' => 500,
'status' => 'Confirmed'
),
'order' => 'ASC'
));
But note that the find() function does not immediately return results - it's just building a query. You can continue to modify this query with additional functions (like ->where() or ->contain()).
To get results from a query you need to call something like toArray() to get all results or first() to get a single one, like so:
$deletedTranscriptsList = $this->DeletedTranscripts->find('all', array(
'conditions' => array(
'field' => 500,
'status' => 'Confirmed'
),
'order' => 'ASC'
))->toArray();
Sending data to the view
Now that you've got the list, set that so it's available in your view as an array:
$this->set('startYear', $deletedTranscriptsList );
See also:
Using Finders to Load Data
Setting View Variables
I also noticed you've had a few other related questions recently - CakePHP's docs are really good overall, it does cover these systems pretty well. I'd encourage you to read up as much as possible on Controller's & View's.
I'd also maybe suggest running through the CMS Tutorial if you've not done so already, the section covering Controllers might help explain a number of CakePHP concepts related here & has some great working examples.
Hope this helps!
I'm trying to paginate with a custom finder in Cakephp 3.0.0-RC1. The docs say that I should do this:
$customFinderOptions = [
'tags' => $tags
];
$this->paginate = [
'finder' => [
'tagged' => $customFinderOptions
]
];
$articles = $this->paginate($this->Articles);
So my controller method contains
$this->paginate = [
'finder' => [
'recentActivity' => [
'limit' => 5,
'offset' => 2,
'foo' => 'bar'
]
]
];
$results = $this->paginate($this->ModelName);
At the top of ModelNameTable::findRecentActivity(), pr($options) results in this:
Array
(
[foo] => bar
[whitelist] => Array
(
[0] => limit
[1] => sort
[2] => page
[3] => direction
)
)
Where did limit and offset go? Digging in the CakePHP 3 core a bit, I see at the top of ORM.Table.php
public function callFinder($type, Query $query, array $options = [])
{
$query->applyOptions($options);
$options = $query->getOptions();
$finder = 'find' . $type;
if (method_exists($this, $finder)) {
return $this->{$finder}($query, $options);
}
Ah, so limit and offset went into $query. Unfortunately, I can't think of a practical way to get them back out from inside my custom finder method.
I checked, and $options accurately reflects the options I set before it's overwritten by $options = $query->getOptions(); and the limit and offset values are removed.
The docs led me to expect $options to be passed (in its entirety) to my custom finder method. So my question is...
Am I using the paginator component / customer finder incorrectly?
Is this intended behavior, or something I should report an issue about? I'm new to CakePHP 3 and still wrapping my head around it, so I didn't want to jump to the conclusion that it was a bug.
Sidenote: I know that I can hack around this by using different keys in $options, like foo_limit and foo_offset to accomplish what I want.
In understand where the confusion comes from. Apart from my concerns expressed in the question comments about the possibly contradictory options your query may get, I think you should know that the following list of options are directly copied to the query and not interpreted as custom options for your finder method:
'fields'
'conditions'
'join'
'order'
'limit'
'offset'
'group'
'having'
'contain'
'page'
Those options will call the matching query methods to set its internal state.
Turns out...
It is not true that all options set for the custom finder are ignored. Most options that are listed in José's answer are integrated into the query and then removed from $options before $options is passed to the custom finder method.
However, the custom finder's limit is always overwritten by either $this->paginate['limit'] or the default value of 20 (and likewise with page). But it looks like this has been identified as a bug and is going to be fixed.
I'm using Containable in an action like this:
public function index()
{
$this->User->recursive = -1;
$this->User->Behaviors->load('Containable');
if ($this->RequestHandler->accepts('xml'))
{
$this->set('users', array("Users" => array("UserEntry" => $this->User->find('all',
array(
'fields' => array('User.id','User.username', 'User.email', 'User.created', 'User.modified'),
'contain' => array(
'Group' => array(
'fields' => array('Group.id','Group.name','Group.created'),
)
)
)
))));
}
else if ($this->RequestHandler->accepts('json'))
{
}
else if ($this->RequestHandler->accepts('html'))
{
$this->set('users', $this->paginate());
}
}
It gets all of the data I need, but there is one thing that I can't figure out. There is a HABTM relationship between users and groups with a join table users_groups. I'm serializing the output of find('all') into Xml for a REST Api in the view. The problem is that the data contains an extra 'GroupsUser' array nested in my 'Groups' array. The users of the Api do not need to know about the join table information so I would like to remove it. The current output looks like this:
index.ctp
<?php
//debug($users);
$xml = Xml::build($users, array('return' => 'domdocument'));
echo $xml->saveXML();
?>
output of index.ctp -> http://www.pastie.org/2789367
See the GroupsUser tag nested in the Group tag? That is what I want to remove. If there is not a nice easy way to do this I will either build the xml by hand using some loops in the view or create my own find method in the the model and use unset() on GroupsUser. Both of those solutions are not ideal, so I'm hoping someone here has a better one. :)
I you are positive that everything is being done in a join (only belongsTo associations) you may use the containable component with autofields in false something like this
$this->Post->Behaviors->attach('Containable', array('autoFields' => false));
(this is to attach it dynamiclly in the controller part.
if your find has hasMany association, you will have 2 queries instead of so cake needs to fetch for this fields to do the join. You may also use linkable component for this case, that put all in joins instead of a lot of queries giving you chance to select only the fields you want.
here is a link for the linkable component
I have a function in my Event model called getEvents - you can pass limit, start and end dates, fields, event types, and event subtypes.
I read that paginate can accept all the parameters I'm using like joins, conditions, limit...etc just like a normal find can.
It returns data just fine when I don't try to paginate. But - I'd like to be able to pass it a paginate variable to tell it instead of doing this:
$this->recursive = -1;
$data = $this->find('all', $qOptions);
to do this:
$this->recursive = -1;
$data = $this->paginate($qOptions);
When I try that, though, it gives me lots of errors. I can specify the errors later if needed - for now, I guess I'm looking for - is this something that can be done? If so, how?
Is there another better way to do something like this? I spent enough time making this function do just what I want, and allowing all the options passed...etc - it just seems like a waste if I can't also use it for pagination. But - if it's not ideal, I'm ok hearing that too. Thanks in advance.
EDIT:
I'm reading other things online that say you shouldn't use paginate in your model, because it draws from URL variables, which defeats the MVC structure purpose. This makes sense, but does that mean I have to write the same joins/queries in both model and controller? And in every action that it's needed?
The way I figured out how I can keep my complex find in my model without having to rewrite it a second time in the controller is by passing a $paginate boolean variable.
If $paginate is true, it returns just the options created, which can then be used in the controller's pagination. If it's false (meaning we don't want to paginate), it returns the actual event results. So far this seems to be working.
In my getEvents() function (this method is in the Events model)
if($paginate) {
return $qOpts; // Just return the options for the paginate in the controller to use
} else {
$data = $this->find('all', $qOpts); // Return the actual events
return $data;
}
Then, in my Events/Index (events controller, index action - where I know I want pagination):
$this->Event->recursive = -1; // (or set recursive = -1 in the appModel)
$opts['paginate'] = true;
$paginateOptions = $this->Event->getEvents($opts);
$this->paginate = $paginateOptions; // Set paginate options to just-returned options
$data = $this->paginate('Event'); // Get paginate results
$this->set('data', $data); // Set variable to hold paginated results in view
The paginate() model method does not accept the same parameters as a find(). Specifically, find() wants an array of options, but paginate() wants every option passed individually. See Custom Query Pagination in the CakePHP book.
So, instead of:
$data = $this->paginate($qOptions);
You want something like:
$data = $this->paginate($qOptions['conditions'], $qOptions['fields'], ...);
EDIT
Custom model pagination isn't a function that you call. It's a function that you need to implement and will be called by the CakePHP framework. In the example in your question you are trying to manually call $this->paginate(...) from somewhere in your model. That doesn't work. Instead, do this.
In your model, implement the paginate and paginateCount methods.
function paginate($conditions, $fields, ...)
{
// return some data here based on the parameters passed
}
function paginateCount($conditions, ...)
{
// return some rowcount here based off the passed parameters
}
Then, in your controller you can use the standard pagination functions.
function index()
{
$this->paginate = array('MyModel' => array(
'conditions' => array(...),
'fields' => array(...),
));
$this->set('myobjects', $this->paginate('MyModel'));
}
Now, the Controller::paginate() function will grab the conditions and other data from the Controller::paginate parameter and, instead of passing it to your Model::find it will pass it to your custom Model::paginate() and Model::paginateCount() functions. So, the data that is returned is based on whatever you do in those two methods and not based on a standard find().
}
you can use this one which is working fine for me.
$condition="your where condition";
$this->paginate = array(
'fields' => array('AsinsBookhistory.id', 'AsinsBookhistory.reffer_id', 'AsinsBookhistory.ISBN','AsinsBookhistory.image','AsinsBookhistory.title','AsinsBookhistory.last_updatedtime'),
'conditions' => $condition,
'group' => array('AsinsBookhistory.ISBN'),
'order' => array('AsinsBookhistory.last_updatedtime' => 'desc')
);
$this->set('lastvisitedbooks', $this->paginate('AsinsBookhistory'));
$paginate array are similar to the parameters of the Model->find('all') method, that is: conditions, fields, order, limit, page, contain, joins, and recursive.
So you can define your conditions like this :
var $paginate = array(
'Event' => array (...)
);
Or you can also set conditions and other keys in the $paginate array inside your action.
$this->paginate = array(
'conditions' => array(' ... '),
'limit' => 10
);
$data = $this->paginate('Event');
http://book.cakephp.org/2.0/en/controllers.html
http://book.cakephp.org/2.0/en/core-libraries/components/pagination.html
R u using $name = 'Event' in your controller ?
If we wont mention model name in $this->paginate() , it will use model as mentioned in $name otherwise look in var $uses array and in that will get Model name (first one )
for e.g var $uses = array('Model1','Model2'); // $name != mentioned
n you want pagination with respect to Model2 then you have to specify ModelName in paginate array like $this->paginate('Model2') otherwise Model1 will be considered in pagination.
The site is already built from years. I am doing some modifications to it. It has controllers "Posts" and "Topics". On the topics page all recent posts are displayed. So it is a simple find of posts from "Posts" table. The fetch is not all assiciated to topic as we are showing all "Posts" and not "Topic" specific "Posts". In Topics controller,
App::import('Model', 'Post'); and $Posts = new Post;
are added and I call the action "getDiscussions" from Model "Post". The problem is that though I am not using any left join with "Topics" and "Users" (which is one more model used to display user details with post), the query is adding Left joins with two tables and that is giving a wrong result.
Please help.
Many Thanks
Yukti
I think it is better to try to find out why cake is doing these "joins", rather than performing yet another forced query, so you might want to look into your model associations to check dependancies. Also, you can set $this->Posts->recursive = -1; to make sure you don't import unwanted stuff.
In the end, I am not sure what query you are trying to perform, so I can't help you further. However, here is a sample of a query I have successfully used to perform a left join, as I've noticed that many examples I found while writing my own were bugged:
$user_record = $this->User->find('first', array(
'conditions' => array('`Openidurl`.`openid`' => $openid),
'joins' => array(
array(
'table' => 'openidurls',
'alias' => 'Openidurl',
'type' => 'LEFT',
'foreignKey' => 'user_id',
'conditions'=> array('`Openidurl`.`user_id` = `User`.`id`')
)
)
)
);