How to avoid errors in saving data - Cakephp - validation

I'm using Cakephp and trying to put in a method to make sure our reservation system doesn't let two users book the same appointment. Ex. User 1 opens the appointment, and User 2 opens it simultaneously. User 1 books the appointment. User 2 tries to book it but the system checks and sees it is no longer available.
I imagine this would take place in validation, or in a beforeSave(), but can't figure out how to do it.
Right now I made a function in the model to call from the controller. In the controller I have:
if ($this->Timeslot->checkIfNotAvailable()) {
$this->Session->setFlash('This timeslot is no longer available');
$this->redirect(array('controller' => 'users', 'action' => 'partner_homepage'));
}
and in the model I have this function:
function checkIfNotAvailable($data) {
$this->recursive = -1;
$timeslot = $this->find('all', array(
'conditions' => array(
'Timeslot.id' => $this->data['Timeslot']['id'])
)
);
if ($timeslot['student_id'] == 0) {
//They can reserve it, do not spring a flag
return false;
} else {
//Throw a flag!
return true;
}
}
I think I'm mixed up using custom validation when it's not called for. And it's not working obviously. Any suggestions?
Thanks!

If what you have is working, you can stick with it, you could also try creating a beforeValidate() call back function in your Model.
class YourModel extends AppModel {
function beforeValidate(){
if( !$this->checkIfNotAvailable( $this->data ) ) {
unset($this->data['YourModel']['time_slot']);
}
return true; //this is required, otherwise validation will always fail
}
}
This way you remove the time_slot before it goes to validation and it will drop a validation error at that point, kicking the user back to the edit page and getting them to pick a different time slot, ideally the updated data entry page will no longer have the used time slot available.

Related

Yii2 validation on controller action

I am developing a Yii 2.0 application in which users can create orders then send the orders to review and after that it follows a number of stages in the workflow.
Everything is ok until yesterday that the customer ask for the possibility that before sending the orders to review the order are considered as draft. Which means I have to turn off validations on create and validate them when users clicks Send To Review button. I know Yii 2.0 supports scenarios but maybe scenarios doesn't apply to this because the Send To Review button is shown in a readonly view. This forces me to do validation inside the controller action because there is no send_to_review view. How can this be done (I mean model validation inside controller action)?
Here is the controller action code
public function actionSendToReview($id)
{
if (Yii::$app->user->can('Salesperson'))
{
$model = $this->findModel($id);
if ($model->orden_stage_id == 1 && $model->sales_person_id == Yii::$app->user->identity->id)
{
$model->orden_stage_id = 2;
$model->date_modified = date('Y-m-d h:m:s');
$model->modified_by = Yii::$app->user->identity->username;
//TODO: Validation logic if is not valid show validation errors
//for example "For sending to review this values are required:
//list of attributes in bullets"
//A preferred way would be to auto redirect to update action but
//showing the validation error and setting scenario to
//"send_to_review".
$model->save();
$this::insertStageHistory($model->order_id, 2);
return $this->redirect(['index']);
}
else
{
throw new ForbiddenHttpException();
}
}
else
{
throw new ForbiddenHttpException();
}
}
What I need to solve is the TODO.
Option 1: Showing validation errors in the same view and the user has to clic Update button change the requested values save and then try to Send To Review again.
Option 2: Redirecting automatically to update view already setting scenario and validation errors found in the controller.
Thanks,
Best Regards
You can use $model ->validate()for validation in controller.
public function actionSendToReview($id)
{
if (Yii::$app->user->can('Salesperson'))
{
$model = $this->findModel($id);
if ($model->orden_stage_id == 1 && $model->sales_person_id == Yii::$app->user->identity->id)
{
$model->orden_stage_id = 2;
$model->date_modified = date('Y-m-d h:m:s');
$model->modified_by = Yii::$app->user->identity->username;
//TODO: Validation logic if is not valid show validation errors
//for example "For sending to review this values are required:
//list of attributes in bullets"
//A preferred way would be to auto redirect to update action but
//showing the validation error and setting scenario to
//"send_to_review".
//optional
$model->scenario=//put here the scenario for validation;
//if everything is validated as per scenario
if($model ->validate())
{
$model->save();
$this::insertStageHistory($model->order_id, 2);
return $this->redirect(['index']);
}
else
{
return $this->render('update', [
'model' => $model,
]);
}
}
else
{
throw new ForbiddenHttpException();
}
}
else
{
throw new ForbiddenHttpException();
}
}
If you don't need validation in actionCreate().Create a scenario for not validating any field and apply there.

