Foreign Key In MySQL using Yii - mysql

I have the database just like this
==== Invoices ====
id
costumer_id
description
==== Costumers ===
id
firstname
lastname
Now I have made the relation in between models just like this.In Invoices models the relation is as like this
public function relations()
{
return array(
'customer' => array(self::BELONGS_TO, 'Customer', 'customer_id')
);
}
In costumer model the relation is just like this
public function relations()
{
return array(
'invoice' => array(self::HAS_MANY, 'Invoices','customer_id')
);
}
Now as my relation is defined one costumer has many invoices and the invoice is belongs to the costumer.
Now I made multimodel and loaded the Costumer model into Invoice model just like this.
public function actionCreate()
{
$model = new Invoices;
$customers = new Customers;
// Uncomment the following line if AJAX validation is needed
// $this->performAjaxValidation($model);
if (isset($_POST['Invoices'],$_POST['Customers']))
{
$model->attributes = $_POST['Invoices'];
$customers->attributes = $_POST['Customers'];
$valid = $model->validate();
$valid = $customers->validate();
if($valid)
{
$model->save(false);
$customers->id = $model->customer_id;
$customers->save(false);
$this->redirect(array('view','id'=>$model->id));
}
}
$this->render('create',array(
'model'=>$model,
'customers'=>$customers,
));
}
Here every thing is okay. I can insert the data for both models easily. But my problem comes here in the way that when I am inserting data from Invoice multimodel the foreign key id is not changing. It is showing zero everytime. Can some one tell me where I am wrong.Any help and suggestions will be highly appriciable.

My guess is that you are overriding the customer's primary key with the invoice's foreign key. I do not say that's not correct that way (maybe in your scenario it makes sense).
Let me explain what you are doing in that code:
First, you create new instances of two models, Invoices and Customers. Yii understands that as "they wish to insert new items in the database".
Then, you check if there are the items coming from an ajax form. If true, then,
You populate Invoices (defined as $model. I'd change it to $invoice, in case you need to edit and understand it further).
You also popupulate the customer's information, overriding the $valid value (so, you don't know if invoice is actually valid).
If valid (remember you're only validating customer's information), do,
Save the invoice
Override customer's id with invoice's foreing key to customer.
Save the customer, and redirect.
Now, what I got from that:
$valid doesn't work as expected: I'd change that to an incremental assignment.
You may not be passing a customer_id coming from the ajax form. Foreing keys are integers, and so if not defined within a model, it becomes 0 or NULL.
You are always passing id = 0 / NULL to Customer's model, so it would probably warn you when validating. However, you are using save(false), which means it doesn't pre-validate on save, so you never know it doesn't work.
So, according to this:
public function actionCreate()
{
$invoice = new Invoices;
$customers = new Customers;
// Uncomment the following line if AJAX validation is needed
// $this->performAjaxValidation($invoice);
if (isset($_POST['Invoices'],$_POST['Customers']))
{
$invoice->attributes = $_POST['Invoices'];
$customers->attributes = $_POST['Customers'];
$valid = true; /* expect it is always valid */
$valid &= $invoice->validate(); /* if $invoice is not valid, $valid will be false (true&false = false) */
$valid &= $customers->validate(); /* same as the above line */
if($valid)
{
$customers->save(); /* First save customers. It's the Foreign item */
$invoice->customer_id = $customers->getPrimaryKey(); /* new instances use getPrimaryKey() to get its id */
$invoice->save(); /* Save invoice AFTER getting customer's primary key */
$this->redirect(array('view','id'=>$invoice->id));
}
}
$this->render('create',array(
'invoice'=>$invoice,
'customers'=>$customers,
));
}
I hope this solves your problem.

Please you need to understand a clear scenerio here. why would you use
if($valid)
{
$model->save(false);
$customers->id = $model->customer_id;
$customers->save(false);
$this->redirect(array('view','id'=>$model->id));
}
$model->save(false); tells model that if this record is not save(), the it shoud set the $customers->id = $model->customer_id;
This will only return false because. I do rather prefer if you call ( $customers->id = $model->customer_id;) before the $model->save();
REMEMBER, if you need to check if Save() returns true, then set it to $model->save(true)

Related

Accessing to the `latest` record stored in DB table right after recording it

