I use CakePHP 2.2.2
I have 3 tables: restaurants, kitchens and kitchens_restaurants - join table for HABTM.
In Restaurant model I have:
public $hasAndBelongsToMany = array(
'Kitchen' =>
array(
'className' => 'Kitchen',
'joinTable' => 'kitchens_restaurants',
'foreignKey' => 'restaurant_id',
'associationForeignKey' => 'kitchen_id',
'unique' => true,
'conditions' => '',
'fields' => 'kitchen',
'order' => '',
'limit' => '',
'offset' => '',
),
The problem is that I have separate controller for my main page in which I need to retrieve data from this models with complex conditions.
I added
public $uses = array('Restaurant');
to my main page controller and here comes the part where I need your advices.
I need to select only those restaurants where kitchen = $id.
I've tried to add
public function index() {
$this->set('rests', $this->Restaurant->find('all', array(
'conditions' => array('Restaurant.active' => "1", 'Kitchen.id' => "1")
)));
}
and I got SQLSTATE[42S22]: Column not found: 1054 Unknown column in 'where clause' error.
Obviously I need to fetch data from "HABTM join table" but I don't know how.
TLDR:
To retrieve data that's limited based on conditions against a [ HABTM ]'s association, you need to use [ Joins ].
Explanation:
The code below follows the [ Fat Model/Skinny Controller ] mantra, so the logic is mostly all in the model, and just gets called from a controller.
Note: You don't need all those HABTM parameters if you follow the [ CakePHP conventions ] (which it appears you are).
The below code has not been tested (I wrote it on this site), but it should be pretty darn close and at least get you in the right direction.
Code:
//Restaurant model
public $hasAndBelongsToMany = array('Kitchen');
/**
* Returns an array of restaurants based on a kitchen id
* #param string $kitchenId - the id of a kitchen
* #return array of restaurants
*/
public function getRestaurantsByKitchenId($kitchenId = null) {
if(empty($kitchenId)) return false;
$restaurants = $this->find('all', array(
'joins' => array(
array('table' => 'kitchens_restaurants',
'alias' => 'KitchensRestaurant',
'type' => 'INNER',
'conditions' => array(
'KitchensRestaurant.kitchen_id' => $kitchenId,
'KitchensRestaurant.restaurant_id = Restaurant.id'
)
)
),
'group' => 'Restaurant.id'
));
return $restaurants;
}
//Any Controller
public function whateverAction($kitchenId) {
$this->loadModel('Restaurant'); //don't need this line if in RestaurantsController
$restaurants = $this->Restaurant->getRestaurantsByKitchenId($kitchenId);
$this->set('restaurants', $restaurants);
}
There is a much cleaner way than the solution provided by Dave.
First you need to set a reverse HABTM Relationship between Restaurant and Kitchen in the Kitchen Model.
Than you just make a find for the Kitchen you are interested in (id = 1) and you will get the associated restaurants, using Containable Behavior for filtering by Restaurant fields.
$this->Restaurant->Kitchen->Behaviors->attach('containable'); // Enable Containable for Kitchen Model
$this->Restaurant->Kitchen->find('first', array(
'recursive' => -1 // don't collect other data associated to Kitchen for performance purpose
'conditions' => array('Kitchen.id' => 1),
'contain' => array('Restaurant.active = 1')
));
Source
You can not need use [join], because use have setting [ HABTM ]'s association
Kitchen model hasAndBelongsToMany Restaurant model so that you can code as bellow
KitchensControllers
<?php
public function index() {
$this->Kitchen->recursive = 0;
$kitchens = $this->Kitchen->find('all', array('contain' => array('Restaurant')));
$this->set('kitchens', $kitchens);
}
?>
Good luck!
Related
I just cant get data from 2 tables in a HABTM relationship and I cant get the answer for these cakephp docs (again). Sorry for asking the question on something that should be explained in the docs.
This should be a simple but I keep getting undefined index and the relationship I set up seems in accordance with the docs. I have another post on a more complicated matter but this issue needs to be isolated .
I dont know how to get related data from the lessons table and the students table via this lessons-students HABTM table.
I want all the lessons a student does. I want the name of the student from the student table and lesson details from the lesson table which has lesson_id as the common link in this lessons-students table. Sounds simple but I cant do it.
public $hasAndBelongsToMany = array(
'Student' => array(
'className' => 'Student',
'joinTable' => 'lessons_students',
'foreignKey' => 'lesson_id',
'associationForeignKey' => 'student_id',
'unique' => 'keepExisting',
)
);
class LessonsController extends AppController {
....
$this->Lesson->recursive = -1;
$options['joins'] = array(
array('table' => 'lessons_students',
'alias' => 'LessonsStudent',
'type' => 'LEFT',
'conditions' => array(
'Lesson.id = LessonsStudent.lesson_id',
)
),
array('table' => 'students',
'alias' => 'Student',
'type' => 'LEFT',
'conditions' => array(
'LessonsStudent.student_id=Student.id',
)
),
);
$options['conditions'] = array('Student.id' => 2);
// $Item->find('all', $options);
$student=$this->set( 'student',$this->Lesson->find('all', $options));
In the view I get the error undefined index Student
<?php
// debug($student);
foreach ($student as $item2):
echo '<td>'. $item2['Lesson']['id'].'</td>';
echo '<td>'. $item2['Student']['id'].'</td>';
http://stackoverflow.com/questions/17250215/cant-get-all-user-data-from-habtm-in-cakephp
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasandbelongstomany-habtm
If you want to use model association you need to remove $this->Lesson->recursive = -1; because it disable any associations defined in model. And than you can remove $options['joins'].
If you have defined many association in your Lesson model and don't want to fetch all of it use unbindModel() or use Containable behavior
$this->Lesson->Behaviors->load('Containable');
$this->Lesson->contain('Student');
If you need to do find many times in many actions it's better to enable Containable in model itself
class Lesson extends AppModel {
public $actsAs = array('Containable');
}
so you can avoid $this->Lesson->Behaviors->load('Containable'); line;
I have a problem with nested model pagination using containable in Cake...
I've got three models: Category, CompanyCategory, Company and there association is like this
Category hasMany CompanyCategory
CompanyCategory belongsTo Category
CompanyCategory belongsTo Company
Company hasMany CompanyCategory
I get data using contain, as shown below:
$options = [
'conditions' => ['Category.slug' => $slug],
'contain' => [
'CompanyCategory.Company.CompanyAddress'
]
];
return $this->find('first', $options);
Everything works properly, until I wanted to paginate the nested model - Company. I use CategoriesController with show method, which renders the view with the selected category and associated companies (Company hasMany Categories and Category hasMany Companies).
I've tried something like this:
$this->Paginator->settings = [
'limit' => 1,
'order' => [
'id' => 'asc'
],
'contain' => array('CompanyCategory.Company')
];
$data = $this->Paginator->paginate('Category.CompanyCategory.Company', array('Category.slug LIKE' => $slug));
$this->set('category', $data);
But this didn't work for me :(
Any suggestions / help?
If you define Category has a hasAndBelongsToMany relation, it would be easy for you. Thus you created a new Model called CompanyCategory, i assume there is a reason..
You can try this-
$this->Category->bindModel(
array(
'hasAndBelongsToMany' => array(
'Company'
)
)
);
$this->Paginator->settings = array(
'conditions' => array(
'Category.slug' => $slug
),
'limit' => 1,
'order' => array(
'id' => 'asc'
),
'contain' => array(
'Company'
)
);
I assume your relation table name is categorys_companies.... If not then you need take different approach
Finnaly I resolve my problem using
$this->Paginator->settings = array(
'limit' => 2,
);
$this->Paginator->paginate('Category.CompanyCategory.Company');
I forgot to write that I use method from CategoriesController, not Companies.
But now another problem blocked me... I use TranslateBehaviour, and data which I get has no translations...
Any idea? I googled a lot, but couldn't find any satisfactory solution.
hi guys i am new to cakephp
now i'm facing a little big problem
here is the situation
i hava a shop that hasMany Catalogs which is related to many products each product has a category
i want to fetch them all just by getting the shop
i dont know how to do it
trying to use hasMAny gives me just the ids
instead is there any way to get shop inside it array of catalogs each catalog has Product's array which has one array of category
thank you
Ok, I'm on my computer now :).
In ShopModel:
class ShopModel extends AppModel {
public $hasMany = array(
'Catalog' => array(
// binding params here...
),
);
}
In CatalogModel:
class CatalogModel extends AppModel {
public $hasMany = array(
'Product' => array(
// binding params...
),
),
}
... and this goes on...
If you don't want to get excessive data in all actions, you should set in AppModel:
class AppModel extends Model {
public $recursive = -1;
}
In the controller action where you call the find function with associations:
$this->Shop->Behaviors->load('Containable');
$big_array = $this->Shop->find('all', array(
'conditions' => array(
//...
),
'contain' => array(
'Catalog' => array(
'Product' => array(
// etc, you get the point :)
),
),
),
));
It is also nice to declare the $belongsTo associations too, so you can access anything from anywhere, something like this:
$this->Catalog->Behaviors->load('Containable');
$big_array = $this->Catalog->find('all', array(
'conditions' => array(
//...
),
'contain' => array(
'Product' => array(
// ...
),
'Shop' => array(
// ...
),
),
));
EDIT
I see you have a Product->Category relation that i guess would be defined with $belongsTo. If you do a query like the one above, you will get a lot of duplicate queries (same category in many products). You can use $this->Category->find('list') but very often I find this inappropriate as it is returning only one field (I would be grateful if someone knows a way how can I get more fields with the list type). For this purpose, my workaround is making a custom function in the Category model like this:
class Category extends AppModel {
public function getSorted ($options = array()) {
$temp= $this->find('all', $options);
$output = array();
foreach ($temp[$this->alias] as $row) {
$output[$this->alias][$row['id']] = $row;
}
unset($temp);
return $output;
}
}
Then in the controller I would declare two arrays, the big one without category association and the category list one:
$this->loadModel('Category');
$this->set('categories', $this->Category->getSorted());
This way, I can get the needed category row by category id wherever i need it in the view.
Do not use CakePHP association they are not good handling complex relationships, you will later face problems with....Instead create all your join queries on the fly...I am giving you one example below:
Create one function inside Shop model and join catalog and product as shown below:
$options = array(
'conditions' => array('Product.id'=>9),
'joins' => array(
array(
'alias' => 'Catalog',
'table' => 'catalogs',
'type' => 'LEFT',
'conditions' => array(
'Catalog.product_id = Product.id',
),
),
array(
'alias' => 'Product',
'table' => 'products',
'type' => 'LEFT',
'conditions' => array(
'Shop.id = Product.shop_id',
),
)
),
'fields' => array('Product.*'),
'group' => array('Product.id')
);
$returnData = $this->find('all',$options);
This will make coding little easier and you can escape from associations!
I wrote 2 days ago to ask about andConditions and it appeared that I didn't understand the idea but the fact is that for two days now I am stuck with the next step using CakeDC:
How do I implement complex HABTM conditions in "query" methods for CakeDC search plugin?
I have Offer HABTM Feature (tables: offers, features, features_offers) and the below works just fine when used in controller:
debug($this->Offer->find('all', array('contain' => array(
'Feature' => array(
'conditions' => array(
'Feature.id in (8, 10)',
)
)
)
)
)
);
The problem comes when I want to use the same conditions in the search:
public $filterArgs = array(
array('name' => 'feature_id', 'type' => 'query', 'method' => 'findByFeatures'),
);
........
public function findByFeatures($data = array()) {
$conditions = '';
$featureID = $data['feature_id'];
if (isset($data['feature_id'])) {
$conditions = array('contain' => array(
'Feature' => array(
'conditions' => array(
'Feature.id' => $data['feature_id'],
)
)
)
);
}
return $conditions;
}
I get an error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column
'contain' in 'where clause'
which makes me think that I cannot perform this search and/or use containable behavior in searches at all.
Can someone with more experience in the field please let me know if I am missing something or point me to where exactly to find a solution for that - perhaps a section in the cookbook?
EDIT: Also tried the joins. This works perfectly fine in the controller, returning all the data I need:
$options['joins'] = array(
array('table' => 'features_offers',
'alias' => 'FeaturesOffers',
'type' => 'inner',
'conditions' => array(
'Offer.id = FeaturesOffers.offer_id'
),
array('table' => 'features',
'alias' => 'F',
'type' => 'inner',
'conditions' => array(
'F.id = FeaturesOffers.feature_id'
),
)
),
);
$options['conditions'] = array(
'feature_id in (13)' //. $data['feature_id']
);
debug($this->Offer->find('all', $options));
... and when I try to put in the search method I get the returned conditions only in the where clause of the SQL
WHERE ((joins = (Array)) AND (conditions = ('feature_id in Array')))
...resulting in error:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'joins' in 'where clause'
EDIT: Maybe I am stupid and sorry to say that but the documentation of the plugin sucks a ton.
I double, triple and quadruple checked (btw, have lost already 30 hours at least on 1 filed of the search form facepalm) and the stupid findByTags from the documentation still doesn't make any sense to me.
public function findByTags($data = array()) {
$this->Tagged->Behaviors->attach('Containable', array('autoFields' => false));
$this->Tagged->Behaviors->attach('Search.Searchable');
$query = $this->Tagged->getQuery('all', array(
'conditions' => array('Tag.name' => $data['tags']),
'fields' => array('foreign_key'),
'contain' => array('Tag')
));
return $query;
}
As I understand it
$this->Tagged
is supposed to be the name of the model of the HABTM association.
This is quite far from the standards of cakePHP though: http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#hasandbelongstomany-habtm
The way it is described here, says that you don't need another model but rather you associate Recipe with Ingredient as shown below:
class Recipe extends AppModel {
public $hasAndBelongsToMany = array(
'Ingredient' =>
array(
'className' => 'Ingredient',
'joinTable' => 'ingredients_recipes',
'foreignKey' => 'recipe_id',
'associationForeignKey' => 'ingredient_id',
'unique' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
);
}
meaning that you can access the HABTM assoc table data from Recipe without needing to define model "IngredientRecipe".
And according to cakeDC documentation the model you need is IngredientRecipe and that is not indicated as something obligatory in the cakePHP documentation. Even if this model is created the HABTM assoc doesn't work properly with it - I tried this as well.
And now I need to re-write the search functionality in my way, using only cakePHP even though I spent already 30 hours on it... so unhappy. :(
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 Using the CakeDC search plugin with associated models this 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.
This is not an issue of the plugin but how you build the associations. You need to properly join them for a search across these three tables. Check how CakePHP is fetching the data from HABTM assocs by default.
http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html#joining-tables
Suppose a Book hasAndBelongsToMany Tag association. This relation uses
a books_tags table as join table, so you need to join the books table
to the books_tags table, and this with the tags table:
$options['joins'] = array(
array('table' => 'books_tags',
'alias' => 'BooksTag',
'type' => 'inner',
'conditions' => array(
'Books.id = BooksTag.books_id'
)
),
array('table' => 'tags',
'alias' => 'Tag',
'type' => 'inner',
'conditions' => array(
'BooksTag.tag_id = Tag.id'
)
)
);
$options['conditions'] = array(
'Tag.tag' => 'Novel'
);
$books = $Book->find('all', $options); Using joins allows you to have
a maximum flexibility in how CakePHP handles associations and fetch
the data, however in most cases you can use other tools to achieve the
same results such as correctly defining associations, binding models
on the fly and using the Containable behavior. This feature should be
used with care because it could lead, in a few cases, into bad formed
SQL queries if combined with any of the former techniques described
for associating models.
Also your code is wrong somewhere.
Column not found: 1054 Unknown column 'contain' in 'where clause'
This means that $Model->contain() is somehow called. I don't see such a call in your code pasted here so it must be somewhere else. If a model method can not be found this error usually happens with the field name as column.
I want to share with everyone that the solution to working with HABTM searches with the plugin lies here: Using the CakeDC search plugin with associated models
#burzum, the documentation is far from ok man. Do you notice the use of 'type' => 'checkbox' and that it is not mentioned anywhere that it is a type?
Not to mention the total lack of grammar and the lots of typos and missing prepositions. I lost 2 days only to get a grasp of what the author had in mind and bind the words in there. No comment on that.
I am glad that after 5 days on the uphill work I made it. Thanks anyway for being helpful.
I have a model template which hasmany themes.I want to show the list of templates with count of themes.I am using this
$this->Template->bindModel(
array(
'hasMany' => array(
'TemplateTheme' => array(
'className' => 'TemplateTheme',
'fields' => 'count(TemplateTheme.id) AS themes'
)
)
), false ...
it gives me 2 templates.But it gives me all the 3 themes count in the first template whereas 2 themes belongs to template 1 and the third theme belongs to template 2
in the query it is using id IN(template_id1,template_id2)
Any idea how to do this?
You are doing a common mistake, you are counting everyrow each time since you are not using group by, you should do is group by Template.id when you do your search. Butttttttt.... has many wont do a join :( so you have to force it a littleor use something like linkable component
example
$join = array(
array('table' => 'templateThemes',
'alias' => 'TemplateTheme',
'type' => 'LEFT',
'conditions' => array(
'Template.id = TemplateTheme.Template_id',
)
)
);
$fields = array('Template.id','count(TemplateTheme.id) AS themes');
$this->Template->find('all', array('fields'=>$fields, 'joins'=>$join', $group =>array('Template.id')));
You may also do it in reverse since belongsTo does the join something like this
in your model (it is always recommended to put it static in your model unless is not a normal association)
var belongsTo = array(
'Template'=> array(
'classname' => 'Template',
'foreign_key' => 'template_id'
);
and in controller
$fields = array('Template.id','count(TemplateTheme.id) AS themes');
$this->Template->find('all', array('fields'=>$fields, $group =>array('Template.id')));
Hope this helps you, if not just comment