Laravel issue with updating hasMany relationship records - php

I have a User model which has a hasMany relationship to a Brands model and I am having issues updating the Brands for a user properly.
I have a form which allows a user to enter / delete / update their own Brands in text fields, the current method i am using to update the users Brands after they have entered them all or edited them is to delete all existing Brands associated with the User then loop over the values, create the Brand model and then 'saveMany' ... But i seem to be getting a constraint violation when adding ... I am wondering if there is a better way to do this;
My User model has the following;
public function brands()
{
return $this->hasMany('Brands::class');
}
Then in my controller I have the following code to update the Brands;
$user->brands()->delete();
foreach ($request['brands'] as $brand) {
$brandArray[] = new Brand([
'name' => $brand['name'],
'rating' => $brand['rating'],
]);
}
!empty($brandArray) && $user->brands()->saveMany($brandArray);
Is there a better way of doing this?

Let's separate things into three parts:
# constraint key violation:
If you've added foreign key constraint on another table, and you need to delete the brands, you should also delete all those related data constrained by your foreign key.
# design
If deleting brand related data is not possible, then maybe we can think about if there is a better design. Maybe we could add a hook on the frontend that call a DELETE API whenever certain data is removed by the user.
# query
If the brand has some unique key, you could use upsert instead of saveMany. That will be more efficient.
# conclusion
I would suggest deleting brands by hooks on the frontend whenever a brand is removed by users, and use upsert to deal with create and update stuff

It looks fine to me. But from my point of view,
Instead of deleting all fields, then creating them. You can use updateOrCreate eloquent method inside your foreach.
And in place of foreach, you can use the map method.

Since you only want to delete all the previous brands of the user, and create brand new brands without editing anything, you can simply follow the concept below:
In your controller:
// Step 1) load all the user brands
$user->load('brands');
// Step 2) delete all the brands with this user
$user->brands()->delete();
// Step 3) create all the new brands.
$user->brands()->createMany($request['brands']);
/* Concept of how createMany works:
The brand array should look similar to this, you can do dd($request['brands']) to verify
$user->brands()->createMany([
['name' => 'A new name.', 'rating' => '1'],
['name' => 'Another new name.', 'rating' => '2'],
]);
*/
You can find more examples in laravel documentation on createMany method: https://laravel.com/docs/9.x/eloquent-relationships#the-create-method
You can also go a step further and validate the array from the request:
$data = request()->validate([
'brands.*' => 'required',
]);
$user->brands()->createMany($data['brands']);
Hope this helps, good luck!

When you save everything you have for the user, do this under:
foreach ($request['brands'] as $brand) {
Brand::updateOrCreate(
[
'name' => $brand['name'],
'rating' => $brand['rating'],
],
[
'name' => $brand['name'],
'rating' => $brand['rating'],
]
);
}

Related

Validation in Laravel on relationship

