CakePHP - Correct way to do hasMany through - php

I have 3 models
Categories:
class Category extends AppModel {
public $belongsTo = array(
'Parent' => array(
'className' => 'Category',
'foreignKey' => 'parent_id'
),
);
public $hasMany = array(
'Children' => array(
'className' => 'Category',
'foreignKey' => 'parent_id'
),
'UserCategoryMeta'
);
}
Users:
class User extends AppModel {
public $hasMany = array(
'UserCategoryMeta' => array(
'className' => 'UserCategoryMeta',
'foreignKey' => 'user_id',
),
);
}
UserCategoryMeta:
class UserCategoryMeta extends AppModel
{
public $belongsTo = array(
'User', 'Category'
);
}
What I need to do is have each user be able to choose many categories and for each of those associations I need the user to set search terms which is just 1 field in the DB.
So the UserCategoryMeta table looks like this:
id | user_id | category_id | search_terms
I've found a way which might work but it seems very hacky.
In the usercontroller I have:
$Categories = $this->User->Category->find('list');
Then in the add view I have the checkboxes:
echo $this->Form->input('Category.Category',array(
'type' => 'select',
'multiple' =>'checkbox',
'options' => $Categories,
));
Then the only way I could get each of those checkboxes to have a search terms input next to them is to do this in the add view:
foreach ($Categories as $key => $category){
echo '<input type="text" id="Category'.$key.'SearchTerms" name="data[Category][search_terms]['.$key.']"><br/>';
}
This produces what I want but obviously since I'm just creating random inputs when the form get's submitted it get's black holed. I have managed to get passed this but I know I'm doing it the wrong way and hopefully someone can help me do it the right way.
Also then once I have this data array in the controller im not sure how to save it to the database correctly.
Thanks in advance for any help!

Just do what you're doing, but in the repeat, use $this->Form->input instead of just manually writing the HTML.

Related

CakePHP recursive not working with conditions

