CakePHP - access to associated model in beforeSave - cakephp

In my app Quotes belongTo a product, which in turn belongs to a material. As I can't get the product model afterFind array to include the material when it is accessed from the Quote model I have associated the Quote directly with a material.
The problem I'm having now is that the material_id for the quote needs to be automatically saved based on the product which is selected for the quote
i.e. pulling the value of Product.material_id from the selected product and saving it to the Quote.material_id field automatically before the Quote has been saved to the database.
I'm quite new to cakePHP. Does anyone know how this can be done?
EDIT:
Here is an example to help explain. In my Quote model i can have:
public function beforeSave($options) {
$this->data['Quote']['material_id'] = 4;
return true;
}
but i need to do something more like this which doesn't work:
public function beforeSave($options) {
$this->data['Quote']['material_id'] = $this->Product['material_id'];
return true;
}

I'm shocked this hasn't been properly answered yet....
Oldskool's response is semi-correct, but not entirely right. The use of "$this->Quote" is incorrect, as the beforeSave function itself resides in the Quote class. I'll explain using an example.
-> We have a model Subscription which belongsTo a SubscriptionsPlan
-> Model SubscriptionsPlan hasMany Suscriptions
To access the SubscriptionsPlan data in a beforeSave function in the Subscription model, you would do the following:
public function beforeSave($options = array()){
$options = array(
'conditions' => array(
'SubscriptionsPlan.subscriptions_plan_id' => $this->data[$this->alias]['subscriptions_plan_id']
)
);
$plan = $this->SubscriptionsPlan->find('first', $options);
//REST OF BEFORE SAVE CODE GOES HERE
return true;
}

It should probably work by using a find instead.
public function beforeSave($options) {
// Assuming your Product model is associated with your Quote model
$product = $this->Quote->Product->find('first', array(
'conditions' => array(
'Product.material_id' => $this->data['Quote']['material_id']
)
));
$this->data['Quote']['material_id'] = $product['material_id'];
return true;
}

Related

CakePHP 3: Properly writing functions for the Entity Model

I have a blog model, and I suspect I'm not writing my code correctly to best take advantage of CakePHP's MVC structure.
Here are some snippets from my Posts controller.
public function view() {
$posts = $this->Posts->find('all')->contain([
'Comments' => ['Users'],
'Users'
]);
$this->set(compact('posts'));
}
public function index() {
$post = $this->Posts->find('all')->contain([
'Users'
])->limit(20);
$this->set(compact('post'));
}
This is a snippet from the index.ctp template
foreach ( $posts as $post ) {
<div class="post">
<h1 class="title>
<?= h($post->title) ?>
<small><?php echo $post->getCommentCount(); ?> comments</small>
</h1>
<div class="body">
<?= h($post->body) ?>
</div>
</div>
}
In my Post Entity, I have the following function
public function getCommentCount(){
$count = 0;
foreach ($this->comments as $comment){
if ( $comment->isPublished() ) {
$count += 1;
}
}
return $count;
}
My problem is I need to call the getCommentCount function from the index.ctp where the $posts object has no comment children (which the function uses).
Am I misunderstanding how the Entity functions should be coded? Instead of accessing expected variables of this object which sometimes aren't there, should I be querying the database from the Entity? Is there another approach I should be doing?
Am I misunderstanding how the Entity functions should be coded?
Yes, you do, because...
Instead of accessing expected variables of this object which sometimes aren't there, should I be querying the database from the Entity? Is there another approach I should be doing?
...an entity should be a dumb data object. When you fetch data from there you add business logic. Any data manipulation should happen somewhere in the model layer, or a service. Most of the time people using CakePHP put code into the table object, which is somewhat OKish. I'm creating additional classes in the model layer for different things and namespace them App\Model\SomeModule or directly in my app root App\SomeModule and inject whatever else I need there (request, tables...).
This code is also absolute inefficient:
public function getCommentCount(){
$count = 0;
foreach ($this->comments as $comment){
if ( $comment->isPublished() ) {
$count += 1;
}
}
return $count;
}
It assumes all comments were loaded
It is required to get all comments to get an accurate count
it iterates over all comments to filter published comments
It does it inside the entity
What if there are 500 comments? Why aren't you simply doing a count query on the database and filter them by their published status? But this would still require you to do one additional query per record. So for counts it is a lot more efficient and easy if you use the Counter Cache Behavior.
If you really need to manipulate data after you fetched it but before rendering it use map/reduce. You can refactor your getCommentCount() and use map/reduce instead if you would like to stick to your inefficient way of doing it. The linked documentation even contains an example for counting stuff.
In your index funcition you set $post, but in the index.ctp you use $posts, and you missed out to contain Comments relationship in your query.
I changed it for you:
public function index() {
$posts = $this->Posts->find('all')->contain([
'Users',
'Comments'
])
->limit(20);
$this->set(compact('posts'));
}
And based on your comment, you could get the total comments in a query like this:
public function index() {
$posts = $this->Posts->find();
$posts->select([
'total_comments' => $posts->func()->sum('Comments.id')
])
->select($this->Posts)
->contain([
'Users',
'Comments'
])
->limit(20);
$this->set(compact('posts'));
}
See Returning the Total Count of Records for more info.
You can do it this way:
$posts = $this->Posts->find('all' , [
'fields' => array_merge(
$this->Posts->schema()->columns(), [
'countCommentsAlias' => '(
SELECT COUNT(comments.id)
FROM comments
WHERE comments.post_id = Posts.id
)',
'Users.name',
'Users.email'
]
),
'contain' => ['Users']
])
->toArray();

