Retrieving data several layers deep in CakePHP from DEEP models - php

Long story short, my database is set up as follows:
Category hasMany Topic
Topic belongsTo Category
Topic hasMany Section
Section belongsTo Topic
Section hasMany Subsection
Subsection belongsTo Section
In my Categories controller I can easily get all my information using a simple find query. However, in my subsections controller for example, I would like to do a complex find query such as:
$conditions = array(
'Subsection.title' => $subsectionname,
'Section.title' => $sectionname,
'Topic.title' => $topicname,
'Category.title => $categoryname
);
$subsection = $this->Subsection->find('first', array(
'conditions' => $conditions,
'recursive' => 3,
));
However, I can only get data about the Subsection and the Section from the query this way. I can access the subsection's Category in the View by using a call such as $mysubsection['Section']['Topic']['Category']['title']; but I'd ideally like to have all the filtering done in one call. Is this possible?

Figured out a way of doing this. Specifically, I wanted to use really nice looking URLs, so that a user could type in something like http://example.com/aCategoryTitle/aTopicTitle/aSectionTitle/aSubsectionTitle - but this data needs to be validated so that the subsection definitely belongs in the correct category, topic etc..
$conditions = array(
'Subsection.title' => $subsectionname,
'Section.title' => $sectionname,
);
$subsection = $this->Subsection->find('all', array(
'conditions' => $conditions,
'recursive' => 2,
));
This is a simple find call which matches all subsections with the correct title and section title.
if (!$subsection) {
// nothing was returned
$this->redirect(array('controller' => 'KnowledgeBase', 'action' => 'index'));
}
If there is no subsection matching that criteria, the user is redirected. However, there still may be multiple subsections found with the same section title. Take for example the following URLs:
http://example.com/Cats/Feeding/Food/Meat and http://example.com/Dogs/Feeding/Food/Meat
With these two examples, there would be multiple subsections found which match the above criteria. So we need to carry out some further validation:
foreach ($subsection as $asubsection) {
if (($asubsection['Section']['Topic']['Category']['title'] == $categoryname) &&
($asubsection['Section']['Topic']['title'] == $topicname) &&
($asubsection['Section']['title'] == $sectionname) &&
($asubsection['Subsection']['title'] == $subsectionname)) {
// correct parameters
// this variable can now be passed to the view
$this->set('mysubsection', $asubsection);
} else {
// incorrect parameters in URL
$this->redirect(array('controller' => 'KnowledgeBase'));
}
}
This loops through all the Subsections we've found, and ensures that their titles, and those of the section, topic and category they belong to, match the URL (as given by $this->request->params).
Probably not the tidiest way of doing it, but it works, and works nicely with my routing. Always happy to take any further suggestions on board, but for now this is working well for me.

Related

A better way to re-use a boilerplate search parameter across controller actions

How do I re-use boilerplate query code across multiple controller actions in CakePHP 2.4?
I've got some join code I need to re-use across multiple actions, which excludes all Posts which belong to a Project where Project.published = 0 from my find(). I've done this by creating a public class array to hold the query code.
This works, however I'd like to add some additional parameters based on variables- specifically, allowing the owner of a Project to see data belonging to their project, even if it's unpublished.
If the array were integrated as part of the controller action, I'd simply add 'ProjectAlias.user_id' => CakeSession::read("Auth.User.id") to the final OR array below. However, I can't include that as part of a class array, and I need to create it in the action, as seen below.
This doesn't feel especially elegant. Is there a cleaner / more Cake way to handle this?
My current code:
//==============
// ADDITIONAL JOIN TO RESTRICT RESULTS TO LIVE PROJECTS
//================
public $joins = array(
array(
'table' => 'projects',
'alias' => 'ProjectAlias',
'type' => 'right',
'conditions' => array(
'OR' => array( // One of these two things:
'Post.project_id' => null, // Posts with no project
'AND' => array( // And posts with a Project that is published.
'Post.project_id = ProjectAlias.id',
'OR' => array(
'ProjectAlias.published !=' => 0,
)
)
)
),
)
);
//===============
// Example function showing how this array is used. There are four in all
// so repeating the above code would get to be too much.
//================
public function example() {
// Let project leads see data from their hidden projects, by modifying the array.
// This doesn't seem very elegant!
$this->joins[0]['conditions']['OR']['AND']['OR'][] = array(
'ProjectAlias.user_id' => CakeSession::read("Auth.User.id")
);
// Use the array
$this->paginator->settings['joins'] = $joins;
$this->set('posts', $this->Paginator->paginate());
}
If I understood it right, you can create a function which requires arguments in AppController which returns the join array & call it from any actions of any controllers. Now, regarding different params for different cases, first you can use-
$this->request->action
to get the current action (or controller as well if needed).
Now, you can set an associative array or if else block in the function in AppController to define the join array, using function arguments as required. Then you can get custom made $joins array from any actions.

