Cakephp subquery in paginate - php

I have an existing cakephp (version 2) controller index function doing this:
$options = ['Person.name LIKE' => $term];
$this->set('people', $this->Paginator->paginate($options));
resulting in a paginated table in the view.
My Person model references a child model of Appointment, where one person has many appointments like so:
public $hasMany = [
'Appointment' => [
'className' => 'Appointment',
'foreignKey' => 'person_id',
'dependent' => false
]
]
I now need to add a Person's Oldest Appointment Date column to my table, i.e. if working with raw SQL I might do this:
select
Person.id,
Person.name,
(select
min(Appointment.Date) from Appointment
where Appointment.person_id = Person.id
) as OldestAppointmentDate
from Person
where Person.name like 'foo%'
How can I modify the paginate() parameters so that this new field is included in the results and is sortable by paginate in the usual way?

The most simple way would probably be to use a virtual field, which you can then include in the paginators fields option, something like:
// in Model/Person.php
public $virtualFields = array(
'OldestAppointmentDate' => '
select
min(Appointment.Date)
from
Appointment
where
Appointment.person_id = Person.id
';
);
// in your controller action
$this->Paginator->settings['fields'] = array(
'Person.id',
'Person.name'
'Person.OldestAppointmentDate'
);
// ...
That will include the subquery and create the required aliases accordingly, and things get stitched together automatically so that the results look like as if OldestAppointmentDate is an actual field of Person, and you can refer to it in the paginator helper like any other field, ie:
$this->Paginator->sort('Person.OldestAppointmentDate');
See also
Cookbook > Models > Virtual fields
Cookbook > Core Libraries > Components > Pagination > Query Setup

Related

Cakephp 3.0 how to populate a select field with database values instead of numeric index

How to populate a select dropdown in cakephp3 from a database table.
Currently Cakephp produces an array (list of options) which is numeric indexed.
The option values should have id of database records (should not be the numeric keys generated by Cakephp)
Departments(table) is a taxonomy to classify events.
Events(table) which should store department based list of events.
A event may belongs to many departments. How do I achieve this?
EventsController.php
<?php $department_results = $connection->execute('SELECT departmentname FROM department')->fetchAll('assoc'); // list of departments
$this->set('departmentvalues', $department_results );
Events: add.ctp
<?php echo $this->Form->input('department', array('label' => false,
'div' => false,
'type' => 'select',
'multiple'=>'checkbox',
'legend' => 'false',
'options' => $departmentvalues,
'empty' => 'Please select a Department'
));
Objective:
A select dropdown with values from database, option values should be id of the database record
Expected result:
<select name="department"><option value="2">Maths</option><option value="4">Physics</option></select>
Issue:
cakephp generates numeric indexed array of options.
<select name="department"><option value="0">Maths</option><option value="1">Physics</option></select>
You should really use the CakePHP standard for querying your models, instead of raw SQL, especially in the case of a simple list.
Something like the below should do what you need:
$department_results = $this->Departments->find('list')
->hydrate(false)
->fields(['id','departmentname'])
->toArray();
$this->set('departmentvalues', $department_results);
Note that you will need to include the fields as you have named your column departmentname. By default, a find('list') should return id and name fields.
Another option is to set the displayField this way :
`class DepartmentsTable extends Table {
public function initialize(array $config) {
parent::initialize($config);
$this->displayField('departmentname');
}
}`
In the controller you may just call the method find('list') this way :
$departments= TableRegistry::get("Departments");
$departmentvalues=$departments->find('list')
->hydrate(false)
->toArray();
$this->set('departmentvalues', $department_results);

Yii Relational Queries with MySQL's SUM

In Yii 1.1.x using relational queries, consider the following tables:
Table 'Invoice'
id, original_balance, invoice_date, due_date
Table 'InvoicePayments'
id, invoice_id, payment_amount, old_balance, new_balance, payment_date
Modal invoice.php function relations():
'Payments' => array(self::HAS_MANY, 'InvoicePayments', array( 'id' => 'invoice_id' )
If I want to use a relational query on a particular Invoice object, how can I get the SUM of all InvoicePayments.payment_amount?
Example (doesn't work):
$model = Invoice::model()->with( array( 'InvoicePayments' => array( "??.SUM(payment_amount) )->findByPk(1);
I am finding that without this I have to pull the relational query without the SUM and loop through each InvoicePayments.payment_amount to get the total amount paid towards the invoice.
Any help is appreciated, thanks for looking
Use the Statistical Query STAT provided by Active Record and force it to use the SQL SUM function by assigning its select option :
invoice.php :
public function relations()
{
return array(
'payments' => array(self::HAS_MANY, 'InvoicePayments', array( 'id' => 'invoice_id' )
'paymentsSum'=>array(self::STAT, 'InvoicePayments', 'invoice_id', 'select' => 'SUM(payment_amount)'),
);
}
Then you can use it to load one or many models by using any of the lazy or eager approache, here is 3 different examples :
// the lazy loading approach
$invoice=Invoice::model()->findByPk(10);
$total = $invoice->paymentsSum;
// the eager loading approach
$invoices=Invoice::model()->with('paymentsSum')->findByPk(10);
$total = $invoice->paymentsSum;
// the eager loading approach to load all invoices
$invoices=Invoice::model()->with('paymentsSum')->findAll();
$total_of_invoice_10 = $invoices[10]->paymentsSum;

HABTM with Pagination

At first take a look at the following model structure:
Model Building:
id
name
Model BuildingRange:
id
building_id
postalcode
Ok, so BuildingRange $belongsTo Building and Building $hasMany BuildingRange. Should be clear til' here.
Now let
$current_postalcode="12345";
I know want to do something like this in the BuildingController:
$this->paginate('Building',array('Building.BuildingRange.postalcode'=>$current_postalcode));
In text: I want to select all buildings for that an entry "BuildingRange" with $current_postalcode exists. How do you do that?
I appreciate your help!
When dealing with such a hasMany association, CakePHPs auto-magic needs two queries, one on the Building table, and one on the BuildingRange table. When passing conditions via the pagiante method, these conditions will be passed to the first query, and thus this it will fail since the associated models table isn't joined.
This problem can be solved on a few different ways, one would be using an ad-hoc join, for example:
$this->paginate = array
(
'joins' => array
(
array
(
'table' => 'building_ranges',
'alias' => 'BuildingRange',
'type' => 'LEFT',
'conditions' => array('BuildingRange.building_id = Building.id')
)
)
);
$this->paginate('Building', array('BuildingRange.postalcode' => $current_postalcode));
This would result in a query that looks something like this:
SELECT `Building`.`id`,
`Building`.`name`
FROM `buildings` AS `Building`
LEFT JOIN `building_ranges` AS `BuildingRange`
ON ( `BuildingRange`.`building_id` = `Building`.`id` )
WHERE `BuildingRange`.`postalcode` = '12345'
LIMIT 20
Note that in the conditions passed to the paginate method there is no need to reference the BuildingRange model through the Building model, ie no need to use Builduing.BuildingRange (that wouldn't work anyway).
ps, it's always good to mention the CakePHP version you are using!

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.

Categories