Cakephp 3.0 - Validating Input against data in another model - cakephp-3.0

I'm a pretty new to Cakephp 3.0 and I'm quite stuck on data validation, or rather Application Rules.
I have a simple user registration form to create a new user in the table 'users'. In the user registration form, there is a field for "ticket_number" where the user has to enter a ticket number which must exist in the table tickets.ticket_number, and also tickets.registration_status must be false (that ticket had not had a user registered with it yet).
My tables look like this (simplified:)
users:
id | username | password
tickets:
id | ticket_number | user_id | registration_status
In my users model, I have defined (user can have many tickets):
$this->hasMany('Tickets', [
'foreignKey' => 'user_id'
]);
In my Tickets model, I have defined (a ticket belongs to a user):
$this->belongsTo('Users', [
'foreignKey' => 'user_id',
'joinType' => 'INNER'
]);
In the users model, I added a rule to attempt to check whether the entered ticket_number exists in the tickets.ticket_number column:
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->isUnique(['username']));
$rules->add($rules->isUnique(['email']));
$rules->add($rules->existsIn
(['ticket_number'], 'Tickets.ticket_number'));
return $rules;
}
This gives me the error:
Error: Call to a member function primaryKey() on a non-object
I'm very stuck here. Can anyone advise on the right approach implementing this check? And then, how to also implement the check to see whether the associated registration_status is 0 (that ticket has not been registered yet).
Thanks in advance for any advice!
--D.

The existsIn rule will not solve your problem, as it will not take your additional condition into account (registration-status).
You will need to provide a custom rule to accomplish the problem you described.
Regarding your update: The rule will return true - so passing the rule, if your tables are not setup correctly or it cannot find the field you are referencing or your field is nullable, check the source here: http://api.cakephp.org/3.0/source-class-Cake.ORM.Rule.ExistsIn.html#74-91

OK, after considerable tinkering, I solved the first issue (On the user add form, the user must enter a ticket number which must exist in the table tickets.ticket_number.)
The only way I could get this to work using an existsIn rule in the UsersTable model, was to declare the ticket_number field as the primary key in the initialize method of the TicketsTable model:
$this->primaryKey('ticket_number');
Then the associations and existsIn rule in the UsersTable model worked. However, this isn't good as I don't want the primary key in tickets permanently set to ticket_number. It also doesn't solve my second problem (checking tickets.registration_status is false.)
So I took a different approach and instead used $this->loadModel('Tickets'); in the add method of my users controller. Then, I could easily perform the required checks:
//Look for this particular ticket
$ticketCount = $this->Tickets->find()->where(['Tickets.ticket_number' => $ticket_number])->count();
$this->set(compact('ticketCount'));
$this->set('_serialize', ['ticketCount']);
//If the ticket isn't found, then return with the Flash error
if (!$ticketCount > 0) {
$this->Flash->error(__('Sorry, but a Ticket with this number could not be found. Please, try again.'));
return;
} //otherwise move on...
I then similarly query the registration status of the ticket, and flash a different error back if it is already registered.
After the checks are passed, it goes on to the normal Add User checks and saving.
While this solution isn't as elegant as what I was hoping to do in the Users model with a custom association and rule, it works.
I'm also not convinced it can't be done in the model with a rule. But for now, this solution works.
Anyway thanks hmic for a response.
DBZ

Related

Only one principal(user) can be created by role attribute which is enum + rails

i am new to rails and creating a school app where i have given an attribute role to user which is deciding the role of user and i want to create only one user can be principal(role), how do i achieve that there can not be create more than one principal user in app,
i am using devise for authentication...
i tried this =>
validates :role, :uniqueness => { :role => "principal" }
but it is restricting me from creating any kind of user , in my user enum i have given 4 roles [admin,principal,teacher,student]
any help or advice will be appreciated.... thanks in advance
I would use a custom validation like this:
validate :one_principal_exists
private
def one_principal_exists
return unless principal?
return unless User.where.not(id: id).exists?(role: 'principal')
error.add(:role, 'already exists')
end
The idea is to check first if the current user is a principal, if not skip the validation. And then check if there is already another user (who has a different id than the current user) in the database who has the principal role. If such a user exists, then add a validation error to the role attribute.
Please note that you might need to add more conditions to the database query when you, for example, want to support multiple schools and each school has a different principal.
in model use below code
user.rb
before_create :check_principle
def check_principle
if User.find_by(role: "principle")
errors.add(:code, "principle already exist")
return false
end
end

Laravel Eloquent - auto-numbering on has many relationship