In Laravel After recording last row to a DB table, can I safely access same recorded data right after recording it by calling latest() queries? Because transactions by other users may occur at the same time, and it may not really be the last record anymore?
Edit:
For example:
Public function StoreNotif($data){
auth()->user()->Notif()->create(store $data here..)
}
Public function SendNotif(){
$data="123";
$this->StoreNotif($data)
event(new Notification(stored Notif instance?));
}
No, you cannot rely on the database to return the record from your current script.
The ->latest() method will always sort the records with the most recent created_at date first.
https://laravel.com/docs/6.x/queries#ordering-grouping-limit-and-offset
But you haven't provided any code or explanation as to why this is a concern. If you just created a new record, why do you need to query it again? You should already have access to an instance of the model.
EDIT: I've made a few edits to demonstrate how you would pass the model from a controller to an event as referenced in the comments. Please post your code if you want more specific help.
SomeController.php
function store()
{
$model = Model::create([
'some_data' => 1
]);
// fire an event with the newly created model
event(new SomeEvent($model));
dd($model);
}
------------------------
Model {
// ...
attributes: [
'id' => 101,
'some_data' => 1
'created_at' => '2019-10-06 12:48:01',
'updated_at' => '2019-10-06 12:48:01',
]
// ...
}
SomeEvent.php
<?php
namespace App\Events;
use App\Model;
use Illuminate\Queue\SerializesModels;
class SomeEvent
{
use SerializesModels;
public $model;
public function __construct(Model $model)
{
$this->model = $model;
// ...
}
}
EDIT: Per your newly added code, you just need to pass the new model back to the original method. You could do something like this.
Public function StoreNotif($data)
{
// add a return statement
return auth()->user()->Notif()->create(store $data here..);
}
Public function SendNotif()
{
$data="123";
// store the returned data to a variable
$model = $this->StoreNotif($data);
// call the event with the model instance
event(new Notification(model));
}
I'm not sure what 'latest' is but I do know that MySQL uses SELECT LAST_INSERT_ID as the query to get the 'per-connection' id of the last inserted item. Under the covers it's using mysql_insert_id so if you are in a language that supports it, you could use that too.

Cakephp Querybuilder : update rows with an identical field as selected one

My app's goal is to schedule posts through a franchise to its franchised.
The HQ schedules a post for a certain date and time, with text and potential image.
It creates the post with all necessary information in the database for each franchised(id, franchise_id, user_id, text, image, network, post_id)
post_id contains an id that is the same for each row that are completely identical besides the franchise_id.
When I add a post, it works well. But when editing, since it gets the ID of the post, it'll only edit the post that matches the id.
And that is fine when it is a franchised, it will then change the post_id to a custom value, and will be independent to the others.
But when it's the HQ(superadmin)logged in, I want him to edit all that matches the selected one by post_id.
Query builder is not something I'm used to and sometimes I thought about dropping it for standard SQL, but if it's there it's for a reason, so I would like your help in solving this with Cakephp's query builder.
public function edit($id = null){
$event = $this->Events->get($id);
if ($this->request->is(['post', 'put'])) {
$event = $this->Events->patchEntity($event, $this->request->data,['associated' => ['Networks'], ]);
if($isuper == 'true'){//if logged in user is superadmin
}else{
$event->user_id = $this->Auth->user('id');
}
if ($this->Events->save($event)) {
$this->Flash->success(__('your post has been updated'));
return $this->redirect(
[
'action' => 'index',
date('Y', $event->date->getTimestamp()),
date('m', $event->date->getTimestamp()),
$event->company_id
]
);
}
$this->Flash->error(__('unable to update your post'));
}
$this->set('event', $event);
$this->layout = 'ajax';
}
You could try making bulk updates using updateAll
Something like:
$this->Events->updateAll(
['field' => true], // whatever fields you are updating
['post_id' => 'some_id'] // the selected post_id
);

Yii2: how to perfect separate validate model and save model?

In which case is it better to use the Validate model? Example: I have two model
AgreementForm
Agreement
When I create data - I use
$model = new AgreementForm();
if ( $model->load( \Yii::$app->request->post() ) && $model->save() ) {
....
}
Code of AgreemtnForm
AgreementForm extemds Model {
......
public function save() {
if(!$this->validate()) {
return null;
}
$model = new Agreement();
$model->content = $this->content;
if( $model->save() ) {
return true;
}
}
But when I update data, I use only that code:
public function actionUpdate( $id ) {
$model = Agreement::findOne( $id );
if( $model->load( \Yii::$app->request->post() ) && $model->save() ){
$this->refresh();
}
else {
return $this->render('update', [
'model' => $model,
]);
}
}
What is the validation model in this case, if I duplicate the validation in another model? Can I only use 1 model?
I will try to explain why you should almost always use a Form model to validate your data, providing some cases:
1) Doing
if( $model->load( \Yii::$app->request->post() ) && $model->save() )
could be dangerous, the user can send post data that he was not supposed to.
eg. Agreement has a userid column that gets id of the user that created it. If a user, sends this field in the post request, he could potentially change its value.
Your form model should define the properties that you expect to be sent.
2) You want to define additional validation for your model based on eg. the user role, the time the Agreement is valid etc.
Lets say you have 2 user roles:
Retailer
Merchant
Retailer can create an Agreement that is 300 characters long and max price 1000, opposed to 700 and 10000 for the merchant.
How do you cope with that?
You create 2 different forms:
$user = Yii::$app->user;
if ($user->can('retailer')) {
$model = new RetailerAgreementForm();
}
else {
$model = new MerchantAgreementForm();
}
if ( $model->load( \Yii::$app->request->post() ) && $model->save() ) {
....
}
In the form models, you can add the additional validation for your fields:
public function rules()
{
return [
['body', 'string', 'max' => 300],
['price', 'integer', 'max'=> 1000],
];
}
Using different forms for this I believe is the best option.
3) Your form model fields do not correspond to Database model columns 1 to 1.
Consider this example:
Your want to save the address of the agreement, street, state and city. You have a list of cities, states and streets.
The only thing you want to do with the address is save it and load it, eg. nobody is going to search per city.
So you just define a column address(type text) in your table and save the data as a JSON.
Your AgreementForm defines the address as separate fields and validates them accordingly and your Agreement model just validates address to be a string.
Do not duplicate your validation between AgreementForm and Agreement models. They should define different validation rules.
Note: Even if you do not render the form in a view and just post some data, it is good to use a form model to get exactly the fields you want to change and if needed, validate them with additional rules. The only case that I could think to just use the DB model directly is when you want to provide just some basic crud operations for your model.

