CakePHP3 - creating multiple associations between same two records - mysql

I have a many-to-many relationship between Invoices and Items. An Item can appear multiple times in the same Invoice, but with different amounts (e.g. when the same service is used several times during one month).
I was hoping I could create this association by including the following in the 'items' element of the invoice when saving (see https://book.cakephp.org/3/en/orm/saving-data.html#saving-belongstomany-associations):
Array
(
[0] => Array
(
[id] => 1
[_joinData] => Array
(
[amount] => 5338.29
)
)
[1] => Array
(
[id] => 1
[_joinData] => Array
(
[amount] => 5988.53
)
)
[2] => Array
(
[id] => 1
[_joinData] => Array
(
[amount] => 6023.40
)
)
)
In the example above, the result I'm hoping for is that three rows are created in the join table invoices_items. The invoice is saved correctly, but only one row is created in the join table.
One both associations I tried setting saveStrategy to append (I wasn't sure what this does), but this didn't help:
$this->belongsToMany('Invoices', [
'saveStrategy' => 'append'
]);
Is it possible to achieve this behaviour out of the box, or do I need to create something more custom, like a new model to specifically keep track of these relationships? Please let me know if more code would help to clarify what I'm trying to achieve.
Thanks!

The answer seems to be that no, this type of mass assignment isn't possible.
The solution I arrived at is to loop through each item I want to associate with the freshly saved invoice and call link() for each. I don't know what the performance hit is for this but for my purposes it works as this operation in my case happens relatively rarely.
// Build an array with an element per item
...
$itemsJoinData[] = [
'item' => $item,
'_joinData' => [
'amount' => $amount
]
];
...
Once the invoice is successfully saved I attach the items with their respective amount.
// Join up the invoice with the items
foreach($itemsJoinData as $itemToJoin) {
$itemToJoin['item']->_joinData = new Entity(['amount' => $itemToJoin['_joinData']['amount']], ['markNew' => true]);
$this->Invoices->Items->link( $invoice, [$itemToJoin['item']] );
}

Related

Yii2 GridView display only related records

I have this database structure:
I need the user to get only those reservations in ReservationsSeachModel that are in the warehouse belonging to the user. Model user_has_warehouse has a function getUserHasWarehouse($user_id, $warehouse_id) to check for relations. I am able to get the relations when viewing single, deleting and updating records in controllers. But I cannot make the SearchModel to return reservations that belong to current user.
I cannot depend on Reservation->user_id, because that value is not mandatory and reservations can be inserted publicly with no user_id. How to make the SearchModel work properly so it only shows reservations belonging to the warehouse where user has the warehouse?
ReservationsSearchModel:
$warehouse_id = User_Has_Warehouse::find()->select(['warehouse_id'])->where(['user_id' => Yii::$app->user->id]);
$freezer_type_id = Freezer_type::find()->select(['id'])->where(['warehouse_id' => $warehouse_id]);
$reservation_id = Reservation_freezer_type::find()->select(['reservation_id'])->where(['freezer_type_id'=>$freezer_type_id]);
...
$query->andFilterWhere([
'id' => $reservation_id,
'customer_id' => $this->customer_id,
'user_id' => $this->user_id,
]);
...
But the following did not work for me. What am I doing wrong?
Managed to solve it.
$h = User_Has_Warehouse::find()->select(['warehouse_id'])->where(['user_id' => Yii::$app->user->id]);
$rt = Freezer_type::find()->select(['id'])->where(['warehouse_id' => $h]);
$res = Reservation_freezer_type::find()->select(['reservation_id'])->where(['freezer_type_id' => $rt]);
// grid filtering conditions
$query->andFilterWhere(['id' => $res]);

Join DB tables with hasMany association - store in array

In a cakePHP application I am building, a profile can have multiple locations; the tables are called "profiles" and "locations" and in the model classes I have defined a HasMany relationship. Now I want the user to be able to search profiles based on their locations. After reading some questions here and the CakePHP Cookbook, I have decided I need to use SQL joins (in reality more tables are involved, and the result of a search should be based on conditions concerning different tables).
I have written the following function inside my Profile model:
public function findProfiles($long, $lat){
$options['joins'] = array(
array('table' => 'locations',
'alias' => 'Location',
'type' => 'Inner',
'conditions' => array('Location.profile_id = Profile.id'))
);
$options['order'] = array('Location.lng ASC'); //this is just as trial
return $this->find('all',$options);
}
The code works, but I get a copy of a profile for each location it possesses. That is, if a profile possesses 5 positions, I get five instances of that profile (each instance containing all five positions!)
How can I achieve this?
[edit]
eg. let's assume I only have one profile, with two positions. I get:
result[0][Profile]
[Position][0]
[1]
[1][Profile]
[Position][0]
[1]
Where the data in result[0] and result[1] is identical.
The problem happens because of the type of join used. With inner join you'll get this return with your query
profile_id location_id
---------------------------
1 2
1 3
And cake understands that as two records of Profile, so you get repeated Profiles with the same info.
If this were all the extent of your problem, I'd say "go with Containable behaviour and forget joins", but since you said there are more tables involved, maybe the type of join can't be changed. So to get the unique Profile without repetitions, you'll have to GROUP BY the query to get
profile_id location_id
---------------------------
1 2 & 3
with a code similar to this
$options['joins'] = array(
array('table' => 'locations',
'alias' => 'Location',
'type' => 'Inner',
'conditions' => array('Location.profile_id = Profile.id')),
'group' => 'Profile.id'
);
and you'll get rid of repetitions. For future problems like this, is best to first check the actual query that gets send to the DB, check yourself if the result that the DB gives you is what you want, and if not, see what you can do in cake to change it.

MySQL join across two tables with serialized data, recursion?

I'm having some major issues trying to grab all the data I need for a SQL query. I'm still new with queries, so I will try to describe this as best as possible.
I am trying to do a cross query with the Wordpress plugin NextGen Gallery. Basically there are two tables nggalbum and nggallery. What I'm attempting to do is create a nested list of all albums and galleries.
The data in nggalbum contains columns id, name, slug, previewpic, albumdesc, sortorder, and pageid. The only values that I'm interested in are id, name, slug, and sortorder. The value for sortorder is serialized data that has the relationship data of this entry and all other album and gallery entries. For example: a:2:{i:0;s:2:"a2";i:1;s:2:"a6";} This basically means the current query item has two sub-albums (their corresponding id prefixed with an "a"): a2 and a6. If it has galleries, the number doesn't have an a prefix and is the ngggallery gid (will cover that in a second).
I use this to grab all the data from the nggalbum table:
$albums = $wpdb->get_results("SELECT * FROM $wpdb->nggalbum" , OBJECT_K );
foreach ($albums as $key => $value) {
$albums[$key]->id = 'a' . $key;
$albums[$key]->galleries = empty ($albums[$key]->sortorder) ? array() : (array) unserialize($albums[$key]->sortorder) ;
$albums[$key]->name = stripslashes( $albums[$key]->name );
$albums[$key]->albumdesc = stripslashes( $albums[$key]->albumdesc );
}
Sample data:
Array
(
[1] => stdClass Object
(
[id] => a1
[name] => Image Gallery
[slug] => image-gallery
[previewpic] => 0
[albumdesc] =>
[sortorder] => a:2:{i:0;s:2:"a2";i:1;s:2:"a6";}
[pageid] => 0
[galleries] => Array
(
[0] => a2
[1] => a6
)
)
[2] => stdClass Object
(
[id] => a2
[name] => ALBUM 1 - High res
[slug] => album-1-high-res
[previewpic] => 0
[albumdesc] =>
[sortorder] => a:2:{i:0;s:1:"2";i:1;s:1:"3";}
[pageid] => 0
[galleries] => Array
(
[0] => 2
[1] => 3
)
)
I add an a prefix to all of these id's because they are albums and I figured this may help later. Since I'm not sure what I'm doing, this may not be the case.
nggallery contains columns gid, name, slug, path, title, galdesc, pageid, previewpic, and author. The only relevant columns are gid, name, slug, path, and title.
I use this to grab all the data from the nggallery table:
$galleries = $wpdb->get_results( "SELECT SQL_CALC_FOUND_ROWS * FROM $wpdb->nggallery", OBJECT_K );
foreach ($galleries as $key => $value) {
$galleriesID[] = $key;
$galleries[$key]->counter = 0;
$galleries[$key]->title = stripslashes($galleries[$key]->title);
$galleries[$key]->galdesc = stripslashes($galleries[$key]->galdesc);
$galleries[$key]->abspath = WINABSPATH . $galleries[$key]->path;
}
Sample data:
Array
(
[2] => stdClass Object
(
[gid] => 2
[name] => new_collection
[slug] => new-collection
[path] => wp-content/gallery/new_collection
[title] => NEW COLLECTION
[galdesc] =>
[pageid] => 0
[previewpic] => 8
[author] => 1
[counter] => 0
[abspath] => /Applications/MAMP/htdocs/igal/wp-content/gallery/new_collection
)
[3] => stdClass Object
(
[gid] => 3
[name] => cha-collection
[slug] => cha-collection
[path] => wp-content/gallery/cha-collection
[title] => CHA COLLECTION
[galdesc] =>
[pageid] => 0
[previewpic] => 15
[author] => 1
[counter] => 0
[abspath] => /Applications/MAMP/htdocs/igal/wp-content/gallery/cha-collection
)
Now this is where I get stuck. I would really love to be able to write some code to parse through each album's galleries array and associate it with the corresponding albums and/or galleries from nggallery.
Eventually I would love to achieve a nested list of albums/galleries, such as:
(a1) [link] Title
(a2) [link] Title
1 [link] Title
2 [link] Title
3 [link] Title
(a3) [link] Title
1 [link] Title
[...]
I'm not entirely sure how to even start going about this. I tried looping some things through some foreach statements and have been largely unsuccessful. I would search google for this but I have no idea what this technique is even called.
I would REALLY like to understand how to do something like this, so if you could please shed any light upon this technique, I would be greatly indebted. Links to similar tutorials or just basic concepts would be very beneficial to me. I don't expect anyone to do all the code for me, but any step in the right direction would be greatly appreciated ( and if you want to do the code, with some steps, I won't argue of course ;) ).
Thanks so much ahead of time!
Tre
I don't quite understand how albums and galleries are related to each other and what you want in the nested list. However, it looks to me like the problem is in the "sortorder" column which is doing too much. I suspect you are trying to express a many-to-many relationship between your tables in which case it might be cleaner to have a separate table that expresses that relationship. Once you've done that I think you can more easily query which nalbums and ngalleries relate to gallery. Does that help much?
Edit:
OK so I think I understand now. You are trying to create a hierarchy of albums and you want to print the whole hierarchy of albums including all the galleries that each album has. So, using your existing design I think you could do something like this:
PrintGallery( string galleryID )
{
//1 do a query that selects the gallery using the id (use a where clause)
//2 print gallery details like the name etc whatever you want to
}
PrintAlbum ( string albumID )
{
//1 do a query that selects the Album using the id (use a where clause)
//2 print Album details (name icon etc) but not the gallery array details.
if(galleries array length > 0 )
/* in case there are no galleries we don't want a hanging <li> with nothing in it */
echo "<ul>" /* This will ensure the items are nested properly */
foreach item currentItemID in galleries array
echo "<li>"
if (currentItemID is an Album)
PrintAlbum(currentItemID) /* recurse */
else /* assume it's a gallery are in the array */
PrintGallery(currentItemID)
echo "</ul>" /* end this level */
}

Need some help with the conditions in CakePHP!

I have three models linked in this manner: Item->Order->Payment
Order hasMany Item
Order hasOne Payment
Now, I am paginating Items and want to add a condition in it to find only items of that order which has payment of a particular id. I hope that makes sense :P
I added the condition as:
array('Payment.id'=>$id)
but it doesn't work. Obviously cause Payment is not associated with Item.
So, how can I go about this?
I am new to cakephp, maybe I am completily wrong but as I understand it you can use other models in your controller with the $uses variable. First make a query on payment model to get your order id, than you can use this id to find the corresponding items.
$uses=array('Item','Order','Payment');
$order_id=$this->Payment->find('first',array('fields'=>'order_id','conditions'=>array('id'=>$payment_id)));
$items=$this->Item->find('all',array('conditions'=>array('order_id'=>$order_id)));
I hope it help.
Why don't you add a condition:
array('Order.payment_id'=>$id)
I think this should work.
If you specify that you want two levels of recursion this should work. Im assuming you have
in Payment.php
//recursion level 1
var $belongsTo = array('Order');
in Order.php
//recursion level 2
var $hasMany = array('Items')
You are right that for paginate to work you must query the model you wish to page and sort the lists by.
in PaymentController.php
//Query two levels deep, so the $payment['Order']['Item'][0-n] will be present
var $paginate = array('recursive' => 2);
Note this method does generate another query for each row to retrieve items.
Make sure the debug level in app/config/core.php is set to 2 to see the database calls.
1) You can use Containable behaviour, in which case you need to put this in your Item model:
var $actsAs = array('Containable');
and this into your Items controller:
$items = $this->Item->find('all'
, array (
'contain' => array('Order' => array('Payment'))
, 'conditions' => array('Payment.id' => $paymentId)
)
)
However I suspect that that will do a left join onto the Payments table (as its a hasMany relationship). So you won't filter Items in any way.
2) If you can't get contains to work then I often use explict joins (read this bakery article by nate on joins) in my find queries. So in your Items controller you'd have:
$items = $this->Item->find('all'
, array (
, 'joins' => array(
array(
'table' => 'payments'
, 'alias' => 'Payment'
, 'type' => 'INNER'
, 'conditions' => array(
'Option.id = Payment.option_id'
)
)
)
, 'conditions' => array('Payment.id' => $paymentId)
)
)
You may also need to specify the join onto the options table.

cakephp COUNT items per month in a year

How do you use cakephp to count, for example the number of posts, made every month in a year?
Preferably using Model->find('count') and get the data in an array.
I just did something similar, using only CakePHP (no direct queries). It works in CakePHP 2, haven't tested in 1.x.
The code for your example would be something like this:
$params = array(
'recursive' => -1,
'fields' => array('id', 'MONTH(created)')
'group' => array('YEAR(created)', 'MONTH(created)')
);
$numberOfPosts = $this->Model->find('count', $params);
This comes close
Query
$data = $this->Post->query("SELECT COUNT(id),MONTH(created) FROM posts GROUP BY YEAR(created), MONTH(created);");
Return
Array
(
[0] => Array
(
[0] => Array
(
[COUNT(id)] => 1
[MONTH(created)] => 3
)
)
[1] => Array
(
[0] => Array
(
[COUNT(id)] => 2
[MONTH(created)] => 4
)
)
)
When using cake, I prefer to stay as close to the framework as possible. This means that I try to avoid writing queries directly in the controllers because this results in the model code being everywhere. Therefore I recommend one of two solutions
1: (and what I do with more complicated stuff): Create a view for the calculation that you want to do and create a model to match.
2: Use a query as mentioned before, but put it in the model class, not the application class.