CakePHP pagination issues

I'm having trouble getting pagination to work when conditions are involved in the query. In my OrdersController I have the following pagination to display all of the entires and it works perfectly
$order_list = $this->Paginate('Order');
$this->Set('orders', $order_list);
When I try to find the orders for a given user it immediately throws an error and I can't even figure out what it's related to.
$order_list = $this->Paginate = array(
'conditions' => array('Order.userid' => $id)
);
$data = $this->Paginate('orders', $order_list);
$this->set(compact('data'));
In my OrdersController I also have listed:
public $paginate = array(
// other keys here.
'maxLimit' => 20
);
When I try to run this I get an error that says "An Internal Error Ocurred" which isn't helpful at all.
When I print the search results for $order_list it just spits out the parameters I'm searching for, so I don't even think it's searching.
Array ( [conditions] => Array ( [Order.userid] => 4 ) )
It basically just tells me what I'm searching for and doesn't actually search the orders table for the values.
Try this
$data = $this->paginate('Order', array('Order.userid' => $id));
$this->set(compact('data'));
You seem to mix two approaches of defining extra parameters to paginate by. See CookBook for more info.

CakePHP: Load count of related Models with bindModel

I have two models in an 1:n relation and I just want to load the count of the related items.
First one is the table/model "Ad" (one) which is related to "AdEvent" (many). AdEvents has a foreign key "ad_id".
In the controller I can use it that way and it loads the related AdEvent-records.
$this->Ad->bindModel(array('hasMany' => array(
'AdEvent' => array(
'className' => 'AdEvent',
'foreignKey' => 'ad_id',
))));
Now I just need the count without the data and I tried with param "fields" and "group" a COUNT()-statement, but in that case the result is empty. I also changed the relation to "hasOne", but no effect.
Any idea how to use the Cake-magic to do that?
EDIT:
With simple SQL it would look like this (I simplyfied it, a.id instead of a.*):
SELECT a.id, COUNT(e.id) AS count_events
FROM cake.admanager_ads AS a
JOIN ad_events AS e ON e.ad_id = a.id
GROUP BY a.id
LIMIT 50;
You can always do a manual count of course. This is what I almost always end up doing because I almost always have the data loaded already for some other purpose.
$Ads = $this->Ad->find('all')
foreach ($Ads as $Ad) {
$NumAdEvents = array(
$Ad['Ad']['id'] => sizeof($Ad['AdEvents']),
)
}
debug($NumAdEvents);
die;
Or you can use a find('count'):
$id_of_ad = 1; //insert your ad id here, or you can search by some other field
$NumAdEventsAtOneAd = $this->AdEvent->find('count', array('conditions' => array(
'AdEvent.ad_id' => $id_of_ad,
)));
debug($NumAdEventsAtOneAd);
die;

Using the CakeDC search plugin with associated models