How to update a pivot table using Eloquent in laravel 5

I am new to laravel. I am working on a laravel 5 app and I am stuck here. I have 2 models as such:
class Message extends Eloquent{
public function user()
{
return $this->belongsTo('App\User', 'from');
}
public function users()
{
return $this->belongsToMany('App\User')->withPivot('status');
}
}
class User extends Eloquent {
public function messages()
{
return $this->hasMany('App\Message', 'from');
}
public function receive_messages() {
return $this->belongsToMany('App\Message')->withPivot('status');
}
}
There exist a many-to-many relationship between Message and User giving me a pivot table as such:
Table Name: message_user
Colums:
message_id
user_id
status
I have an SQL query as such:
update message_user
set status = 1
where user_id = 4 and message_id in (select id from messages where message_id = 123)
How can I translate this query to the laravel equivalent?
The code below solved my problem:
$messages = Message::where('message_id', $id)->get();
foreach($messages as $message)
$message->users()->updateExistingPivot($user, array('status' => 1), false);
You may use one of these two functions, sync() attach() and the difference in a nutshell is that Sync will get array as its first argument and sync it with pivot table (remove and add the passed keys in your array) which means if you got 3,2,1 as valued within your junction table, and passed sync with values of, 3,4,2, sync automatically will remove value 1 and add the value 4 for you. where Attach will take single ID value
The GIST: if you want to add extra values to your junction table, pass it as the second argument to sync() like so:
$message = Messages::find(123);
$user = User::find(4);
// using attach() for single message
$user->message()->attach($message->id, [
'status' => 1
]);
$message2 = Messages::find(456); // for testing
// using sync() for multiple messages
$user->message()->sync([
$message->id => [
'status' => 1
],
$message2->id => [
'status' => 1
],
]);
Here is a small example of how to update the pivot table column
$query = Classes::query();
$query = $query->with('trainees')
->where('user_id', Auth::id())
->find($input['classId']);
foreach ($query->trainees as $trainee) {
$trainee->pivot->status = 1 //your column;
$trainee->pivot->save();
}
Note: make sure your relation data must in an array
Hope its help you :)
happy coding
Laravel 5.8
First, allow your pivot columns to be searchable by chaining the withPivot method to your belongsToMany
Copied from my own code to save time
// I have 3 columns in my Pivot table which I use in a many-to-many and one-to-many-through scenarios
$task = $user->goalobjectives()->where(['goal_objective_id'=>$goal_objective_id,'goal_obj_add_id'=>$goal_obj_add_id])->first(); //get the first record
$task->pivot->goal_objective_id = $new; //change your col to a new value
$task->pivot->save(); //save
The caveat is that your pivot table needs to have a primary 'id' key.
If you don't want that then you can try the following:
$tasks=$user->posts()->where(['posts_id'=>$posts_id,'expires'=>true])->get()->pluck('id'); // get a collection of your pivot table data tied to this user
$key=join(",",array_keys($tasks->toArray(),$valueYouWantToRemove));
$tasks->splice($key,1,$newValueYouWantToInsert);
$c = array_fill(0,$tasks->count(),['expires'=>true]); //make an array containing your pivot data
$newArray=$tasks->combine($c) //combine the 2 arrays as keys and values
$user->posts()->sync($newArray); //your pivot table now contains only the values you want
4th July Update Update to above snippet.
//Ideally, you should do a check see if this user is new
//and if he already has data saved in the junction table
//or are we working with a brand new user
$count = $user->goalobjectives->where('pivot.goal_obj_add_id',$request->record)->count();
//if true, we retrieve all the ids in the junction table
//where the additional pivot column matches that which we want to update
if($count) {
$ids = $user->goalobjectives->where('pivot.goal_obj_add_id',$request->record)->pluck('id');
//convert to array
$exists = $ids->toArray();
//if user exists and both saved and input data are exactly the same
//there is no need
//to update and we redirect user back
if(array_sum($inputArray) == array_sum($exists)) {
//redirect user back
}
//else we update junction table with a private function
//called 'attachToUser'
$res = $this->attachToUser($user, $inputArray, $ids, $request->record);
}//end if
elseif(!$count) {
//we are working with a new user
//we build an array. The third pivot column must have equal rows as
//user input array
$fill = array_fill(0,count($inputArray),['goal_obj_add_id'=>$request->record]);
//combine third pivot column with user input
$new = array_combine($inputArray,$fill);
//junction table updated with 'user_id','goal_objective_id','goal_obj_add_id'
$res = $user->goalobjectives()->attach($new);
//redirect user if success
}
//our private function which takes care of updating the pivot table
private function attachToUser(User $user, $userData, $storedData, $record) {
//find the saved data which must not be deleted using intersect method
$intersect = $storedData->intersect($userData);
if($intersect->count()) {
//we reject any data from the user input that already exists in the database
$extra = collect($userData)->reject(function($value,$key)use($intersect){
return in_array($value,$intersect->toArray());
});
//merge the old and new data
$merge = $intersect->merge($extra);
//same as above we build a new input array
$recArray = array_fill(0,$merge->count(),['goal_obj_add_id'=>$record]);
//same as above, combine them and form a new array
$new = $merge->combine($recArray);
//our new array now contains old data that was originally saved
//so we must remove old data linked to this user
// and the pivot record to prevent duplicates
$storedArray = $storedData->toArray();
$user->goalobjectives()->wherePivot('goal_obj_add_id',$record)->detach($storedArray);
//this will save the new array without detaching
//other data previously saved by this user
$res = $user->goalobjectives()->wherePivot('goal_obj_add_id',$record)->syncWithoutDetaching($new);
}//end if
//we are not working with a new user
//but input array is totally different from saved data
//meaning its new data
elseif(!$intersect->count()) {
$recArray = array_fill(0,count($userData),['goal_obj_add_id'=>$record]);
$new = $storedData->combine($recArray);
$res = $user->goalobjectives()->wherePivot('goal_obj_add_id',$record)->syncWithoutDetaching($new);
}
//none of the above we return false
return !!$res;
}//end attachToUser function
This will work for pivot table which doesn't have a primary auto increment id. without a auto increment id, user cannot update,insert,delete any row in the pivot table by accessing it directly.
For Updating your pivot table you can use updateExistingPivot method.

