CakePHP transparent saving of unique data - cakephp

I have models Person and Phone/Email with HABTM relationship. After some pain I found out, that my life is easier, when I break HABTM into: Person hasMany PeoplePhone, Phone hasMany PeoplePhone, PeoplePhone belongsTo (Person,Phone). Well, I don't need any help with this :-) now, my problem is different:
Before I can pair Person with his Phone or Email, I need to save this Phone/Email and then get its ID.
Now I would like to save only unique Phones and unique Emails, so I have created this method in app_model.php:
function saveUnique($data = null, $unique_fieldname = null)
{
if (! $data) { return false; }
if (! $unique_fieldname) { return false; }
$id = $this->field('id', array($unique_fieldname => $data[$this->name][$unique_fieldname]));
if ($id)
{
$this->read(null, $id);
}
else
{
$this->create();
$this->set($data);
if (! $this->validates()) { return false; }
if (! $this->save()) { return false; }
}
return true;
}
It seems to work, but I am all new to CakePHP. How would CakePHP guru solve this function/method?
Thank you very much for your time.
-Petr

If I were you, I would stick with default Cake functionality rather than what you are doing. All of this functionality is built into Cake, so why reinvent the wheel?
First, HABTM relationships already work as you have broken them out. You can access the join models by adding with in your model associations. This should give you access to the intersection table data.
$hasAndBelongsToMany = array(
'Phone' => array(
'className' => 'Phone',
'joinTable' => 'persons_phones',
'foreignKey' => 'person_id',
'associationForeignKey' => 'phone_id',
'unique' => false,
'with' => 'PersonsPhones'
)
);
As far as your unique phone and email requirements go, why not use Cake's built in validation to do the checking for you? That way, you just call the save function and then Cake will do all the checking for you.
For instance, if a person only has one email address, then do this in the person model. This will validate that the input is an email address and that it is a unique email address in the database. You can do this validation with any field and build custom validation rules with ease.
public $validate = array(
'email' => array(
'email' => array(
'rule' => 'email',
'message' => 'You must enter an email address.'
),
'isUnique' => array(
'rule' => 'isUnique',
'message' => 'An account with this email address already exists.'
)
)
);
I take it that there is a reason for why you would use HABTM for an email address. Normally, people would not share email addresses, so hasMany would probably be a better relationship. I can see it working for phone as people do share phone numbers frequently. I say this so that you are sure that HABTM is the relationship you really want to use.

I've only been using CakePHP for a couple months but I would implement it a little different.
I'd add a UNIQUE index on person_id & email_id in my PeopleEmail table (do the same thing in PeoplePhone). This way we're not accidentally saving duplicate data, our physical schematic prevents duplicate records before they're even stored.
Now we just have to keep an eye out for the SQL error being thrown. I haven't dug too deep into this aspect of Cake yet but I do know the base AppModel class has an onError() method that may provide useful in this regard.

Related

Delete belongsTo association not working

I have a problem with associations. I have two tables : Companies and Users.
User hasOne Company and Company belongsTo User (OneToOne)
In my models, I wrote :
/* User.php Model */
public $hasOne = array(
'Company' => array(
'className' => 'Company',
'dependent' => true
)
);
/* Company.php Model */
public $belongsTo = array(
'User' => array(
'className' => 'User',
'dependent' => true
)
);
My problem : when I do
$this->Company->delete($id, true)
in my CompaniesController, the Company with the id $id is deleted but the User associated is not.
Could you help me?
There is no 'dependent' option in belongsTo, so the only one actually working is the other way around.
If you delete a User, it will delete it's Company.
Basically, deleting a parent can delete it's dependent children. But deleting a child can't delete it's "dependent" parent (since there's really no such thing in Cake's case of a "dependent parent").
More details here: http://book.cakephp.org/2.0/en/models/associations-linking-models-together.html
You could choose to run the association in both directions (requiring a field in each table to determine which it belongs to). That way, regardless of which you delete, it should always also delete the other.
Or, you could just delete the User that owns the company.

CakePHP conditions clause on associations when using Model::find()