select list association in cakephp

i am new to cake and using version 2.
i have models
hgcylinder.php
class HgCylinder extends AppModel {
//put your code here
var $name= "HgCylinder";
var $belongsTo = array('HgKeyGase');
}
hgkeygase.php
class HgKeyGase extends AppModel {
//put your code here
var $name= "HgKeyGase";
public $belongsTo = array(
'HgKeyColor' => array(
'className' => 'HgKeyColor',
'foreignKey' => 'hg_key_color_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
}
Controller HgCylindersController.php
<?php
class HgCylindersController extends AppController
{
var $name = "HgCylinders";
// var $scaffold;
function index()
{
$this->set('hey',$this->HgCylinder->find('all'));
}
public function edit($id = null) {
$this->HgCylinder->id = $id;
if ($this->request->is('post')) {
var_dump($this->request->data);
exit;
if ($this->HgCylinder->save($this->request->data)) {
$this->Session->setFlash(__('Cylinder has been updated successfull'));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(__('The cylinder could not be saved. Please, try again.'));
}
} else {
$this->request->data = $this->HgCylinder->read(null, $id);
}
$hg_key_gase_id = $this->HgCylinder->HgKeyGase->find('list');
$this->set(compact('hg_key_gase_id'));
}
}
?>
View : edit.ctp
<?php
echo $this->form->create('HgCylinder',array('action'=>'edit'));
echo $this->form->input('hg_key_gase_id',);
echo $this->form->input("capacityM3");
echo $this->form->input("weightEmpty");
echo $this->form->input("weightFilled");
echo $this->form->input("isFilled");
echo $this->form->end('Add');
?>
my problem is hg_key_gase_id is become select list with no options. if i changed the name to "hgKeyGas" in view and controller it shows the options from the hg_key_gases table. but on saving does not saving the value of hg_key_gase_id field in hg_cylinders table instead it stores null in this field.
second i want to know that it is necessary to have variable name passing to view from controller exactly same for as field in table.
try to stick to conventions.
so its
$hgKeyGases = $this->HgCylinder->HgKeyGase->find('list');
$this->set(compact('hgKeyGases'));
the pluralized form will be able to populate your select box (as documented in the cook book)
also use $this->Form->input() (note the capital F). $name is not necessary.
dont use read(), use find(first) instead. dont set the action (the form will post to itself by default).
and most importantly. ALWAYS respect the casing of files in your filesystem. especially if you plan on deploying on NIX systems (which are case sensitive).
so it would be HgCylinder.php and HgKeyGase.php as Model class files.
last tip: use baking (cake bake via shell console) to bake your crud files. this way you learn how its done the right way. it would have also answered your question itself by the way.
the documentation can be found here: http://book.cakephp.org/2.0/en/index.html
maybe you found an old outdated version, the 2.x one is the one you should have used.

Running find('all') or find('first') on identical set of conditions in Cake 2

In a controller/index action in my CakePHP2 app, I have
$this->Model-find('all',array( ... lots conditions and things... ));
Then, in controller/view I have:
$this->Model-find('first',array( ... lots conditions and things... ));
Both of these work really well, but I'm obviously repeating a large chunk of code (namely, the find conditions, field list etc) with the only two differences being that in the first one I'm doing find('all') and in the second one I'm doing find('first') and providing the id as one of the conditions.
Is there a way that I can move the conditions into the model and then run different types of find on those conditions?
You can create a method in Youur Model:
public function getConditions($id = null) {
return array( ... lots conditions and things... );
}
somewhere in this method just test if (!empty($id)) to return correct result. (You haven't provide the 'conditions and things' so I can't write the exact code here...
and use it in both controler actions:
$this->Model->find('first', $this->Model->getConditions()));
$this->Model->find('all', $this->Model->getConditions()));
Yes you can move your query into model class as a function/method. see sample bellow for post model:
class Post extends AppModel {
public $name = 'Post';
public function getAll() {
return $this->find('all', array('limit' => 20, 'order' => 'Post.created DESC'));
}
}
and call it on your controller by $this->Model->getAll();
$this->Post->getAll();
You can also add parameter to your function.
Add the conditions as an attribute of your controller
class FoosController extends AppController{
public $conditions = array( ... lots conditions and things... )
public function index(){
$this-Foo->find('all', $this->conditions);
}
public function view(){
$this-Foo->find('first', $this->conditions);
}
}

CakePHP-2.0 How can i get the username from $allposts=$this->paginate('Post'); where user_id is foreign key of posts table?

I'm using CakeDC-Users plugin.
<?php
class Post extends AppModel {
public $useTable='posts';
public $belongsTo = array('User');
public $hasMany=array('Comment');
}
I had to use paginate:
$allposts=$this->paginate('Post');
I can get the user_id in this way:
foreach ($allposts as $post) {
debug($post['Post']['user_id']);
But i need the username not the user_id. How can i get username?
The containble feature of CakPHP hides all associated models by default: http://book.cakephp.org/2.0/en/core-libraries/behaviors/containable.html
If you want to add only one field of an associated Model you can use this syntax:
$allposts = $this->Post->find('all', array('contain' => 'User.username'));
or with paginate use this in your controller class: (http://book.cakephp.org/1.3/en/view/1232/Controller-Setup)
var $paginate = array('contain' => 'User.username');
Try the following to access it:
$post['User']['username'];
Try setting the recursive property to 2:
$this->Post->recursive = 2;
$allposts = $this->paginate('Post');
debug($allposts);
$allposts should contain every Post and it's belonging user.
See also: Cakephp pagination on recursive conditions

CakePHP association one Model to exclusively one out of three others?

i got a problem with designing my Database and the CakePHP code around it.
I have 4 Models,
ServerName
VirtualMachine
ServerHousing
ManagedServer
In ServerName, i want to save all ServerNames which then can be used in either one of the three other models. How can i achieve that i am only able to link one ServerName to either one of the other models?
Thank you guys in advance.
EDIT:
I now did it a little bit different. First of all it need to be done in the Model itself.
I used the validate option in cakePHP's models.
The code is like this:
public $validate = array(
'server_name_id' => array(
'rule' => 'serverNameTaken',
'message' => 'This Servername has already been taken.'
)
);
public function serverNameTaken()
{
$this->ManagedServer = ClassRegistry::init("ManagedServer");
// Assuming the server_name_id was passed from the form...
$server_name_id = $this->data['VirtualMachine']['server_name_id'];
// Check if this servername_id is already saved in virtual_machines
if ($this->ManagedServer->find('count', array(
'conditions' => array(
'ManagedServer.server_name_id' => $server_name_id
)
)) > 0
) {
// Found the server_name in the VirtualMachine model!
return false; // Prohibit saving the data
}
if ($this->find('count', array(
'conditions' => array(
'VirtualMachine.server_name_id' => $server_name_id
)
)) > 0
) {
// Found the server_name in the VirtualMachine model!
return false; // Prohibit saving the data
}
// Do this for the other models too. If a return false is not hit by now,
// everything should be fine and you can...
return true;
}
Same code I used in the other models, only the code had to be altered.
Thanks again for you answer!!!
You can't do this on the Model layer. Models are either associated or they are not. What you are probably looking for is some logic in your Controller that checks if a row for ServerName X has already been saved in any of the other models, prior to saving it from another model.
The beforeSave function is useful for this. For example, in your Controller, put this:
public $uses = array('VirtualMachine', 'ServerHousing', 'ManagedServer');
public function beforeSave() {
// Assuming the servername_id was passed from the form...
$servername_id = $this->request->data['servername_id'];
// Check if this servername_id is already saved in virtual_machines
if($this->VirtualMachine->find('count', array(
'conditions' => array(
'VirtualMachine.servername_id' => $servername_id
)
)) > 0) {
// Found the servername in the VirtualMachine model!
return false; // Prohibit saving the data
}
// Do this for the other models too. If a return false is not hit by now,
// everything should be fine and you can...
return true;
}
Hope this will get you going in the right direction.

Resources