Preventing malicious users update data at add action

Here is a basic add action:
public function add()
{
$article = $this->Articles->newEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->data);
if ($this->Articles->save($article)) {
$this->Flash->success('Success.');
return $this->redirect(['action' => 'index']);
} else {
$this->Flash->error('Fail.');
}
}
$this->set(compact('article'));
}
If a malicious user injects at form a field with name id and set the value of this field to 2. Since the user do that the id value will be in $this->request->data so at $this->Articles->patchEntity($article, $this->request->data) this id will be patched and at $this->Articles->save($article) the record 2 will be updated instead of create a new record??
Depends.
Entity::$_accessible
If you baked your models, then this shouldn't happen, as the primary key field will not be included in the entities _accessible property, which defines the fields that can be mass assigned when creating/patching entities. (this behavior changed lately)
If you baked your models, then this shouldn't happen, as the primary key field(s) will be set to be non-assignable in the entities _accessible property, which means that these the fields cannot be set via mass assignment when creating/patching entities.
If you didn't baked your models and haven't defined the _accessible property, or added the primary key field to it, then yes, in case the posted data makes it to the patching mechanism, then that is what will happen, you'll be left with an UPDATE instead of an INSERT.
The Security component
The Security component will prevent form tampering, and reject requests with modified forms. If you'd use it, then the form data wouldn't make it to the add() method in the first place.
There's also the fieldList option
The fieldList option can be used when creating/patching entities in order to specifiy the fields that are allowed to be set on the entity. Sparse out the id field, and it cannot be injected anymore.
$article = $this->Articles->patchEntity($article, $this->request->data, [
'fieldList' => [
'title',
'body',
//...
]
]);
And finally, validation
Validation can prevent injections too, however that might be considered a little wonky. A custom rule that simply returns false would for example do it, you could create an additional validator, something like
public function validationAdd(Validator $validator) {
return
$this->validationDefault($validator)
->add('id', 'mustNotBePresent', ['rule' => function() {
return false;
}]);
}
which could then be used when patching the entity like
$article = $this->Articles->patchEntity($article, $this->request->data, [
'validate' => 'add'
]);