Where to put filter form code in zf2 MVC pattern

I’ve added a small form to an index view to allow users to filter the data. I have placed the following code for the form inside the controller, but I question whether this is the right place to put it.
// ...
public function indexAction()
// ...
// build group list
$groupList = array(
0 => 'all',
1 => 'short people',
2 => 'tall people',
3 => 'fun people',
4 => 'boring people',
);
// create group selection box
$groupSelect = new Element\Select('group');
$groupSelect->setValueOptions($groupList);
$groupSelect->setAttributes(array(
'onChange' => 'this.form.submit()',
));
// create filter form
$form = new Form('group-filter');
$form->add($groupSelect);
$form->setData(array(
'group' => $group,
));
// process the form
$request = $this->getRequest();
if ($request->isPost()) {
$groupSelection = $request->getPost('group', 0);
return $this->redirect()->toRoute('admin-members', array('group'=>$groupSelection,));
}
// ...
Following the MVC pattern, does all of this code belong in the controller?
Nope it does not belong in the controller. Create a new form class (that extends Zend\Form\Form) and inject into the controller class. You can do that through the controllers factory, either through a factory class or the anonymous function "factory".
Other way to do it would be to get it (the form you created) in the controller from the service manager, but as far I know that's not the recommended method anymore, even though it still in the ZF2 docs.
That way your form code will be separated from the controller code, not mixing with the actual controller logic and, in the former case, also more easily testable.
You can learn more from this ZF2 forum thread. It's lengthy, but there are code samples and lead devs from ZF2 team are weighing in.

Clean way of checking if resource exists and if user can edit it

I have two very common steps that I have to repeat in almost every CRUD method in my Controllers. I have my Users split into 2 groups ( Users, Administrators ). Now Users can edit, update and delete only their own entries while admins can do all the CRUD operations.
The second piece of code I find my self writing every time is checking if the resource exist which is repetitive and somewhat annoying.
Here is what I attempted:
<?php
class BaseController extends Controller
{
// Received Eloquent model each model has user_id field
public function authorize($resource)
{
// Check if currently logged in users id matches user_id
// value of the resource
if($resource->user_id !== CurrentUser::getUser()->id)
{
// Users id does not match with resource user_id check if user is admin
if(!CurrentUser::getGroup() === 'Admin')
{
// The id's do not match and user is not admin redirect him back to root
Session::flash('error', 'You cannot edit this resource');
return Redirect::to('/');
}
}
}
}
class CarController extends BaseController
{
public function edit($id)
{
// Attempt to find the resource
$car = Car::find($id);
// Check if found
if(!$car)
{
// Resource was not found
Session::flash('error', 'Resource was not found');
return Redirect::to('/cars');
}
// First check if user is allowed to edit the resource
// this however does not work because returned Redirect is simply ignored I would
// have to return boolean and then check it but...
$this->authorize($car);
// ... rest of the code
}
}
This would not be a problem if I had 3-4 methods but I have some 6-10 methods and as you can see this part takes some 20 lines of code add that 6-10 times not to mention it's repetitive to the point where it get's annoying.
I have tried to solve the problem using a filter but the problem is that I can pass the id to the filter but not get it to work in a way that I would pass the model as well.
There has to be a cleaner way to implement all this. I'm somewhat happy with authorize function/process but it would be awesome not having to call is every time possibly having some filter and each controller would define global variable/array of methods that require authorization.
As for checking if record was found I was hoping maybe a filter could be done to catch all RecordNotFound exceptions and redirect back to controllers index route with a message.
You can use findOrFail() and catch the exception in your BaseController and you also have two options:
try
{
$post = $this->post->findOrFail($id);
return View::make('posts.show', compact('post'));
}
catch(ModelNotFoundException $e)
{
return Redirect::route('posts.index');
}
Or
$post = $this->post->findOrFail($id);
return View::make('posts.show', compact('post'));
And a exception handler returning back to your form with the input:
App::error(function(ModelNotFoundException $exception)
{
return Redirect::back()->withErrors()->withInput();
});
Note that those are just examples, not took from your code.