I just confused because of a find() result. This is my configurations:
First, users can have different User.role values: student, admin, and some others.
// Book
public $belongsTo = array(
'Student' => array(
'className' => 'User',
'foreignKey' => 'student_id'
'conditions' => array('User.role' => 'student')
);
);
When I chain Models like $this->Book->Student->find('list'); I was expecting to get only users whose role are 'student', but instead, it gets all users. What is going on here, what is conditions for on association definition, where can it and cannot be used. Any lead would help, thanks.
PS: I am aware that I could put conditions on find(), that's not the issue
There is a difference between associated data and accessing an associated model object. If you access $this->Book->Student you're accessing the Student model and work in it's scope. The conditions in the defined associations work only in the context of the accessed object.
So if you do a find on the Book and list the students for that book:
$this->Book->find('first', array('contain' => array('Student'));
Your code will work correctly. It will find the book plus the first user who has the role stundent. BUT your association is wrong then: It should be hasMany. because why would you filter a book by role if the book just belongsTo one student?
If you want to filter users by their role you can implement a query param that is checked in beforeFind(), pseudocode: if isset roleFilter then add contention to filter by that role from roleFilter.
Or, if you don't need to paginate just create a getStudents() method in the user model that will return a find('list') that has the conditions.
Or Student extends User and put the filter in the beforeFind() and use that model instead of the User model in your Book association.
If you want to filter on model level or per model I think the last one is a good option. Don't forget to set $useTable and $name or the inherited model will cause problems.
you have miss , inside your model.
try this:
public $belongsTo = array(
'Student' => array(
'className' => 'User',
'foreignKey' => 'student_id', //<------ miss
'conditions' => array('User.role' => 'student')
);
);
Yoi can debug your query to check what is the real query that you make.
Personally I have never use this approach, I prefer to use foreign key with another table for examples Rolesand User.role_id.
Is better for me to use this approach to have more flexibility inside your app.
After I prefer to use a conditions where inside controller to check well the query, because in your way every query you search always for student role not for the other and can be a problem for the rest of role, because inside controller you see a find without conditions but it doesn't take right value because in your model there is a particular conditions.
For me the good way is to create a new table, use foreign key and where conditions inside action of the controller to view well what are you doing.
For default all relations are "left join", you must set the parameter "type" with "inner" value
// Book
public $belongsTo = array(
'Student' => array(
'className' => 'User',
'foreignKey' => 'student_id'
'conditions' => array('Student.role' => 'student'), // <-- Fix That (field name)
'type' => 'inner', // <-- add that
);
);

CakePHP permissions : are ACLs overkill

I have two main tables:
Books (id, author, isbn, ...)
Users (id, username, password, ...)
I am looking at building an application, wherein User1 logs in and can:
view list of all books (eg. only title)
view details (author, isbn, ...) of only certain books that he should have access to
Each user may have access to a certain set of books. I don't need various roles.
I have setup the MVC and the relationships (habtm) for the above. I am now looking at developing the permissions. Do you think the CakePHP ACL solves this problem or is it overkill?
If it is overkill, is there another component or easier way to build the desired functionality?
Yes, ACL is overkill
ACL is a very powerful and flexible system - but it doesn't come free it brings with it complexity. Unless you have a usecase where you absolutely need fine-grained permissions (the two rules you've described do not fit this) - don't use ACL.
Restricting to books a user he has added
This rule is easy to implement - e.g. add to relevant find calls:
$results = $BookModelInstance->find('all', array(
'conditions' => array(
'created_by' => AuthComponent::user('id')
)
));
Restricting to books a user he has bought
This rule is also easy to implement, thought slightly more involved:
$BookModelInstance->bindModel(array(
'hasOne' => array( // Yes, hasOne not hasMany
'MyPurchase' => array(
'className' => 'Purchase',
'foriegnKey' => 'user_id'
)
)
));
$results = $BookModelInstance->find('all', array(
'recursive' => 0, // to join hasOne+belongsTo associations into the query
'conditions' => array(
'MyPurchase.user_id' = AuthComponent::user('id'),
)
));
The bindModel call achieves the equivalent of SELECT .. FROM books LEFT JOIN book_users.. The conditions in the find call will therefore restrict results to books where there is a record of the user purchasing the book.
Putting them both together
A simplistic implementation of automatically applying both these rules would be:
model Book extends AppModel {
public $actsAs = array('Containable');
public $restrictToUser = true;
public function beforeSave($options = array()) {
if (!$this->id) {
// Store who created this book
$this->data[$this->alias]['created_by'] = AuthComponent::user('id');
}
return true;
}
public function beforeFind($queryData) {
if (!$this->restrictToUser) {
// we don't want to apply user-level restrictions
return true;
}
$userId = AuthComponent::user('id');
if (!$userId) {
// we want to restrict to the current user - there isn't one.
return false;
}
// define the association to the purchase table
$this->bindModel(array(
'hasOne' => array(
'MyPurchase' => array(
'className' => 'Purchase',
'foriegnKey' => 'user_id'
)
)
));
//ensure the purchase table is included in the current query
$queryData['contain']['MyPurchase'] = array();
// restrict to rows created by the current user, OR purchased by the current user
$queryData['conditions']['OR'] = array(
$this->alias '.created_by' => $userId,
'MyPurchase.user_id' => $userId
);
return $queryData;
}
}
This requires a field created_by (or equivalent) to be in the books table, and uses containable to ensure that the purchases table (or equivalent) is included in all relevant queries.
Most simple solution: Add a condition in your controller, so:
$this->set('books', $this->Book->find(
'all',
array('conditions' => array('Book.user_id' => $user['User']['id']))
);
Disadvantages: You will likely create duplicate code here since this check has to happen also in other places. Also when you start testing your model you can only test that it returns books, you cannot test a model method like: getMyBooks($userId). So no, not the preferred solution.
Next solution: Check in the model
It could be done by a check in for example your books model. You could just check in the afterfind() method whether the returned records are allowed or not. In your beforefind you could also add an additional condition to all queries.
In general a model should be fat so I would suggest implementing clear methods there like: getAllBooks, getBooksOfUser($User), getLatestBooksOfUser($User) etc.
Why is this a nice implementation? Because you now manage the access levels in a central place. You can test the model and you are sure it does only return books from this user.
With beforeSave etc. you can intervene every save attempt and first check: hey, you want to save this but is this really your book?
ACL solution
But in general it could be wise to implement some ACL solution (preferably the built in one) since that makes you application much more future proof. It allows flexibility, for example:
Each user may have access to a certain set of books. I don't need various roles.
That's true for now but the future can change it. So if you need a quick solution just custom filter the records. But think about the future.

CakePHP: the very basics of Containable

I read much about how great containable is. Honestly I have read all docs, I have it working in my Users controller, but some things are not clear:
Do I have to use it in All actions or only in Index()?
Do I have to define it in every controller index() function or is it enough to it once in the Users controller
What about if e.g. Country_ID is a FK connected to both user and a related model? For example:
function index() {
$this->paginate = array(
'limit'=>10,
'order'=>'User.created DESC',
'fields'=>array('User.id','User.name', 'User.country_id', 'User.email'),
'contain'=>array(
'Post',
'Company' => array(
'Country' => array(
'fields' => array('id', 'country')
)
),
'Position' => array(
'Profession'
),
'Preference',
'Country',
'Type'
),
);
$this->set('users',$this->Paginate('User'));
}
Country is both connected to User and Company. How to define this without creating duplicates?
Many thanks!
You seem to have the wrong idea about containable. It "allows you to filter and limit model find operations". You use it whenever you need include (or exclude) specific related model data in your find().
For example, a User hasOne Profile, hasMany Roles, which belongsTo a Company. And you need to get all the roles and related companies for a user, but you don't need the profile, you can use $this->User->find('first',array('conditions'=>...,'contain'=>array('Role'=>array('Company'))))
It has nothing to do with index() or users_controller.
Country is both connected to User and Company. How to define this without creating duplicates? What duplicates?

HABTM form validation in CakePHP

I have a Projects table and a Users table which are linked by a HABTM relation. In the "add" new Project page I have a multiple checkbox section to select Users for the new project. I want to have at least one User for the Project. What's the best way to approach this in CakePHP ?
Try this:
// app/models/project.php
/**
* An additional validation check to ensure at least one User is
* selected. Spoofs Cake into thinking that there are validation
* errors on the Project model by invalidating a non-existent field
* on the Project model, then also invalidates the habtm field as
* well, so when the form is re-displayed, the error is displayed
* on the User field.
**/
function beforeValidate() {
if (!isset($this->data['User']['User'])
|| empty($this->data['User']['User'])) {
$this->invalidate('non_existent_field'); // fake validation error on Project
$this->User->invalidate('User', 'Please select at least one user');
}
return true;
}
I stumbled on the same issue, but now - 3 years later - with CakePHP 2.3.
To be clear; Group has and belongs to User. I've had a form like this:
// View/Groups/add.ctp
echo $this->Form->input('name');
echo $this->Form->input('User');
With the validation rule like in user448164's answer:
// Model/Group.php
public $validate = array(
'User' => array(
'rule' => array('multiple', array('min' => 1)),
'message' => 'Please select one or more users'
)
);
That didn't work, after Googling for it, I found this question which couldn't still be the best solution. Then I tried several things, and discovered this to work just fine:
// View/Groups/add.ctp
echo $this->Form->input('name');
echo $this->Form->input('Group.User');
Way too easy solution, but had to dig into it to find out it works this way.
Hopefully it helps somebody one day.
Update for CakePHP 2.4.x (possibly 2.3.x as well)
When I wrote this answer, I was using CakePHP 2.3.x. Back then it worked perfectly for both validating and saving the data. Now when applying the same code on a new project, using CakePHP 2.4.x, it didn't work anymore.
I created a test case, using the following code:
$data = array(
'User' => array(
'Client' => array(8)
),
);
$this->User->create();
$this->User->saveAll($data);
My first thought was: Saving all means saving all "root" models, what actually makes sense to me. To save deeper than just the "root" ones, you'll have to add the deep option. So I ended up with the following code:
$data = array(
'User' => array(
'Client' => array(8)
),
);
$this->User->create();
$this->User->saveAll($data, array('deep' => true));
Works like a charm! Happy coding. :)
Update (2014/03/06)
Struggling with the same problem again, in this case with hasMany instead of habtm. Seems like it behaves the same way. But I found myself looking for this answer again, and got confused.
I'd like to make clear that it's key to use Group.User instead of User in your input. Else it won't use the User model validation.
I've just been looking at his problem myself on a project and came across a slightly more elegant solution, as long as you're only dealing with a habtm relationship and you need to ensure that at least one checkbox is selected.
so for example you're editing a Project and you want it to be associated with at least one user
Add this to beforeValidate()
// check habtm model and add to data
foreach($this->hasAndBelongsToMany as $k=>$v) {
if(isset($this->data[$k][$k]))
{
$this->data[$this->alias][$k] = $this->data[$k][$k];
}
}
In the validation rules add the following:
'User' => array(
'rule' => array('multiple', array('min' => 1)),
'message' => 'Please select one or more users'
)
teknoid's blog has a pretty in depth solution to your issue here. The most Cakey way of doing this would be to add custom validation to your model, as you mention in your comment above. Check out http://teknoid.wordpress.com/2008/10/16/how-to-validate-habtm-data/
From the article, where Tag HABTM Post (:: Project HABTM Users):
First, we validate the Tag model, by
using the data from the form to ensure
that at least one Tag was selected. If
so, we save the Post and the relevant
Tags.
2016 update for CakePhp 2.7
Full answer here : HABTM form validation with CakePHP 2.x
TL;DR;
AppModel.php
public function beforeValidate($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
if(isset($this->data[$model][$model]))
$this->data[$this->name][$model] = $this->data[$model][$model];
}
return true;
}
public function afterValidate($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
unset($this->data[$this->name][$model]);
if(isset($this->validationErrors[$model]))
$this->$model->validationErrors[$model] = $this->validationErrors[$model];
}
return true;
}
In the main model of your HABTM :
public $validate = array(
'Tag' => array(
'rule' => array('multiple', array('min' => 1)),
'required' => true,
'message' => 'Please select at least one Tag for this Post.'
)
);
If you are using CakePHP 2.3.x, you may need to add this code to your model in addition to the code that GuidoH provided, otherwise your HABTM model data may not save:
public function beforeSave($options = array()){
foreach (array_keys($this->hasAndBelongsToMany) as $model){
if(isset($this->data[$this->name][$model])){
$this->data[$model][$model] = $this->data[$this->name][$model];
unset($this->data[$this->name][$model]);
}
}
return true;
}
As per my comment on Guido's answer above, I use Guido's answer exactly, however I modify the data with the beforeSave callback before it saves to the database.
I have this issue on Cake 2.4.5+
public function beforeSave($options = array()) {
$temp = $this->data['Group']['User'];
unset($this->data['Group']['User']);
$this->data['User']['User'] = $temp;
return true;
}

Resources