I'm using CakePHP 1.3.8, and I've installed the CakeDC Search plugin. I have a Tutorial model, which is in a HABTM relationship with a LearningGoal model.
I have a search action & view in the Tutorials controller with which I can successfully search fields in the Tutorial model. I'd also like to filter my tutorial search results using LearningGoal checkboxes on the same form. I've tried adding various parameters to Tutorial's $filterArgs and TutorialsController's $presetVars. I've also tried moving the relevant $filterArgs to the LearningGoal model. I have not yet been able to successfully trigger the entry for learning goals in $filterArgs.
I think I must be missing something obvious. Or maybe the Search plugin doesn't support what I'm trying to do. Does anyone know how to use this plugin to search on associated models?
So here's what I've figured out. You can combine what's below with the Search plugin directions to search on related models.
The $filterArgs piece in the Tutorial model must look like this:
var $filterArgs = array(
array('name' => 'LearningGoal', 'type' => 'subquery', 'method' => 'findByLearningGoals', 'field' => 'Tutorial.id'),
);
Here's the supporting function in the Tutorial model:
function findByLearningGoals($data = array()) {
$ids = explode('|', $data['LearningGoal']);
$ids = join(',', $ids);
$this->LearningGoalsTutorial->Behaviors->attach('Containable', array('autoFields' => false));
$this->LearningGoalsTutorial->Behaviors->attach('Search.Searchable');
$query = $this->LearningGoalsTutorial->getQuery('all',
array(
'conditions' => array('LearningGoalsTutorial.learning_goal_id IN (' . $ids . ')'),
'fields' => array('tutorial_id'),
)
);
return $query;
}
In TutorialsController, $presetVars should look like this:
public $presetVars = array(
array('field' => 'LearningGoal', 'type' => 'checkbox', 'model' => 'Tutorial'),
);
And in my search action in TutorialsController, I did this:
$this->LearningGoal = $this->Tutorial->LearningGoal;
The Prg component seems to need that.
I am using CakePHP version 2.X
Every time I come to do this in a project I always spend hours figuring out how to do it using CakeDC search behavior so I wrote this to try and remind myself with simple language what I need to do. I've also noticed that although Michael is generally correct there is no explanation which makes it more difficult to modify it to one's own project.
When you have a "has and belongs to many" relationship and you are wanting to search the joining table i.e. the table that has the two fields in it that joins the tables on either side of it together in a many-to-many relationship you want to create a subquery with a list of IDs from one of the tables in the relationship. The IDs from the table on the other side of the relationship are going to be checked to see if they are in that record and if they are then the record in the main table is going to be selected.
In this following example
SELECT Handover.id, Handover.title, Handover.description
FROM handovers AS Handover
WHERE Handover.id in
(SELECT ArosHandover.handover_id
FROM aros_handovers AS ArosHandover
WHERE ArosHandover.aro_id IN (3) AND ArosHandover.deleted != '1')
LIMIT 20
all the records from ArosHandover will be selected if they have an aro_id of 3 then the Handover.id is used to decide which Handover records to select.
On to how to do this with the CakeDC search behaviour.
Firstly, place the field into the search form:
echo $this->Form->create('Handover', array('class' => 'form-horizontal'));?>
echo $this->Form->input('aro_id', array('options' => $roles, 'multiple' => true, 'label' => __('For', true), 'div' => false, true));
etc...
notice that I have not placed the form element in the ArosHandover data space; another way of saying this is that when the form request is sent the field aro_id will be placed under the array called Handover.
In the model under the variable $filterArgs:
'aro_id' => array('name' => 'aro_id', 'type' => 'subquery', 'method' => 'findByAros', 'field' => 'Handover.id')
notice that the type is 'subquery' as I mentioned above you need to create a subquery in order to be able to find the appropriate records and by setting the type to subquery you are telling CakeDC to create a subquery snippet of SQL. The method is the function name that are going to write the code under. The field element is the name of the field which is going to appear in this part of the example query above
WHERE Handover.id in
Then you write the function that will return the subquery:
function findByAros($data = array())
{
$ids = ''; //you need to make a comma separated list of the aro_ids that are going to be checked
foreach($data['aro_id'] as $k => $v)
{
$ids .= $v . ', ';
}
if($ids != '')
{
$ids = rtrim($ids, ', ');
}
//you only need to have these two lines in if you have not already attached the behaviours in the ArosHandover model file
$this->ArosHandover->Behaviors->attach('Containable', array('autoFields' => false));
$this->ArosHandover->Behaviors->attach('Search.Searchable');
$query = $this->ArosHandover->getQuery('all',
array(
'conditions' => array('ArosHandover.aro_id IN (' . $ids . ')'),
'fields' => array('handover_id'), //the other field that you need to check against, it's the other side of the many-to-many relationship
'contain' => false //place this in if you just want to have the ArosHandover table data included
)
);
return $query;
}
In the Handovers controller:
public $components = array('Search.Prg', 'Paginator'); //you can also place this into AppController
public $presetVars = true; //using $filterArgs in the model configuration
public $paginate = array(); //declare this so that you can change it
// this is the snippet of the search form processing
public function admin_find()
{
$this->set('title_for_layout','Find handovers');
$this->Prg->commonProcess();
if(isset($this->passedArgs) && !empty($this->passedArgs))
{//the following line passes the conditions into the Paginator component
$this->Paginator->settings = array('conditions' => $this->Handover->parseCriteria($this->passedArgs));
$handovers = $this->Paginator->paginate(); // this gets the data
$this->set('handovers', $handovers); // this passes it to the template
If you want any further explanation as to why I have done something, ask and if I get an email to tell me that you have asked I will give an answer if I am able to.

Is it possible to page and sort two different models in the same view in CakePHP?

I want to do something very straight forward and simple. I want to have two different sets of paginated data on the same page. The two different sets depend on different models. For discussion's sake we'll say they are Image and Item.
I can set up two pagers for two models, and get the correct set of objects. I can get the correct pager links. But when it comes to actually following the links to the parameters, both pagers read the parameters and assume they apply to them.
It winds up looking something like this:
$this->paginate = array (
'Item'=>array(
'conditions'=>array('user_id'=>$id),
'limit' => 6,
'order' => array(
'Item.votes'=>'desc',
'Item.created'=>'desc'
),
'contain'=>array(
'User',
'ItemImage' => array (
'order'=>'ItemImage__imageVotes desc'
)
)
),
'Image'=>array(
'limit'=>6,
'contain'=>array(
'User',
'ItemImage'=>array('Item'),
),
'order'=>array(
'Image.votes'=>'desc',
'Image.views'=>'desc'
),
'conditions'=>array(
'Image.isItemImage'=>1,
'Image.user_id'=>$id
)
)
);
$this->set('items', $this->paginate('Item'));
$this->set('images', $this->paginate('Image'));
That's in the controller. In the view I have sort links that look like this:
<div class="control"><?php echo $this->Paginator->sort('Newest', 'Image.created', array('model'=>'Image')); ?></div>
However, that yields a link that looks like this:
http://localhost/profile/37/page:1/sort:Image.created/direction:asc
There's nothing in there to tell the paginator which model I intend to sort. So when I click on the link it attempts to sort both models by Image.created. The result is an error, because Item cannot be sorted by Image.created. Is there something I'm doing wrong? Or is this something that isn't supported by CakePHP's paginator?
You'll need to override the paginate method for the Model of the Controller of that page.
I did something similar, maybe this snippet will help:
function paginate($conditions, $fields, $order, $limit, $page = 1, $recursive = null, $extra = array())
{
$pageParams = compact('conditions', 'fields', 'order', 'limit', 'page', 'recursive', 'group');
$this->contain('ModuleType', 'NodeDescriptor');
$pageItems = $this->find('all',$pageParams);
$pagesOut = array();
foreach($pageItems as $pageItem)
{
$status = $pageItem['SiteAdmin']['status_id'];
$moduleInfo = null;
$nodeTitle = $pageItem['NodeDescriptor']['title'];
$published = $pageItem['NodeDescriptor']['published'];
$pageitemID = $pageItem['SiteAdmin']['id'];
$moduleId = $pageItem['SiteAdmin']['module_id'];
$contName = $pageItem['ModuleType']['controller'];
if($moduleId)
{
$thisModel = ClassRegistry::getObject($moduleType);
$thisModel->contain();
$moduleInfo = $thisModel->read(null,$moduleId);
$moduleInfo = $moduleInfo[$moduleType];
}
$pagesOut[] = array(
'status'=>$status,
'node'=>$nodeTitle,
'published'=>$published,
'info'=>$moduleInfo,
'module_id'=>$moduleId,
'contName'=>$contName,
'pageitem_id'=>$pageitemID);
}
return $pagesOut;
}
By doing it this way, you gain control over the parameters passed to paginate, so you can pass model specific data, control flags etc.
The easiest solution would be to implement both grids as elements that fetch their own data and use AJAX to load the elements into the page.
The only other option would be to modify the params so you pass the params for both grids to each grid when sorting or stepping through pages. The code posted by Leo above is a good start. You can prepend the Model key from the paginate array onto each named param and make sure you pass all url params to the paginate function and you should be headed in the right direction.

Categories