Cakephp recursive find including top model again - cakephp

I have this cakephp find query:
$results = $this->Instrument->find('all', array('recursive' => 2, 'conditions' => array('OR' => array('Instrument.name' => $tag_search, 'Instrument.nameEN' => $tag_search))));
The Model Instrument has and belongs to many Instrumentbox. I want to find an Instrumentbox with all it's instruments searching for one instrument name with $tag_search.
It returns the Insrtumentboxes that have that instrument properly but it does not include all the instruments agaion in Instrument. Is there a way to tell cakephp to load the "top model" again so that I get the structure
$results = array(
0 => [
'Instrument' => [
'name' => "$tag_search value",
'...' => ...,
'Instrumentbox' => [
0 => [
'...' => '...', //everything else is properly included
'Instrument' => [...] //this is NOT included, how can I include it?
]
]
]
]
The same thing happens when I search it with tags, so imagine in the structure above 'Tag' instead of 'Instrument', then the instruments are included in the Instrumentbox but the tags are not. So the "top model" is not included again in the structure. Do I have to make that by hand or can I make cakephp do it?
Thanks in advance!

You Could try to do this with Contain. Its much better than using recursive over 1 anyway. You can control exactly the results you want to get, which is good for this sort of thing, and generally better for performance.
http://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html
So, untested code:
//model
public $actsAs = array('Containable');
// controller
$this->Instrument->recursive=1;
$this->contain(array('Instrument'=> array('InstrumentBox'=> array('Instrument'))));

Related

CakePHP 3.x Find records from current user only with paginate

This seems like a thing that is probably super easy in CakePHP, but I cannot find a clear example.
I would think this works in my controller:
public function index()
{
$this->paginate = [
'where' => ['user_id' => $this->Auth->user('id')],
'contain' => ['Users']
];
$slates = $this->paginate($this->Slates);
$this->set(compact('slates'));
$this->set('_serialize', ['slates']);
}
But I always get the full collection back. I am most definitely logged in, I have two unique users, I have created records with each of them.
Change where to conditions. Couldnt find a reference in the docs.
$this->paginate = [
'conditions' => ['user_id' => $this->Auth->user('id')],
'contain' => ['Users']
];

Ignore Callbacks (beforeFind, beforeSave etc) in CakePHP 3.x

In CakePHP 2.x we have the find('all','callbacks'=>false)
What is an equivalent alternative in CakePHP3?
I have a situation where in my beforeFind callback (in my model's behavior) I'm appending a site_id to every query (for a multi-tenant app).
90% of the time I want this query appended via beforeFind, but %10 of the time I want my finds to ignore the callbacks.
I've looked at: Cakephp 3: How to ignore beforefind for specific queries? which comes close, but applying that method won't 'chain' the ignored beforeFind() callback on associated models, which I need it to do.
Updated code:
I've got two tables 'sites' and 'details' sites hasOne details, details belongs to sites. Inner Join.
In my AppController's initialize() function I've got
$tbl = TableRegistry::get( 'Sites' );
$options = [
'conditions' =>
['Sites.domain' => 'three.dev.mac',
'Sites.is_current' => 1,
'Sites.is_archive' => 0],
'contain' => 'Details', // this contain causes the problem
'ignoreCallbacks' => true
];
$tenant_details = $tbl->find('all',$options)->first();
My models beforeFind() behavior callback
public function beforeFind( Event $event, Query $query, ArrayObject $options ) {
debug($options);
if(isset($options['ignoreCallbacks']) && $options['ignoreCallbacks'] === true) {
// don't filter where clause
}
else{
// filter where clause by default
$query->where( [$this->_table->alias().'.'.'site_id'=> 7 ]);
}
return $query;
}
If I comment out the 'contain' line of my find call, the where query get's ignored as it should and my debug call returns 'ignoreCallbacks' => true' which is great.
If I leave the 'contain' line in the find() call (which is what I want) I get 2 debug outputs from beforeFind(), the first has 'ignoreCallbacks' => true', the second is empty. Apparently the second overrides the first and the query tries to append a site_id, which I don't want.
Any thoughts?
http://book.cakephp.org/3.0/en/orm/retrieving-data-and-resultsets.html
Any options that are not in this list will be passed to beforeFind listeners where they can be used to modify the query object. You can use the getOptions() method on a query object to retrieve the options used.
So just pass a custom option to your queries find() call as 2nd arg and read that option in the beforeFind() like described above.
if (isset($options['useSiteId']) && $options['useSiteId'] === true) { /*...*/ }
I've found a way to (although it seems ugly) to have the custom $options that are passed (as #burzum mentioned in his answer) in a find call to the associated table's beforeFind() method if using 'contain' in the find. Hope this helps someone who was experiencing the same issue.
$tbl = TableRegistry::get( 'Sites' );
$options = [
'conditions' =>
$conditions,
'contain' => [
'Details'=> function ($q) {
return $q->applyOptions(['ignoreCallbacks' => true]); // IMPORTANT this is required to send the 'ignoreCallbacks' option to the contained table.
}
],
'ignoreCallbacks' => true
];
$tenant_details_query = $tbl->find('all',$options)->first();

How do I load associated models and save related data in CakePHP?

I am setting up a user/group system that allows users to send requests to join a group.
I can't seem to load the associated model and add a row. It's really really really difficult to resist the urge to just use $this->query() and be done with it... but I'm trying to learn the Cake conventions and do things the right way.
In my group model's function for handling group join requests:
$this->loadModel('GroupRequest');
$this->data = array('GroupRequest' =>
array('user_id' => $uid, 'group_id' => $gid));
if($this->GroupRequest->save($this->data)){ echo 'save success'; }
else { echo 'save fail'; }
Here are the errors I get when I run this:
Warning (512): SQL Error: 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'loadModel' at line 1 [CORE/cake/libs/model/datasources/dbo_source.php, line 684]
Query: loadModel
Notice (8): Undefined property: Group::$GroupRequest [APP/models/group.php, line 124]
Fatal error: Call to a member function save() on a non-object in /home/wxworksmat/sspot3/app/models/group.php on line 124
I also tried using App::import:
App::import('Model','GroupRequest');
I don't get any SQL errors importing the Model this way, but it still doesn't work. I get the following error on the save() or create() call:
Fatal error: Call to a member function save() on a non-object in /home/wxworksmat/sspot3/app/models/group.php on line 124
You are confusing controller and model methods
$this->loadModel()
is a controller method and can only be used there.
You should always use
$this->ModelName = ClassRegistry::init('ModelName');
everywhere else
I might be wrong, and please excuse me if I'm wrong, but it looks like you don't understand the concept of the framework very well. It is difficult to answer your question without giving you a complete tutorial.
This said, everything relies on model associations. If it's done correctly, things are getting easy. You should read:
Associations: Linking Models Together
Once you have your models correctly linked, you will be able to save the primary model, as well as the related model, very easily.
Saving Related Model Data (hasOne, hasMany, belongsTo)
As I understand, you are trying to do this from inside a model?
class GroupRequest extends AppModel {
public function associate($user, $group) {
$data["GroupRequest"] = array("user_id" => $user, "group_id" => $group);
$this->save($data);
}
}
Then in your Controller (assuming group_requests_controller)
$this->GroupRequest->associate($user, $group);
If you're calling this from another controller you would loadModel first
$this->loadModel("GroupRequests");
$this->GroupRequest->associate($user, $group);
However, if you're doing all of this from within GroupRequests controller you should be able to save directly, without making a separate method for it
public function add() {
$this->GroupRequest->create();
$this->GroupRequest->save($this->data); #for < 2.0
}
Your view should be something like
<?php
echo $this->Form->create("GroupRequest");
echo $this->Form->input("user_id");
echo $this->Form->input("group_id");
echo $this->Form->end("Submit");
?>
The problem I had was that I didn't have the correct model association declarations at the top of my model.
Now I have:
group.php
var $hasMany = 'GroupRequest';
group_request.php
var $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Group' => array(
'className' => 'Group',
'foreignKey' => 'group_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
public function new_request($user, $group) {
$data["GroupRequest"] = array("user_id" => $user, "group_id" => $group, 'status' => 'pending');
if($this->save($data)){ return true;} else {return false;}
}
Now because everything is set up CORRECTLY... I can do this in my group.php model:
$this->GroupRequest->new_request($uid,$gid)
As an added bonus, because the assocations are populating properly, when I do $this->find in my group or user model, now all the related GroupRequest entries show up. Bonus data FTW.

CakePHP: How can I customize the labels for multiple checkboxes

I've done a lot of searching but havent been able to solve this yet. I have a user registration form where the user can select a number of store branches as favourites upon registration. I have this relationship set up as a HABTM.
I am able to display the store branches as multiple checkboxes, but I need to display the store name (the store branches belongs to store names) along with the branch name in the label for each checkbox. Something like:
Levi's - Canal Walk
where Canal Walk is the branch name, and Levi's is the store name (coming from a different table)
Can anyone please share some insight on how to do this?
I haven't tested this, but it might work (or help you along the right path):
$branches = $this->Branch->find('list', array(
'fields' => array('Branch.id', 'CONCAT(Store.name, " - ", Branch.name)'),
'joins' => array(
array(
'table' => 'stores',
'alias' => 'Store',
'type' => 'inner',
'foreignKey' => false,
'conditions' => array('Branch.store_id' => 'Store.id')
)
)
));
Set this in your controller, or keep things DRY by placing it in your model in a function such as findFullList()
Hope that helps!
I would do all of this in the view. If your HABTM relationship is setup correctly, a query of the Store model should work something like this:
$stores = $this->Store->findAll();
//Returns
Array([0] => Array(
'Store' => array(),
'Branch' => array(
[0] => array(),
[1] => array()...));
Then pass the $stores variable into the view, and iterate through it with a double nested loop.
foreach($stores as $store){
foreach($store['Branch'] as $branch){
//Code to output checkbox by using $branch['name']
}
}

how to create a chain select form in cakephp

My business directory application calls for 3 chained select boxes, and I'm using cakephp to build this application.
The hierarchy and order of choices for the sections is this:
1 - business group
2 - business type
3 - city (included in table customer)
The relationships are:
customer HABTM business types
business groups have many business types
business types have one business group, HABTM customers
I have searched for jquery plugins that help with this, and found one by Remy Sharp, but it doesn't have the more complex relationships I have.
http://remysharp.com/2007/09/18/auto-populate-multiple-select-boxes/
What I imagine happening is the first selection box (business groups) is pre-populated and once a selection is made, an event listener send a message that filters the second selection box, and the same for the third.
What I don't know is how to structure the search action based on the event listener.
Any advice or am I way off base?
As always, I come to the well for help.
Much appreciated.
Paul
Thanks very much Nick, I've read many of your posts I really appreciate your response.
I've followed your instructions but have run into problems. I've tried my best to resolve them but haven't figured it out.
This is what I've done so far:
1) created 'chained' actions in both the business_type and business_directory (renamed customer to business directory, which is more appropriate.)
business type chained action:
function chained($business_group_id) {
$business_types = $this->BusinessType->find('list', array(
'conditions' => array( 'BusinessType.business_group_id' => $business_group_id)
));
$this->set('business_types', $business_types);
}
business directory chained action:
function chained($business_type_id) {
$business_directories = $this->BusinessDirectory->bindModel(array( 'hasOne' => array('business_directories_business_types' )));
$business_directories = $this->BusinessDirectory->find('all', array(
'fields' => array( ' BusinessDirectory.city'),
'conditions' => array( 'business_directories_business_types.business_type_id' => $business_type_id)
));
$this->set('business_directories', $business_directories);
}
I did find that with a HABTM relationship, using find 'list' didn't create the join query, whereas find 'all' did.
2) I then created a search action in the business directory and corresponding view.
For the business groups I created a getList action to populate the option list in the search form:
function getList() {
return $this->BusinessGroup->find('list');
}
In the search view, I've added the javascript for the chain select:
<script type="text/javascript">
<!--
$(function () {
var group = $('#businessGoup');
var type = $('#businessType');
var city = $('#businessDirectoryCity');
type.selectChain({
target: city,
url: '../business_directories/chained/'+$(this).val(),
data: { ajax: true, anotherval: "anotherAction" }
});
group.selectChain({
target: type,
url: '../business_types/chained/'+$(this).val()
}).trigger('change');
});
//-->
</script>
And the form:
create('business_directories', array('action'=>'/search_results')); ?>
input('business_group_id',
array( 'type' => 'select',
'id' => 'businessGoup',
'empty' => '-- Select Business Group --',
'multiple' => true,
'options' => $this->requestAction('/business_groups/getList' ),
'label' => 'Business Group'));
?>
input('business_type.id',
array( 'type' => 'select',
'id' => 'businessType',
'empty' => '-- Select Business Type --',
'multiple' => true,
'options' => 'none selected',
'label' => 'Business Type'));
?>
input('business_directories.id',
array( 'type' => 'select',
'id' => 'businessDirectoryCity',
'empty' => '-- Select City --',
'multiple' => true,
'options' => 'options',
'label' => 'City'));
?>
end('Search'); ?>
When I test the business type chain function, /business_types/chained/1, everything works.
But when I test the search view, I get a javascript alert error. Then when I check firebug, I get the following two errors:
Warning (2): Missing argument 1 for BusinessTypesController::chained() [APP\controllers\business_types_controller.php, line 71]
Notice (8): Undefined variable: business_group_id [APP\controllers\business_types_controller.php, line 73]
Any additional help with this is very much appreciated.
Thanks, Paul
What you need is to have 2 actions in the controllers (business_type and customer).
each action should look like this. In that case for the business type
function chained($parent_id){
$business_types = $this->BusinessType->find('list', array('conditions'=>'BusinessType.business_group_id'=>$parent_id));
$this->set('business_types', $business_types);
}
of course you need also view for that action which will format the values in the proper format for the chained select.
For Business group you need to show all values directly so no ajax is needed.
The Customer controller's action is similar, but you need to select cities of all related customers.
Then with the chained select you need to set the proper elements and set the proper actions which need to be called.
i.e.:
$('#id-of-the-business-group').selectChain({
target: $('#id-of-the-business-type-field'),
url: '/business_types/chained/'+$(this).val()
});

Resources