In Laravel, I have a persons model that has a many-to-many relationship with its group. The person's name needs to be unique in its group but not on the persons table. How would ensure that?
My validation is done in the controller store method using $request->validate(['name => ...
I currently save the new person in a controller using simply - Person::create([...
My simple approach is using a composite primary keys on pivot table and use basic exception handling like try catch stuff whenever inserting data is fail due to migration
$table->foreignId('group_id') // Add any modifier to this column
$table->foreignId('person_id') // Add any modifier to this column
$table->primary(['group_id', 'person_id']);
If you want to do it on controller, make sure to setup relationship. Then just use Rule::notIn() Validation
'name' => [
'required',
Rule::notIn(/* put your logic here */),
],
You can use 'exist' rule in Laravel Validation like that:
'name' => 'exists:group,name,person_id,'.$id
For more info you can check here:
https://laravel.com/docs/9.x/validation#rule-unique

Using Codeigniter's Model - issue with saving according to primary key

I have been looking for a solution that would replicate MySQL's INSERT ... ON DUPLICATE KEY UPDATE in my Codeigniter model, which led me to the save() function.
I get the logic of it, and my script works fine... but for UPDATING only.
What I've realised now is that my data array is formed from info pulled from an external datafeed, and includes both new AND existing products. There will ALWAYS be a sku (primary key) included in for every product, therefore the save function will ALWAYS look to update the corresponding row in my database - but never insert it - hence, no new objects are ever added to my database - only updated.
// Defined as a model property
$primaryKey = 'sku';
// Does an insert()
$data = [
'brand' => 'Heinz',
'name' => 'Baked Beans'
];
$userModel->save($data);
// Performs an update, since the primary key, 'sku', is found.
$data = [
'sku' => xrt567ycr,
'brand' => 'Heinz',
'title' => 'Baked Beans'
];
$userModel->save($data);
What is the easiest way to get around this and have the script either insert or update based on whether the sku is actually present in the database already or not? All help greatly appreciated!

Using Eloquent's ORM to insert usermeta.id into posts.usermeta_id

I'm working on a user-generated content blog that allows a user to go through the whole upload process before being prompted to sign up. Basic flow: fill out form to pick username/basic info->upload blog post->prompt to sign up with email/password. The purpose of reversing the normal flow is to increase the UX and conversion rate and avoid a wall in the beginning.
Instead of migrating, I've just created the tables manually in PHPmyAdmin. I have 3 relational models: Usermeta->hasOne(App\Mopdels\Post), Post->belongsTo(App\Models\Usermeta), and User->belongsTo(App\Models\Usermeta).
What I'm having trouble with is once the user has created a username and submits the first form to the usermeta table, and then submits the second form to upload their blog post to the post table, it doesn't seem to be attaching the usermeta.id to posts.usermeta_id linking them together. I must be missing something or not attaching it correctly. Here's my StoryController:
<?php
namespace App\Controllers\Story;
use App\Models\Post;
use App\Models\User;
use App\Models\Usermeta;
use App\Controllers\Controller;
use Respect\Validation\Validator as v;
class StoryUploadController extends Controller
{
public function guidance($request, $response)
{
return $this->view->render($response, 'storyupload/guidance.twig');
}
//set up our the Upload Story class so the user can upload their story
//render the view 'uploadstory.twig'
public function getStoryUpload($request, $response)
{
return $this->view->render($response, 'storyupload/upload.twig');
}
// This method is called when the user submits the final form
public function postStoryUpload($request, $response, $id)
{
//set up our validation rules for our complete sign up form
$validation = $this->validator->validate($request, [
'title' => v::stringType()->notEmpty()->length(1, 80),
'body' => v::stringType()->notEmpty()->length(1, 2500),
]);
//if validation fails, stay on story upload page
if ($validation->failed()) {
return $response->withRedirect($this->router>pathFor('storyupload.upload'));
}
$user = Usermeta::find($id)->first();
//We can use our Post Model to send the form data to the database
$post = Post::create([
'title' => $request->getParam('title'),
'body' => $request->getParam('body'),
'category' => $request->getParam('category'),
'files' => $request->getParam('img_path'),
'usermeta_id' => usermeta()->attach($user->id),
]);
//after submit, redirect to completesignup page
return $response->withRedirect($this->router->pathFor('auth.completesignup'));
}
}
I continue to get the error 'usermeta_id cannot be null' so it's definitely not pulling the id from the usermeta table correctly.
I've used the create() method to send the usermeta data to the table in my Auth controller.
Would it be better to have all of my form submissions in the Auth controller and what is the proper way using my example to make sure that my posts.usermeta_id is linked to my usermeta.id?
The usermeta form is taken care of by my Auth Controller:
//render the view 'signup.twig'
public function getSignUp($request, $response)
{
return $this->view->render($response, 'auth/signup.twig');
}
// This method is called when the user submits the form
public function postSignUp($request, $response)
{
$validation = $this->validator->validate($request, [
'name' => v::notEmpty()->alpha(),
'username' => v::noWhitespace()->notEmpty()->UsernameAvailable(),
'city' => v::notEmpty()->alpha(),
'country' => v::notEmpty()->alpha(),
]);
//if validation fails, stay on signup page
if ($validation->failed()) {
return $response->withRedirect($this->router->pathFor('auth.signup'));
}
$usermeta = Usermeta::create([
'name' => $request->getParam('name'),
'username' => $request->getParam('username'),
'city' => $request->getParam('city'),
'country' => $request->getParam('country'),
'share_location' => $request->getParam('share_location'),
]);
//after submit, redirect to storyupload/guidance
return $response->withRedirect($this->router>pathFor('storyupload.guidance'));
}
I wrote quite a bit here. To jump directly to what I believe will solve your problem, see the "Your Issue" section. The rest is here as an educational exercise.
A Quick Intro to Laravel Relations
As you probably already know, "relations" in Laravel are virtual concepts that are derived from the hard data in the database. Because they are virtual, there is some overlap in the definition of relations.
When you say "Usermeta has one Post" - what this means is that the posts table will have a usermeta_id field.
When you say "Post belongs to Usermeta" - what this means is that the posts table will have a usermeta_id field.
Notice that these two relations map to the exact same field in the exact same table. Declaring one relation will declare the other by simple congruence. "Usermeta has one Post" and "Post belongs to Usermeta" are identical relations.
A Tweak to Your Relations
There's one other relation that share this same schema (the posts table have a usermeta_id field). That is "Usermeta has many Posts". The difference here is not in how the relations are stored to the database, but in how Laravel interprets the relations and in what queries Laravel will run.
When you say "Usermeta has one Post", Laravel will scan the database for the first Post with a matching usermeta_id and return that as an instance of the Usermeta model.
When you say "Usermeta has many Posts", Laravel will scan the database for all matching usermeta_ids and return them as a Collection of Usermeta models. You likely want this second behavior -- otherwise users won't be able to make a second post after they sign up.
Setting the usermeta_id Field
Laravel allows you to set database fields directly through a relationship. See their documentation on inserting related models for details.
Because many relationships are just ciphers for the same underlying schema, there's no need to insert or update a related model both ways. For instance, suppose we had the following two models:
class User extends Eloquent {
public function posts() {
return $this->hasMany("App\Post");
}
}
class Post extends Eloquent {
public function user() {
return $this->belongsTo("App\User");
}
}
In this case, the following two lines of code are identical and you only need to use one of them:
$post->user()->associate($user);
$user->posts()->save($post);
Both of these will have the same effect (setting the user_id field on the posts table)
The reason I mention this is that it looks like you're trying to double-dip in your code. You're using attach() (conceivably to set the usermeta_id) and you're also setting the usermeta_id directly. I've added a side-note on the attach method below - as I don't believe it's the right method, anyway.
To use Laravel's relations, you would want code like the following to set this field:
public function postStoryUpload($request, $response, $id)
{
//set up our validation rules for our complete sign up form
$validation = $this->validator->validate($request, [
'title' => v::stringType()->notEmpty()->length(1, 80),
'body' => v::stringType()->notEmpty()->length(1, 2500),
]);
//if validation fails, stay on story upload page
if ($validation->failed()) {
return $response->withRedirect($this->router>pathFor('storyupload.upload'));
}
$user = Usermeta::find($id)->first();
//We can use our Post Model to send the form data to the database
$post = Post::create([
'title' => $request->getParam('title'),
'body' => $request->getParam('body'),
'category' => $request->getParam('category'),
'files' => $request->getParam('img_path'),
]);
// Set the usermeta_id field
$post->usermeta()->associate($user);
// Save the model so we write changes to the database
$post->save();
//after submit, redirect to completesignup page
return $response->withRedirect($this->router->pathFor('auth.completesignup'));
}
Manually Setting the usermeta_id Field
Instead of using Laravel's relations to set this field, you can set the field manually. This can sometimes be cleaner, but it's less explicit and can lead to minor bugs if you aren't careful. To do this, you need to treat the usermeta_id field like any other field on your model.
$post->usermeta_id = $user->id;
This also works when mass assigning attributes using fill or create like so:
$post = \App\Post::create([
'title' => $title,
'body' => $body,
'usermeta_id' => $user->id
]);
$post->fill([
'title' => $title,
'body' => $body,
'usermeta_id' => $user->id
]);
Note that when manually setting the usermeta_id like this, you do not need to use any relationship methods. The following code is redundant:
$post->usermeta_id = $user->id;
$post->usermeta()->associate($user);
Your Issue (I Believe)
There's a caveat to mass assignment, however. Per the Laravel documentation, mass assignment requires you to fill out the model's fillable or guarded attributes.
This is one of the most common bugs, if not the most common bug, in any Laravel code - and it doesn't throw an obvious error so it's easy to miss. Consider the following model:
class Post extends Eloquent {
private $fillable = ["title", "body"];
}
If you attempt to mass assign the usermeta_id field like so:
$post = \App\Post::create([
'title' => $title,
'body' => $body,
'usermeta_id' => $user->id
]);
Then it will silently fail. No error is thrown and the Post is created but the usermeta_id field will be NULL - because it's not mass assignable. This is fixed by updating your model like so:
class Post extends Eloquent {
private $fillable = ["title", "body", "usermeta_id"];
}
I will repeat again, as I did above, that if using mass assignment like this you do not not need to use the associate or save relationship methods. This would be redundant. Therefore you can just set usermeta_id directly to $user->id without any of the usermeta()->associate() shenanigans.
The Bugs I Mentioned
I mentioned that manually setting the field like this can cause bugs. So let's actually discuss what some of those bugs are now instead of glossing over them.
If you update the relationship field manually, Laravel will be unaware that the two models are related until it reloads the model from the database. Consider the following two chunks of code:
$post = new Post();
$post->usermeta_id = $user->id;
dd( $post->usermeta->name );
$post = new Post();
$post->usermeta()->associate($user);
dd( $post->usermeta->name );
The first code block will fail, throwing the error "cannot read attribute of null object" -- because as far as Laravel is aware, $post->usermeta is NULL. You set $post->usermeta_id, but you didn't set $post->usermeta.
The second code block will work as expected, because by running the associate function it sets both usermeta_id and usermeta.
95% of the time this doesn't really cause any issues, however. If you're using an asynchronous API call to save the post and then a separate asynchronous API call to read the post at a later time, then Laravel will read the post from the database and properly set up the relation automatically when we sees the usermeta_id field is filled out.
Side-note On the attach() Method
Laravel uses different methods for saving different types of relations - because the different relations imply different underlying database fields.
associate: This sets the *_id field on the current model's table. For instance: $post->user()->associate($user) will set the user_id on the posts table
save: This sets the *_id field on the other model's table. For instance: $post->comments()->save($comment) will set the post_id on the comments table
attach: This sets both *_id fields on a linking table for many to many relationships. For instance, if you had a tag system then $post->tags()->attach($tag) would set post_id and tag_id on the post_tags table
It can be a bit tricky to remember which of these three functions you need. In general, there's a direct mapping from relation to function:
hasOne, hasMany --> save
belongsTo --> associate
belongsToMany --> attach

Changing own id in pivot table

I'm developing a Laravel application where I have a posts table, a tags table and a post_tag table which acts as a pivot table.
Now I need to give all the tags from a post to another post. In other words I need to make:
$tags = $post->tags;
And change the post_id to each record in the pivot table. I have all the relationships already set.
EDIT: this is my code
class Post extends Model
{
public function tags()
{
return $this->belongsToMany(Tag::class)->withPivot('is_active')->withTimestamps();
}
}
class Tag extends Model { }
The main problem is that I have to keep the is_active value as it is. I just need to replace the post_id from the pivot table where post_id equals the one I want to override (I know I could make a raw query but I'm trying to avoid it)
EDIT 2:
I made it work this way but I still prefer an object oriented way
DB::table('post_tag')->where('post_id', $post_a->id)->update(['post_id' => $post_b->id]);
You can use the method updateExistingPivot of Eloquent, from the laravel documentation:
If you need to update an existing row in your pivot table, you may use updateExistingPivot method. This method accepts the pivot record foreign key and an array of attributes to update:
$user = App\User::find(1);
$user->roles()->updateExistingPivot($roleId, $attributes);
Try something like
$tags = $post->tags;
//convert tags to IDs only for upcoming steps
... //an array of IDs
$post->tags()->sync($tags); //remove the tags from this post
$post2->tags()->sync($tags); // add the tags to this post
This should get you to the right tracks.
Update
If it was only one, this would do
$post2->tags()->attach($tag, ['is_active' => true]);
$post2->tags()->->sync([1 => ['is_active' => true], 2 => ['is_active' => true]);
You can try to adapt the example above.
But I have no idea how to do it with an array of IDs.

Adding fixtures with its associations in Cakephp 3.x

after I did some research, I didn't find any proper answer to this question.
I'm starting writing tests for my CakePHP (3.x) app and I was wondering how can we add fixtures and their associations?. In other words, how can we link a fixture to another without writing the Primary and foreign keys directly inside the fixture.
i.e
I have a Users table and a UserProfiles table.
User hasOne UserProfile through user_id column. Here's my user fixture.
namespace App\Test\Fixture;
use Cake\TestSuite\Fixture\TestFixture;
class UsersFixture extends TestFixture
{
public $import = ['table' => 'users'];
public $records = [
[
'username' => 'mark#mail.com',
'primary_role' => 1
'is_active' => 1,
]
];
}
And here's my UserProfile fixture
namespace App\Test\Fixture;
use Cake\TestSuite\Fixture\TestFixture;
class UserProfilesFixture extends TestFixture
{
public $import = ['table' => 'user_profiles'];
public $records = [
[
'first_name' => 'Mark',
'last_name' => 'Yellowknife',
]
];
}
How do I link both record together? Aka, how can I tell the user profile record to add its user_id to be linked to the user record?
Must be a way to do this with Fixtures without writing the keys manually.
Thanks guys!
You have to define this in the fixtures. The only other way would be to issue update queries at runtime to set the foreign keys, but that's anything but a good idea.
Other than that there isn't really a way to do this, I mean, how would any code know which records needs to be associated with each other without you explicitly defining it?
So, you'll have to at least define the foreign key, and depending on how primary keys are being generated (auto incrementing integers, UUIDs, etc), you might have to define them too.

Categories