I am trying to setup a simple Restfull api using cakephp.
I followed the documentation from the Cakephp site.
But I am encountering the following issue.
I am using Postman plugin to test the Api calls.
I have a table called 'Categories' and in its controller have an action add().
public function add() {
if ($this->request->is('post')) {
$this->Category->create();
if ($this->Category->save($this->data)) {
$message = 'Saved';
}
else {
$message = 'Error';
}
$this->set(array(
'message' => $message,
'_serialize' => array('message')
));
}
}
and in db, I have Category table with schema (id (int 11, PK, A_I), name(varchar(40)), created (datetime), modified(datetime)).
So from postman I send POST requests to http://myProject/categories.json.
From my understanding when i send key:name and value: test, it must save into the database, which works fine. But I must get error when I replace the "key" with something other than name. i.e for exmaple key: name123 and value: test2, But this data too is getting saved in the db without any error except for the name field. i.e it is saving (id:some value, name:"", created:somevalue, modified:someValue)
I dont understand how. Any help will be greatly appreciated.
You will need to provide more info because what you say doesn't make sense. For example what do your posted data look like? Is there any kind of validation in the model? How do you expect a key/value pair to be stored in only one field (specifically name) in the DB?
What happens now is that you tell Cake to save the supplied data ($this->Category->save($this->data)) although you don't check (via cake's validation rules in the model or any other means) that it contains useful arguments and especially Category.name.
As computers will just do what you tell them to do and not what you imply or have in mind, it goes on and sends the calculated created/modified fields to the DB which in turn saves them with the autoincremented ID. Since name doesn't have a UNIQUE or NOT NULL field condition in the DB it is saved as NULL or empty string.
Related
I'm using ZF2 and mysql, but the question is platform-independent. I have a data transfer object Organization that gets hydrated from an html form. OrganizationMapper has a save method that (1) gets Organization as an argument and (2) fills a couple of database tables one after another.
Suppose the 1st table gets filled ok, but the 2nd doesn't because one of the properties of Organization isn't set (not null constraint on a column). The user gets an error, but the 1st table is already filled. If he attempts to submit the form again, but this time with all html fields filled, all the tables get filled ok, but the 1st has a previous unused row.
How could I avoid this situation?
I thought of checking for empty values with if's in the mapper's save method, but it doesn't seem elegant. I know about the InputFilter validations in ZF2, but these check the user input in the form, they don't check things when the php code communicates with the database.
Any help?
The best way is to validate all the data before you start writing it to the database.
I didn't use ZF2 and this solution is actually framework-dependent, so you need to check the ZF2 docs. For example, in Yii, you just define validation rules for every field of the model, so you can ensure that your Organization contains all the data before you start saving it to the database, probably something similar is possible in Zend.
Note, that validation doesn't mean just to check for empty values, you may need to verify different things, like: "email is correct email like xxx#yyy.com", "name is not empty", "name length is more than 3 chars", "name length is less than 1000 chars" and so on.
For Yii it roughly looks like this:
class Organization extends ActiveRecord {
...
// here we define the validation rules
public function rules() {
return [
// name is required
['name', 'required'],
// check min / max length
['name', 'string', 'min' => 3, 'max' => 12],
// check if email is valid
['email', 'email']
];
}
}
Now you can do $organization->validate() to make sure everything is correct (also when you do $organization->save() the rules will be checked before saving to the database).
And one more solution to protect from the inconsistent data is to use transactions. In the case you write to multiple tables, you anyway need them, even if you validated everything. Unexpected things happen, so it is better to protect your saving code like this (pseudo-code):
$transaction->start();
try {
$table1->writeSomeData();
$table2->writeMoreData();
$transaction->commit();
} (catch Exception $e) {
$transaction->rollback();
}
Again, check your framework documentation, it probably supports this in some way.
In my controllers that Gii creates it is common to see the following:
if($model->load(Yii::$app->request->post()) && $model->save()){
//.....do something such as redirect after save....//
}else
{
//.....render the form in initial state.....//
}
This works to test whether a POST is sent from my form && the model that I am specifying has saved the posted information (as I understand it).
I've done this similarly in controllers that I have created myself but in some situations this conditional gets bypassed because one or both of these conditions is failing and the form simply gets rendered in the initial state after I have submitted the form and I can see the POST going over the network.
Can someone explain why this conditional would fail? I believe the problem is with the 'Yii::$app->request->post()' because I have removed the '$model->save()' piece to test and it still bypasses the conditional.
Example code where it fails in my controller:
public function actionFreqopts()
{
$join = new FreqSubtypeJoin();
$options = new Frequency();
$model = new CreateCrystal();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
$model->insertFreqopts();
return $this->redirect(['fieldmap', 'id' => $join->id]);
} else {
return $this->render('freqopts', ['join' => $join, 'options' => $options]);
}
}
My initial thought was that I'm not specifying the correct "$model" in that I'm trying to save the posted data to FreqSubtypeJoin() in this case and the $model is CreateCrystal(); however, even when I change the model in this conditional it still fails. It would be helpful if someone could briefly explain what the method 'load' is actually doing in layman's terms if possible.
The load() method of Model class is basically populating the model with data from the user, e.g. a post query.
To do this it firstly loads your array of data in a form that matches how Yii stores your record. It assumes that the data you are trying to load is in the form
_POST['Model name']['attribute name']
This is the first thing to check, and, as long as your _POST data is actually getting to the controller, is often where load() fails, especially if you've set your own field names in the form. This is why if you change the model, the model will not load.
It then check to see what attributes can be massively assigned. This just means whether the attributes can be assigned en-mass, like in the $model->load() way, or whether they have to be set one at a time, like in
$model->title = "Some title";
To decide whether or not an attribute can be massively assigned, Yii looks at your validation rules and your scenarios. It doesn't validate them yet, but if there is a validation rule present for that attribute, in that scenario, then it assumes it can be massively assigned.
So, the next things to check is scenarios. If you've not set any, or haven't used them, then there should be no problem here. Yii will use the default scenario which contains all the attributes that you have validation rules for. If you have used scenarios, then Yii will only allow you to load the attributes that you have declared in your scenario.
The next thing to check is your validation rules. Yii will only allow you to massively assign attributes that have associated rules.
These last two will not usually cause load() to fail, you will just get an incomplete model, so if your model is not loading then I'd suggest looking at the way the data is being submitted from the form and check the array of _POST data being sent. Make sure it has the form I suggested above.
I hope this helps!
Either I don't understand how CCompareValidator works in Yii (sic!) or it doesn't work for me at all.
I want to check, if an ID of row / record / user being updated isn't the same, as an ID of currently logged-in user. And prohibit update, if it is.
I used CCompareValidator at first:
array('id', 'compare', 'compareValue'=>Yii::app()->user->id, 'message'=>'Boom!')
It doesn't work -- it halts editing / update of every row / record / user, no matter, what an ID actually is.
So, I rewrote it to my own, custom validator. In my opinion, the code is the same as in case of built-in one:
array('id', 'compareId', 'compareValue'=>Yii::app()->user->id, 'message'=>'Boom!')
public function compareId($attribute = null, $params = null)
{
if($attribute === 'id')
{
if($this->id == $params['compareValue'])
{
$this->addError($params['message']);
}
}
}
It works like a charm -- allows update of any row / record / user, which ID is different than currently logged-in user's ID. Blocks update, showing defined message, in case compared IDs are equal.
What am I missing? Why original Yii's built in validator fails on such simple example, while my own works?
The validator works, as supposed, my logic have failed:
CCompareValidator, throws an error, when two compared values are not equal. On the other hand, if they're equal -- it passes validation without errors. That is supposed behavior.
I wanted an error, when values are equal (which means, that user is attempting to edit himself or herself) and pass as validated, when both values are different (logged user edits different one).
That's why I need to use 'operator'=>'!=' as configuration of validator. This is the answer.
BTW: All the glory of solving this problem goes to Keith at YiiFramework.com's forum.
I am currently building a web app which has two models, Donor and Donation Models respectively. It has multiple user roles. When the staff user first registers a donor, I want him to be redirected to another form which allows him to fill in the Donation details(the donor is registered once the first donation is successful).
Firs of all, should I create a donation controller, from which I would redirect the user using:
return $this->redirect(array('controller'=>'donations','action'=>'add'));
For the above to work, it requires me to save the newly registered donor's id in a session like so :
$this->Session->write('id', $this->Donor->id);
So the user is redirected to 'donations/add' in the url, and this works fine.. However I think this has some flaws. I was wandering whether I should create another action inside the Donor controller called 'add_donation', which will have its respective 'View'. The idea is to be able to form a url of the sort : 'donors/add_donation/4' (4 being the donor_id ! )
This URL follows this construct: 'controller/action/id'
If anyone could shed some light on best practices, or describe any caveats to my solution(the former, using session etc.) , please do help a brother out! Ill be deeply indebted to you! Thanks in advance!
After you saved the data you can do this in the DonorsController:
$this->redirect(array(
'controller' => 'donations',
'action' => 'add',
$this->Donor->getLastInsertId()
));
There is no need to return a redirect, it's useless because you get redirected. Notice that we pass the last inserted record id as get param in the redirect. The redirect method of the controller calls by default _stop() which calls exit().
CakePHP3: There is a discussion about changing that default behavior in 3.0. Looks like in CakePHP 3.0 the redirect() won't exit() by default any more.
DonationsController:
public function add($donorId = null) {
// Get the donor to display it if you like to
if ($this->request->is('post')) {
$this->request->data['Donation']['donor_id'] = $donorId;
// Save code here
}
}
I would not use the session here, specially not by saving it to a totally meaningless and generic value named "id". If at all I would use always meaningful names and namespaces, for example Donor.lastInsertId as session key.
It's not always clear where to put things if they're related but the rule of thumb goes that things should go into the domain they belong to, which is pretty clear in this case IMHO.
Edit:
Leaving this edit here just if someone else needs it - it does not comply with the usage scenario of the asker.
If you have the user logged in at this stage, modify the add function to check if the userId passed is the same as the one logged in:
DonationsController:
public function add($donorId = null) {
// Get the donor to display it if you like to
if ($this->request->is('post')) {
if ($this->Auth->user('id') != $donorId) {
throw new InvalidArgumentException();
}
$this->request->data['Donation']['donor_id'] = $donorId;
// Save code here
}
}
You can use also the same controller using more models with uses.
Or you can also to ask to another controller with Ajax and morover to get response with Json.
Okay, so I'm fairly new to CakePHP. This is the setup:
I've got a Model (Reservation), and a controller, (ReservationController).
I'm trying to provide a simple add() functionality.
The request url is: www.example.com/reservations/add/3
Where 3 is the ID of the event this reservation is for.
So far, so good. The problem is, the form is constructed like this:
<h2>Add reservation</h2>
<?php echo $form->create('Reservation');
echo $form->input('first_name');
echo $form->input('last_name');
echo $form->input('email');
echo $form->input('tickets_student'); echo $form->input('tickets_nstudent');
echo $form->end('Add');?>
When I hit the send button, the request URL becomes simply www.example.com/reservations/add/, and the event id is lost.
I've solved it now by grabbing the id in the controller, and make it available to the form:
// make it available for the form
$this->set('event_id', $eventid);
And then, in the form:
$form->create('Reservation',array('url' => array($event_id)));
It works, but it strikes me as a bit ugly. Isn't there an easier way to make sure the form POST action gets made to the current url, instead of the url without the id?
Nik's answer will fail if the website isn't in the server public_html root.
This answer is more solid:
<?php echo $form->create('Reservation',array('url'=>'/reservation/add/'.$data['id']));?>
Following the strictest convention for just a moment, reading a URL like /reservations/add/3 would be, well, confusing. You're calling on the ReservationsController to act on the Reservation model, but passing it an event ID. Calling /reservations/edit/3 is far less confusing, but just as wrong for your situation since the id value, "3", would be assumed to be a reservation identifier.
Essentially, you're making an ambiguous request at best. This is a simple form to create a reservation and that reservation has to be associated with an event. In a "traditional" scenario, the form would allow the user to select an event from some kind of list. After all, the foreign key, probably event_id in this case, is really just another property of a reservation. The list would have to be populated in the controller; this is usually done via a find( 'list' ) call.
If you already know the event that you want to create the reservation against (which you apparently do), then I'd probably select the analogous method of using a hidden field in the form. You still have to set the value in the controller just as you're doing, but the end result is a bit more Cake-y. The hidden field would be named data[Reservation][event_id] (again, I'm assuming the name of your foreign key field).
$form->create('Reservation',array('action' => 'add',$eventId);
and in the controller:
function add($eventId = null)
{
if($eventId == null)
{
// error state
throw new NotFoundException('Invalid id');
}
...
}
I do it all the time.
You can do following:
$form->create('Reservation',array('url' => $this->Html->url()));
this way all your variables from the url will be added in the form action :)
As Rob Wilkerson suggests, the issue is your URL route doesn't accurately describe the operation being performed. It becomes further confusing when you want to edit the reservation: /reservations/edit/6. Now the number in the URL means something different.
The URL convention I use for situations like these (adapted to your particular case) is /events/3/reservations/add. It takes a bit more up-front to configure your routes, but I've found it's superior for clarity down the road.
Sample routes.php:
Router::connect(
'/events/:event_id/reservations/:action',
array('controller'=>'reservations','action'=>'index'),
array(
'pass' => array('event_id'), // pass the event_id as a param to the action
'event_id' => '[0-9]+',
'actions'=>'add|index' // only reverse-routes with 'action'=>'add' or
// 'action'=>'index' will match this route
)
)
// Usage: array('controller'=>'reservations','action'=>'add','event_id'=>3)
Router::connect(
'/events/:event_id/reservations/:id/:action',
array('controller'=>'reservations'),
array(
'pass' => array('id'), // pass the reservation id as a param to the action
'id' => '[0-9]+',
'event_id' => '[0-9]+',
'actions'=>'edit|delete' // only reverse-routes with 'action'=>'edit' or
// 'action'=>'delete' will match this route
)
)
// Usage: array('controller'=>'reservations','action'=>'edit','event_id'=>3,'id'=>6)
In your FormHelper::create call, you'd have to specify most of the reverse-route you want to follow, but again, the up-front cost will pay dividends in the future. It's usually better to be explicit with Cake than to hope its automagic always works correctly, especially as the complexity of your application increases.