Vallidation errors with saveAll in CakePHP

I have a list of User records that I'm saving at one time (the form is populated from an uploaded CSV).
Everything seems to be working fine, and my validation rules are being honoured, but one thing that's eluding me are the specific validation error messages.
I get the general validation error (if say, a field that is supposed to be unique is not), but I'm not getting an error on the specific field to let the user know what the actual problem is.
Is this a bug, or is there something wrong with my Code? Here's the action from my Controller (fwiw I'm passing the CSV data to this action from another action, hence the snippet at the beginning):
public function finalizeCsv() {
if ( isset($this->request->params['named']['csvData']) ) {
$csvData = unserialize( $this->request->params['named']['csvData'] );
} else {
$csvData = $this->request->data;
}
$this->set('users', $csvData);
if ($this->request->is('get')) {
$this->request->data = $csvData;
}
if ($this->request->is('post') || $this->request->is('put')) {
if ($this->User->saveAll($this->request->data)) {
$this->Session->setFlash('Users added!', 'default', array('class' => 'success'));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash('There were errors with your data.');
}
}
}
For unique fields add a validation rule
'field_name' => array(
'rule' => 'isUnique',
'required' => 'create'
),
Next - when you get a error you will need to review $this->User->validationErrors. There will be many arrays inside - 1 for each created record. Loop through them, collect errors and send them to $this->Session->setFlash().
Also you better wrap $this->User->saveAll($this->request->data) into transaction if you're using InnoDB or other engine that supports transactions. On error - do rollback and try again.
$ds = $this->User->getDataSource();
$ds->begin();
// > saving here < //
$ds->commit(); //< on success
$ds->rollback(); //< on error

Are cake events handled asynchronously?

At the moment I don't have any queuing functionality in my Cakephp aplication. I will need that in the near future. An upload will result in a batchjob that uses external API with usage limitations, so it would be best if it was handeled in a seperate threat with a queue.
I don't have any experience with this, so I'm going to try a different, but easier, example.
User actions result in e-mails being send. At the moment, the loading of the page is delayed by the (rather long) time it takes the server to send the e-mail. I'd like to use the Event system to fix this. (I am aware I can also do this using this the afterRender function, or dispatch it to a shellTask, but that way I don't learn anything)
From the example page:http://book.cakephp.org/2.0/en/core-libraries/events.html
I've found this example:
// Cart/Model/Order.php
App::uses('CakeEvent', 'Event');
class Order extends AppModel {
public function place($order) {
if ($this->save($order)) {
$this->Cart->remove($order);
$this->getEventManager()->dispatch(new CakeEvent('Model.Order.afterPlace', $this, array(
'order' => $order
)));
return true;
}
return false;
}
}
Let's say the function was called by a controller action:
public function place_order() {
$result = $this->Order->place($this->request->data);
$this->set('result', $result);
}
Now my question... Will the corresponding view be rendered after all the dispatched events completes? or will the Model function just trigger the event and then forget about it?
The last option seems more logical to me (which also resembles the mentioned jQuery functionality in the article)
The problem is that If this were true, I don't understand the later example:
In the example about using results:
// Using the event result
public function place($order) {
$event = new CakeEvent('Model.Order.beforePlace', $this, array('order' => $order));
$this->getEventManager()->dispatch($event);
if (!empty($event->result['order'])) {
$order = $event->result['order'];
}
if ($this->Order->save($order)) {
// ...
}
// ...
}
if the event was just triggered (and then forgot about) there is no way you can asume it has modified the passed event object on the next line of code!
I would like to use cake as much as possible, but I'm not sure if I can get my desired background behavior without shellTasks and external queue. Any tips about these Cake Events?
Cake Events are triggered synchronously. When an event is triggered, all available listeners are called, before proceeding with other instructions.
You can imagine it on your second example as:
public function place($order) {
$event = new CakeEvent('Model.Order.beforePlace', $this, array('order' => $order));
$this->getEventManager()->dispatch($event); // -> all listeners are called at this point
// ... here you can assume your $event was modified
if (!empty($event->result['order'])) {
$order = $event->result['order'];
}
if ($this->Order->save($order)) {
// ...
}
// ...
}

Resources