I'm very much a beginner when it comes to database relationships hence what I suspect is a basic question! I have two database tables as follows:
Projects
id
company_id
name
etc...
rfis
id
project_id (foreign key is id on the Projects table above)
Number (this is the column I need help with - more below)
question
The relationships at the Model level for these tables are as follows:
Project
public function rfi()
{
return $this->hasMany('App\Rfi');
}
RFI
public function project()
{
return $this->belongsTo('App\Project');
}
What I'm trying to achieve
In the RFI table I need a system generated number or essentially a count of RFI's. Where I'm finding the difficulty is that I need the RFI number/count to start again for each project. To clarify, please see the RFI table below which I have manually created with the the 'number' how I would like it displayed (notice it resets for each new project and the count starts from there).
Any assistance would be much appreciated!
Todd
So the number field depends on the number of project_id in the RFI table. It is exactly the number of rows with project_id plus one.
So when you want to insert a new row, you calculate number based on project_id and assign it.
RFI::create([
'project_id' => $project_id,
'number' => RFI::where('project_id', $project_id)->count() + 1,
...
]);
What I understood is that you want to set the value of the "number" field to "1" if it's a new project and "increment" if it's an existing project. And you want to automate this without checking for it every time you save a new row for "RFI" table.
What you need is a mutator. It's basically a method that you will write inside the desired Model class and there you will write your own logic for saving data. Laravel will run that function automatically every time you save something. Here you will learn more about mutators.
Use this method inside the "RFI" model class.
public function setNumberAttribute($value)
{
if(this is new project)
$this->attributes['number'] = 1;
else
$this->attributes['number']++;
}
Bonus topic: while talking about mutators, there's also another type of method called accessor. It does the same thing as mutators do, but just the opposite. Mutators get called while saving data, accessors get called while fetching data.

Validation for three unique fields and soft deletes

Last year I made a laravel site with an events table where I needed three fields to be unique for any event (place, date and time). I wasn't able to set up a validation request to do this so I added an unique index for these three fields directly through phpmyadmin and catching the exception that could happen if a duplicated event was inserted.
So basically my store() method has a try/catch like this:
try {
$event = new Event;
$event->place = $request->input('place');
$event->date = $request->input('date');
$event->time = $request->input('time');
$event->save();
return view(...);
} catch (\Illuminate\Database\QueryException $e) {
// Exception if place-date-time is duplicated
if($e->getCode() === '23000') {
return view('event.create')
->withErrors("Selected date and time is not available");
}
}
Well, now I had to change the app so events could be soft deleted and I simply added the 'deleted_at' field to the unique index, thinking it would be so easy... This approach doesn't work anymore so I've been reading here and there about this problem and the only thing I get is I should do it through a validation request with unique, but honestly I just don't get the syntax for this validation rule with three fields that can't be equal while a fourth one, deleted_at, being null.
My app checks for the available places, dates and times and doesn't let the user choose any not available event but no matter how many times I've told them there's always someone who uses the browser back button and saves the event again :(
Any help will be much appreciated. Thank you!
This is not a good approach to solve the problem.
You can do follow things to solve this problem
Before insert into database get a specific row if exist from database
and store into a variable.
Then check the data is already stored into the database or not.
If data is already there create custom validation message using Message Bag Like below.
$ifExist = $event
->wherePlace(request->input('place'))
->whereDate(request->input('date'))
->whereTime(request->input('time'))
->exist();
if ($ifExist) return 'already exist';
It might help you.
#narayanshama91 have pointed the right way.
You said you would like to use the unique rule to validate the input but the problem is that last week there was a post in Laravel Blog warning users of a possible SQL Injection via the unique rule if the input is provided by the user.
I would highly advise you to NOT USE this rule in this case since you depend on users input.
The correct approach in your case would be #narayanshama91 answer.
$ifExist = $event
->wherePlace(request->input('place'))
->whereDate(request->input('date'))
->whereTime(request->input('time'))
->exist();
if ($ifExist) {
return 'already exist';
}

How can we use the auth_rule table in Yii2 RBAC?

