CakePHP 2 - Deriving table name of model within Behavior - cakephp

Whats the usual practice in getting a behavior (linked to multiple models) to build filters for SQL queries, and then read the table that belongs to that model?
I have a Behavior function which is meant to do a database query with certain SQL conditions. I currently pass in the $this->request->data.
I have issues building the SQL conditions because i'm not sure how to derive the name of the table (that corresponds to the model). See below for example, I want to change "BillingCenterDetail" which is the table name (and also the model name), to something generic I can use across different models. I want this table name to be derived automatically based on the model name. I'm not sure if i can use the $model reference for that.
public function saveWithTimeConstraintCheck(Model $model, $data) {
//FIND ALL RECORDS THAT OVERLAP
$overlapfilter = array(
'BillingCenterDetail.billing_center_id =' => $data['BillingCenterDetail']['billing_center_id'],
'BillingCenterDetail.startdate <=' => $data['BillingCenterDetail']['enddate'],
'BillingCenterDetail.enddate >=' => $data['BillingCenterDetail']['startdate']
);
... after building the filter, I can use $model->find to execute the query, this should be OK because its generic.
$overlapresults = $model->find('all', array('conditions' => $overlapfilter));

I've answered my own question.
And actually to build filter conditions, I needed name of the model, not the name of the table, because the name of the table is a plural name with the "S" at the end.
I used
$Model->name
From:
CakePHP: get current model name in a controller
For table names i found out u can also use
$this->Model->table
cakephp - get table names and its column details

Related

Cakephp contains association when using matching()

In my project I have the following tables: Messages, Recipients, Groups and Users. A Message has many Recipients, and a Recipient has one Group and one User.
In my RecipientsTable::beforeFind I have some code to automatically contain Groups and Users for Recipient finds, since I always need to access those associations.
public function beforeFind($event, $query, $options, $primary) {
return $query->contain([
'Groups',
'Users',
]);
}
I don't know if this is a bad design decision but it has worked for me so far.
The problem has come now that I'm trying to filter messages by group, and I tried doing so by using the matching function:
$possible_groups = [1,2,3]; //just an example
$query->matching('Recipients', function($q) use ($possible_groups){
return $q->where(['Recipients.group_id IN' => $possible_groups]);
});
When I execute the query, I get the following error:
Messages is not associated with Groups
Is there any way to keep my beforeFind like that and be able to use matching? Or, is there a better way to automatically load associations without using beforeFind?
TL;DR: A hasMany B, B hasOne C. If a query on table A uses matching on table B and table B's beforeFind uses contain to load C, C ends up contained onto the original query (of A) and the execution fails since A is not associated with C.
Use table classes to load Table associations automatically. Like using hasMany(), hasOne(), belongsTo(), belongsToMany() in necessary table classes.
Official documentation:
https://book.cakephp.org/3.0/en/orm/associations.html
Table files should be in the Table folder inside the Model folder like src\Model\Table.

Cakephp find('list') to get displayField from child model

I have a master model, with a child model. The child model has a foreign key that links to the master model id, following cakephp naming conventions. The child model defines the name/description of the records in the master model.
I'm trying to populate a dropdown listbox in my view with these values (id and name). But the Cakephp find('list') usually gets the information from one model only.
How can I do it so that my find('list') retrieves 2 fields such as :
- child.master_id
- child.name
or
- master.id
- child.name (where child.master_id = master.id).
Thanks!
To change the fields for the keys or values of find('list', $params) you can hand an array with the key fields and the names of the fields you want to use as the second parameter over.
In your example the following code would change the key of the returned array to child.master_id and the value to child.name:
$childs = $this->Child->find('list', array(
'fields' => array('Child.master_id', 'Child.name')
));
For further information on the find type 'list' read the cookbook.
Just define the fields
For the examples given in the question you need only specify the two fields in the find call:
$result = $Child->find('list', array(
'fields' => array('master_id', 'name')
));
When find list is passed two fields, they are used as the key and value of the resultant array.
If you do need to return fields from different models, you need to tell cake to make a join. The simplest way is to specify a recursive value of 0 (Assuming the relationship Child belongsTo Master has been setup correctly):
$sameResult = $Child->find('list', array(
'fields' => array('Master.id', 'Child.name'),
'recursive' => 0
));
Note that in this scenario it is probably necessary to specify the model name in the field list to avoid ambiguous sql being generated.

Possible to have association or not based on main model's data?

I have a nodes table (Node model). I'd like it to be associated to different data types, but only if one if it's field is set to 1.
Example:
My nodes table has a data_article field (tinyint 1). I only want the Node to $hasMany Article IF that field is a 1.
I tried this:
public $hasMany = array(
'Article' => array(
'conditions' => array('Node.data_articles' => '1')
),
);
But I get an error:
Column not found: 1054 Unknown column 'Node.data_articles' in 'where
clause'
Because the association is doing the Article find in it's own query:
SELECT `Article`.`id`, `Article`.`title`, `Article`.`node_id`, ...more fields...
FROM `mydatabase`.`articles` AS `Article`
WHERE `Node`.`data_artiles` = '1'
AND `Article`.`node_id` = ('501991c2-ae30-404a-ae03-2ca44314735d')
Obviously that doesn't work, since the Node table isn't being Joined at all in this query.
TLDR:
Is it possible to have associations or not based on a field in the main model? If not, how else can I keep different data types in multiple tables, and not have to query them all every time?
I don't think that there's a standard way to do this with CakePHP (at least I can't imagine a way). What definitely is possible would be binding associated models dynamically.
So you might query your model without associations by passing the recursive parameter as -1 to the find() method and based on the result unbind the associated models dynamically. Afterwards you would have to query again, for sure. You might build a behavior out of this to make it reusable.
A quite elegant solution would be possible, if Cake could make a two-step query with a callback after the main model was queried, but before associated models are queried, but this isn't possible at the moment.
Edit: There might be a non-Cake way to archieve this more performantly with a custom Query including IF-statements, but I'm not the SQL expert here.
You should do it from the other side:
public $belongsTo = array(
'Node' => array(
'conditions' => array('Node.data_articles' => '1')
),
);

pull Drupal field values with db_query() or db_select()

I've created a content type in Drupal 7 with 5 or 6 fields. Now I want to use a function to query them in a hook_view call back. I thought I would query the node table but all I get back are the nid and title. How do I get back the values for my created fields using the database abstraction API?
Drupal stores the fields in other tables and can automatically join them in. The storage varies depending on how the field is configured so the easiest way to access them is by using an EntityFieldQuery. It'll handle the complexity of joining all your fields in. There's some good examples of how to use it here: http://drupal.org/node/1343708
But if you're working in hook_view, you should already be able access the values, they're loaded into the $node object that's passed in as a parameter. Try running:
debug($node);
In your hook and you should see all the properties.
If you already known the ID of the nodes (nid) you want to load, you should use the node_load_multiple() to load them. This will load the complete need with all fields value. To search the node id, EntityFieldQuery is the recommended way but it has some limitations. You can also use the database API to query the node table for the nid (and revision ID, vid) of your nodes, then load them using node_load_multiple().
Loading a complete load can have performance impacts since it will load way more data than what you need. If this prove to be an issue, you can either try do directly access to field storage tables (if your fields values are stored in your SQL database). The schema of these tables is buld dynamicaly depedning on the fields types, cardinality and other settings. You will have to dig into your database schema to figure it out. And it will probably change as soon as you change something on your fields.
Another solution, is to build stub node entities and to use field_attach_load() with a $options['field_id'] value to only load the value of a specific field. But this require a good knowledge and understanding of the Field API.
See How to use EntityFieldQuery article in Drupal Community Documentation.
Creating A Query
Here is a basic query looking for all articles with a photo that are
tagged as a particular faculty member and published this year. In the
last 5 lines of the code below, the $result variable is populated with
an associative array with the first key being the entity type and the
second key being the entity id (e.g., $result['node'][12322] = partial
node data). Note the $result won't have the 'node' key when it's
empty, thus the check using isset, this is explained here.
Example:
<?php
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'node')
->entityCondition('bundle', 'article')
->propertyCondition('status', 1)
->fieldCondition('field_news_types', 'value', 'spotlight', '=')
->fieldCondition('field_photo', 'fid', 'NULL', '!=')
->fieldCondition('field_faculty_tag', 'tid', $value)
->fieldCondition('field_news_publishdate', 'value', $year. '%', 'like')
->fieldOrderBy('field_photo', 'fid', 'DESC')
->range(0, 10)
->addMetaData('account', user_load(1)); // Run the query as user 1.
$result = $query->execute();
if (isset($result['node'])) {
$news_items_nids = array_keys($result['node']);
$news_items = entity_load('node', $news_items_nids);
}
?>
Other resources
EntityFieldQuery on api.drupal.org
Building Energy.gov without Views

How do I join two tables in a third n..n (hasAndBelongsToMany) relationship in CakePHP?

I have a n...n structure for two tables, makes and models. So far no problem.
In a third table (products) like:
id
make_id
model_id
...
My problem is creating a view for products of one specifi make inside my ProductsController containing just that's make models:
I thought this could work:
var $uses = array('Make', 'Model');
$this->Make->id = 5; // My Make
$this->Make->find(); // Returns only the make I want with it's Models (HABTM)
$this->Model->find('list'); // Returns ALL models
$this->Make->Model->find('list'); // Returns ALL models
So, If I want to use the list to pass to my view to create radio buttons I will have to do a foreach() in my make array to find all models titles and create a new array and send to the view via $this->set().
$makeArray = $this->Make->find();
foreach ($makeArray['Model'] as $model) {
$modelList[] = $model['title'];
}
$this->set('models', $models)
Is there any easier way to get that list without stressing the make Array. It will be a commom task to develops such scenarios in my application(s).
Thanks in advance for any hint!
Here's my hint: Try getting your query written in regular SQL before trying to reconstruct using the Cake library. In essence you're doing a lot of extra work that the DB can do for you.
Your approach (just for show - not good SQL):
SELECT * FROM makes, models, products WHERE make_id = 5
You're not taking into consideration the relationships (unless Cake auto-magically understands the relationships of the tables)
You're probably looking for something that joins these things together:
SELECT models.title FROM models
INNER JOIN products
ON products.model_id = models.model_id
AND products.make_id = 5
Hopefully this is a nudge in the right direction?
Judging from your comment, what you're asking for is how to get results from a certain model, where the condition is in a HABTM related model. I.e. something you'd usually do with a JOIN statement in raw SQL.
Currently that's one of the few weak points of Cake. There are different strategies to deal with that.
Have the related model B return all ids of possible candidates for Model A, then do a second query on Model A. I.e.:
$this->ModelB->find('first', array('conditions' => array('field' => $condition)));
array(
['ModelB'] => array( ... ),
['ModelA'] => array(
[0] => array(
'id' => 1
)
)
Now you have an array of all ids of ModelA that belong to ModelB that matches your conditions, which you can easily extract using Set::extract(). Basically the equivalent of SELECT model_a.id FROM model_b JOIN model_a WHERE model_b.field = xxx. Next you look for ModelA:
$this->ModelA->find('all', array('conditions' => array('id' => $model_a_ids)));
That will produce SELECT model_a.* FROM model_a WHERE id IN (1, 2, 3), which is a roundabout way of doing the JOIN statement. If you need conditions on more than one related model, repeat until you have all the ids for ModelA, SQL will use the intersection of all ids (WHERE id IN (1, 2, 3) AND id IN (3, 4, 5)).
If you only need one condition on ModelB but want to retrieve ModelA, just search for ModelB. Cake will automatically retrieve related ModelAs for you (see above). You might need to Set::extract() them again, but that might already be sufficient.
You can use the above method and combine it with the Containable behaviour to get more control over the results.
If all else fails or the above methods simply produce too much overhead, you can still write your own raw SQL with $this->Model->query(). If you stick to the Cake SQL standards (naming tables correctly with FROM model_as AS ModelA) Cake will still post-process your results correctly.
Hope this sends you in the right direction.
All your different Make->find() and Model->find() calls are completely independent of each other. Even Make->Model->find() is the same as Model->find(), Cake does not in any way remember or take into account what you have already found in other models. What you're looking for is something like:
$this->Product->find('all', array('conditions' => array('make_id' => 5)));
Check out the Set::extract() method for getting a list of model titles from the results of $this->Make->find()
The solution can be achieved with the use of the with operation in habtm array on the model.
Using with you can define the "middle" table like:
$habtm = " ...
'with' => 'MakeModel',
... ";
And internally, in the Model or Controller, you can issue conditions to the find method.
See: http://www.cricava.com/blogs/index.php?blog=6&title=modelizing_habtm_join_tables_in_cakephp_&more=1&c=1&tb=1&pb=1

Resources