I have a problem with querying associated data from a Model in CakePHP. I wrote an example to show the behavior:
TestController.php:
class TestController extends AppController
{
public $uses = array(
'User',
'Upload',
'Detail'
);
public function test(){
$result = $this->Upload->find('all', array(
'recursive' => 2,
'conditions' => array('Detail.id' => 1)
));
print_r($result);
}
}
Upload.php:
class Upload extends AppModel {
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
}
Detail.php:
class Detail extends AppModel {
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id'
)
);
}
User.php:
class User extends AppModel {
public $hasOne = 'Detail';
public $hasMany = array(
'Upload' => array(
'className' => 'Upload',
'foreignKey' => 'user_id',
)
);
}
When I remove the condition I get back an array with Details included. But with the condition I get the following error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Detail.id' in 'where clause'
Looking at the SQL Queries it seems like he is not joining the tables correctly when I add the condition. Without the condition he is joining all three tables.
Is this a bug in CakePHP or am I doing anything wrong?
No, it is not a bug with CakePHP. It's simply the way it's designed, using conditions during a find on associated models will often create an invalid query. You should be using containable behavior or manually joining to use conditions on associated models.
Also, I suspect that you will not get the results you are looking for doing this way anyways. CakePHP by default uses left joins. Therefore, your results will not be limited by those associated with the desired Detail ID, but rather, it will get all uploads, all users associated with those uploads, and then only those details associated with those users that have the correct ID. The simplest way then to get what you're probably looking for is to do the query from the opposite direction:
$result = $this->Detail->find('all', array(
'recursive' => 2,
'conditions' => array('Detail.id' => 1)
));
EDIT: If you do want to do left joins, then make your query this way:
$result = $this->Upload->find('all', array(
'contain' => array('User' => array('Detail' => array('conditions' => array('Detail.id' => 1))),
));

CakePHP - Third Part Assignments

Sorry if the Title misses what i'm aiming at, but had no clue how to name this. Here's my Quest:
I have a single Cart, holding multiple Items (1:n) which are belonging to prices (1:1).
The Relations between Items and Prices are working as well:
class Item extends AppModel {
public $hasOne = 'Price';
}
class Price extends AppModel {
public $belongsTo = array(
'Item' => array(
'className' => 'Item',
'foreignKey' => 'id'
)
);
}
But now i make a select (find) of a cart and want those items included, which is also working as well:
class CartItem extends AppModel {
public $useTable = 'cart_items';
public $belongsTo = array('Item', 'Address');
}
But what i really need is to get those prices of each item, which is not working ($result in afterFind()-Callback in Item-Model does not include the assigned prices & afterFind()-Callback in Price-Model is not called when finding a cart)..
what am i missing here?
/EDIT: Recent Changes:
class AppModel extends Model {
var $actsAs = array('Containable');
}
class CartsController extends AppController {
public function getCart() {
$cart = ClassRegistry::init('CartItem')->find('all', array(
'contain' => array(
'Item' => array(
'Price'
),
'Address'
),
'conditions' => array(
'id_cart' => $cart['Cart']['id']
)
));
}
The above changes cause that i'll get a Price into a found Item but only into the last one thats found.
You haven't shown your actual find(), but I suspect you're not setting an appropriate 'recursive' param or are not using 'contain'.
I prefer using the containable behaviour enabled from AppModel:
var $actsAs = array('Containable');
Then you can do something like:
$cartItem = $this->CartItem->find(
'first',
array(
'contain' => array(
'Item' => array(
'Price'
),
'Address'
),
'conditions' => array('CartItem.id' => 123)
)
);

CakePHP 2.X: HABTM with Bookmarks / Tags, saving from same form

I'm trying to build a simple bookmarking site as a project to learn some CakePHP, but am having significant trouble figuring out how to do HABTM (or whatever I might need) for Bookmarks/Tags.
I have a simple form which has the fields 'Title', 'Url', 'Tags', which is used to create a new bookmark.
My tables are set up as follows:
bookmarks: id (primary), uid, title, url, private (boolean), time
tags: id (primary), tag
bookmarks_tags: id (primary), bookmark_id, tag_id
There's also a users table.
My bookmark model (Bookmark.php) is pretty simple so far, looks like:
class Bookmark extends AppModel {
public $name = "Bookmark";
public $displayField = 'name';
public $hasAndBelongsToMany = array(
'Tag' => array(
'className' => 'Tag',
'joinTable' => 'bookmarks_tags',
'foreignKey' => 'bookmark_id',
'associationForeignKey' => 'tag_id',
'unique' => 'keepExisting',
)
);
And then some validation.
The tag model looks like:
class Tag extends AppModel {
public $name='Tag';
public $displayField = 'name';
public $hasAndBelongsToMany = array(
'Bookmark' => array(
'className' => 'Bookmark',
'joinTable' => 'bookmarks_tags',
'foreignKey' => 'tag_id',
'associationForeignKey' => 'bookmark_id',
'unique' => 'keepExisting',
)
);
The form on the frontend so far looks like this:
<div id="addBookmark" class="card">
<div id="addBookmarkTopSpan" class="topSpan">add new bookmark</div>
<?php echo $this->Form->create(null, array('url' => array('controller' => 'bookmarks', 'action' => 'add'))); ?>
<fieldset>
<?php echo $this->Form->input('Bookmark.title');
echo $this->Form->input('Bookmark.url');
echo $this->Form->input('Bookmark.private', array('type' => 'checkbox'));
echo $this->Form->input('Bookmark.uid', array('type' => 'hidden', 'value' => $user['User']['id']));
echo $this->Form->input('Bookmark.Tag');
?>
</fieldset>
<?php echo $this->Form->end(__('Submit')); ?>
</div>
As I said, I'm pretty new to Cake so I'm not sure what's going on here..currently if I enter data into that form and hit submit the bookmark gets created just fine but nothing at all happens with the tag. How do I save the data into the associated join table and the Tags table?
In your view change
echo $this->Form->input('Bookmark.Tag');
to
echo $this->Form->input('Tag.id');
or
echo $this->Form->input('Tag.0.id');
echo $this->Form->input('Tag.1.id');
echo $this->Form->input('Tag.2.id');
...
Just couple of side notes about your code ...
Since you're following CakePHP conventions, you don't need to specify the parameters for the relationship;
public $hasAndBelongsToMany = array('Tag');
and
public $hasAndBelongsToMany = array('Bookmark');
is enough.
Rather than setting the user ID in the form, which could be modified, set it in your controller:
BookmarksController.php
public function add() {
if ($this->request->is('post')) {
$this->request->data['Bookmark']['uid'] = $this->Auth->user('id');
if ($this->Note->save($this->request->data)) {
...

cakephp HABTM same model

I have a site develop in cakephp 2.0
I want to make a HABTM relation to the same model: A product can has more products.
I thinked to do in this mode into my model:
class Product extends AppModel {
public $name = 'Product';
public $useTable = 'products';
public $belongsTo = 'User';
public $actsAs = array('Containable');
public $hasAndBelongsToMany = array(
'Ingredient' => array(
'className' => 'Product',
'joinTable' => 'ingredients_products',
'foreignKey' => 'product_id',
'associationForeignKey' => 'ingredient_id',
'unique' => true
)
);
}
Is correct my relation? But have I to insert into my table products the field product_id and ingredient_id?
And how can I save my data with a form? I know how to save data with HABTM but I never done an HABTM to the same table.
Your relation is fine. What you have written will create a Product Model that can have any number of Ingredients and allows an Ingredient to belong to any number of Products.
When saving, you must simply treat the Ingredient as if it were another Model. The CakePHP example on saving HABTM works just as well as for associating the same model as with 2 different models: http://book.cakephp.org/2.0/en/models/saving-your-data.html.
So, if you're saving multiple Ingredients to a Product, your Array structure will look like this:
Array(
[0] => Array(
Product => Array(
id => 1
),
Ingredient => Array(
id => 18
)
),
1 => Array(
Product => Array(
id => 1
),
Ingredient => Array(
id => 23
)
)
// ...
)
It is up to you how you capture this in a form, but the form example used in the link provided above should manage this properly.

cakePHP bind hasmany through relationship

Ok this is kind of hard to explain but I'll try my best.
I have 3 tables
companies products product_availabilities
--------- -------- ----------------------
id id id
name name company_id
product_id
buys (tinyint)
sells (tinyint)
And their models
class Company extends AppModel
{
public $name = 'Company';
public $hasMany = array(
'ProductAvailability'
);
class Product extends AppModel
{
public $name = 'Product';
public $hasMany = array(
'ProductAvailability'
);
class ProductAvailability extends AppModel
{
public $name = 'ProductAvailability';
public $belongsTo = array(
'Company',
'Product'
);
}
What I want to do is when I create a company, I want to be able to select products that the company buys or sells. I've seen an example of a hasMany through relationship in the book (http://book.cakephp.org/1.3/view/1650/hasMany-through-The-Join-Model) but they are creating the form from the "join table" controller. Is it possible to bind the productAvailability model to my company model to be able to select the products while creating the company?
Edit : Here is how I've done it. I know it is not optimal as there is a lot of looping involved but it works.
Company controller :
$products = $this->Company->ProductAvailability->Product->find('list', array('fields' => array('Product.id', 'Product.label')));
$this->set('products', $products);
if($this->request->is('post')){
if($this->Company->save($this->request->data)){
foreach($products as $product)
{
$tmpArray = array(
'company_id' => $this->Company->id,
'product_id' => $product['Product']['id']
);
foreach($this->request->data('BuyProducts.product_id') as $buyProduct)
{
if($buyProduct == $product['Product']['id'])
$tmpArray['buys'] = 1;
}
foreach($this->request->data('SellProducts.product_id') as $sellProduct)
{
if($sellProduct == $product['Product']['id'])
$tmpArray['sells'] = 1;
}
if(count($tmpArray) > 2)
{
$this->Company->ProductAvailability->create();
$this->Company->ProductAvailability->set($tmpArray);
$this->Company->ProductAvailability->save();
}
}
$this->Session->setFlash('Yay', 'success');
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash('Nay', 'error');
}
}
Company add form :
<?php echo $this->Form->create('Company'); ?>
<?php echo $this->Form->input('name', array( 'div' => 'full-form')); ?>
<?php echo $this->Form->input('BuyProducts.product_id', array('multiple' => 'checkbox', 'options' => $products, 'div' => 'full-form', 'label' => false)); ?>
<?php echo $this->Form->input('SellProducts.product_id', array('multiple' => 'checkbox', 'options' => $products, 'div' => 'full-form', 'label' => false)); ?>
<?php echo $this->Form->end(array('label' => __('Save'), 'div' => 'center', 'class' => 'bouton-vert')); ?>
You have two options. Either let cakePHP do some magic with the hasAndBelongsToMany relationship or doing it manually which is necessary if you add attributes to the join table
1. CakePHP HABTM
Using the capabilities of CakePHP and making a straight forward solution I would make these changes:
Model
If one company has more than one product, and the products belong to many companies. It is a hasAndBelongsToMany relationship between Company<->Product
// company.php
...
var $hasAndBelongsToMany = array(
'Product' => array(
'className' => 'Product',
'joinTable' => 'companies_products',
'foreignKey' => 'company_id',
'associationForeignKey' => 'product_id',
'unique' => true,
)
);
...
// similar in product.php
Add the required table 'companies_products' in the database.
Controller
Then in the add function from the Company Controller there should be something like:
$products = $this->Company->Product->find('list');
$this->set(compact('products'));
View
Finally insert the products in the add.ctp, the select should allow multiple selections and let cakePHP do some magic, like this:
echo $this->Form->input('products', array(
'label' => 'Products to buy (Ctr+multiple choice)'
'type' => 'select',
'multiple' => true,
));
2. Manually
When the HABTM becomes more 'exotic' and includes some attributes like in your case 'buy' or 'sell' you need to do the manual way. This is in the Product Controller setting manually the fields before inserting them in the database. Something like:
foreach($availableProducts as $availableProduct ){
$this->Product->ProductAvailabilities->create();
$this->Product->ProductAvailabilities->set( array(
'company_id' => $this->Product->id,
'product_id' => $availableProduct['Product']['id'],
'buys' => $availableProduct['Product']['buy'],
'sells' => $availableProduct['Product']['sell']
// or however you send it to the controller
));
$this->Product->ProductAvailabilities->save();
}
Let's hope this helps you ...
you are planning a habtm-relationship (m:n) with the possibility to have extra fields in the join table. Even though this can be done with regular habtm I prefer the way you choose and implement the m:n as 1:n:1, which is simply the same and gives you more options when saving your data.
Here is a similar question and answer
As for your question: You can collect the the data from your products table like this:
$this->Company->ProductAvailability->Product->find('all', $params);
Also you might want to have a look at the containable-behaviour which is very useful for this use case:
$params['conditions'] = array(
'Company.id' => $id
);
$params['contain'] => array(
'ProductAvailability' => array(
'conditions' =>array(
'buys' => 1
),
'Product' => array(
'order' => array(
'name' => 'ASC'
)
)
)
);
$this->Company->find('all', $params);

Categories