In Yii 2 RBAC, there is a new table called auth_rule. Can anyone explain its usage with a small example
create table [auth_rule]
(
[name] varchar(64) not null,
[data] text,
[created_at] integer,
[updated_at] integer,
primary key ([name])
);
The basic parts of yiis RBAC-cconcept stayed exactly the same. In both Yii1 and Yii2 you have the following tables:
auth_item: holds the actual rights, groups, roles, etc.
auth_item_child: defines the graph / hierarchy of the items
auth_assignement: assigns an item to a user
In Yii2 you now have a fourth table:
auth_rule: holds reusable rules to check if a right is actually granted
Why is this?
Yii1
The concept behind the rule was already there in Yii1...kind of at least. In Yii1 you had the possibility to define a "bizrule" in auth_item and auth_assignement. "bizrule" and "data" were columns in both those tables.
The contents of the columns were the following:
bizrule: held php-code which had to return a boolean value. This code was executed during rights check with eval(). That way you could control if a right was granted or not even though the user had the item assigned. Example: it makes no sense, but you could give a user a right only on even hours with this bizrule: return date('h') % 2 == 0.
data: held params which could be passed to the bizrule while beeing executed. This data was then available in the scope of the bizrule.
Yii2
The above solution works perfectly, except that the code of a bizrule is not reusable. Therefore this functionality was extracted into its own table.
If you look at the migration-file creating the basic rbac-tables (yii\rbac\migrations\m140506_102106_rbac_init.php) you can see that the item table now has a relation to the rule-table instead of hosting the code in one of its own columns.
There is however no relationship between auth_assignement and auth_rule. In Yii1 this allowed you to disable groups of rights at once. Since you can reuse a rule and attach it to all relevant items this is no longer necessary and was therefore removed.
Example
If you look at the actual implementation of yii\rbac\DbManager and yii\rbac\BaseManager an example shouldn't be necessary. Interesting are the following mthods:
DbManager::addRule(): serializes and persists a rule-instance
DbManager::getRule(): here you can see how the rule is retrieved, unserialized and returned. This means the rule is saved in a serialized format within the data-column of auth_rule.
BaseManager::executeRule(): the rule loaded above is executed via Rule::execute()
If you want to add a rule simply create an instance of yii\rbac\Rule and call DbManager::addRule($rule) with it as its param. This will serialize and save your rule making it reusable elsewhere. Awesome!
VoilĂ ...should be pretty clear now. If you have some open questions or want more details just write a comment.
Cheers and have a good one!
The rule attribute data is serialized.
What does this data look like? Is it like the array below as not yet unserialized?
[
'allow' => true,
'actions' => ['view'],
'roles' => ['viewPost'],
],

Laravel Eloquent how to limit access to logged in user only

I have a small app where users create things that are assigned to them.
There are multiple users but all the things are in the same table.
I show the things belonging to a user by retrieving all the things with that user's id but nothing would prevent a user to see another user's things by manually typing the thing's ID in the URL.
Also when a user wants to create a new thing, I have a validation rule set to unique but obviously if someone else has a thing with the same name, that's not going to work.
Is there a way in my Eloquent Model to specify that all interactions should only be allowed for things belonging to the logged in user?
This would mean that when a user tries to go to /thing/edit and that he doesn't own that thing he would get an error message.
The best way to do this would be to check that a "thing" belongs to a user in the controller for the "thing".
For example, in the controller, you could do this:
// Assumes that the controller receives $thing_id from the route.
$thing = Things::find($thing_id); // Or how ever you retrieve the requested thing.
// Assumes that you have a 'user_id' column in your "things" table.
if( $thing->user_id == Auth::user()->id ) {
//Thing belongs to the user, display thing.
} else {
// Thing does not belong to the current user, display error.
}
The same could also be accomplished using relational tables.
// Get the thing based on current user, and a thing id
// from somewhere, possibly passed through route.
// This assumes that the controller receives $thing_id from the route.
$thing = Users::find(Auth::user()->id)->things()->where('id', '=', $thing_id)->first();
if( $thing ) {
// Display Thing
} else {
// Display access denied error.
}
The 3rd Option:
// Same as the second option, but with firstOrFail().
$thing = Users::find(Auth::user()->id)->things()->where('id', '=', $thing_id)->firstOrFail();
// No if statement is needed, as the app will throw a 404 error
// (or exception if errors are on)
Correct me if I am wrong, I am still a novice with laravel myself. But I believe this is what you are looking to do. I can't help all that much more without seeing the code for your "thing", the "thing" route, or the "thing" controller or how your "thing" model is setup using eloquent (if you use eloquent).
I think the functionality you're looking for can be achieved using Authority (this package is based off of the rails CanCan gem by Ryan Bates): https://github.com/machuga/authority-l4.
First, you'll need to define your authority rules (see the examples in the docs) and then you can add filters to specific routes that have an id in them (edit, show, destroy) and inside the filter you can check your authority permissions to determine if the current user should be able to access the resource in question.