Attach existing record to eloquent hasMany / belongsTo relationship - laravel

I have a model SalesArea which hasMany Regions. Regions belongTo one SalesArea.
class SalesArea extends Model
{
public function regions() {
return $this->hasMany('App\Region');
}
}
class Region extends Model
{
public function sales_area() {
return $this->belongsTo('App\SalesArea');
}
}
Due to some changes in organization, the parent (SalesArea) is being created after the Regions have been created. I am trying to associate the Regions with their associated Sales areas when the Sales Area record is created.
I created a migration to add sales_area_id to the regions table, and they are currently all null.
I have a select box that has the various IDs of Regions and I would like to associate the regions with a Sales Area at once with an array of ids. None of the association methods are working.
$area = SalesArea::create($request->all());
$area->regions()->attach([1,2,3]); // BadMethodCallException with message 'Call to undefined method Illuminate\Database\Query\Builder::attach()
$area->regions()->add([1,2,3]); // BadMethodCallException with message 'Call to undefined method Illuminate\Database\Query\Builder::add()'
$area->regions()->sync([1,2,3]); // BadMethodCallException with message 'Call to undefined method Illuminate\Database\Query\Builder::sync()'
$area->regions()->associate([1,2,3]); // BadMethodCallException with message 'Call to undefined method Illuminate\Database\Query\Builder::associate()'
I am stumped on how this is supposed to work. Regions already exist, I just need to add their relationships to their associated sales area.

public function store(Request $request)
{
$this->validate($request, [
'name' => 'required|unique:sales_areas',
'regions' => 'required|array',
'regions.*' => 'int',
]);
$salesArea = SalesArea::create($request->all());
// Because we have a hasMany not a many to many, we have to do it this way
Region::whereIn('id', $request->regions)->update(['sales_area_id' => $salesArea->id]);
flash('Created Sales Area ' . $salesArea->name);
return redirect()->route('areas.index', $salesArea->id);
}

Please ensure you have the column, sales_area_id in your regions table
$salesArea = SalesArea::create($request->all());
$regions = Region::whereIn('id', [1, 2, 3])->get();
$salesArea->regions()->saveMany($regions);
I am not sure, why you are creating the regions this way. If there is a hasMany()relation between SalesArea & Region then all regions should be created like :
$salesArea->regions()->create([ ... ]);
And if your relations are like this
class SalesArea extends Model
{
public function regions() {
return $this->belongsToMany(Region::class)->withPivot(['id'])->withTimestamps();
}
}
class Region extends Model
{
public function sales_area() {
return $this->belongsToMany(SalesArea::class)->withPivot(['id'])->withTimestamps(
}
}
then you can do something like this.
$salesArea = SalesArea::create($request->all());
$regions = Region::whereIn('id', [1, 2, 3])->get();
$salesArea->regions()->sync($regions->pluck('id')->toArray());

Related

How to create a hasMany relationship with different models in folder?

I have a strange Laravel 9 setup due to being constrained to a very ancient database.
I'm trying to come up with a clean way to create a hasMany relationship to multiple models located in a folder. I believe it would be easiest explained with diagrams:
app/Models/
- Customer
app/Models/Records/
- Orange
- Green
- Blue
Now, all of these records has some connection to customer, but these are all different as well, for example, orange refers to customer using cid, where Green might use customerid.
I've already set up logic where every record model has a customer belongsTo relationship depending on the different field names.
public function customer()
{
return $this->belongsTo('App\Models\Customer', 'CustomerId');
}
I need to create a records() function in the Customer model, that pulls in all of these Records where found. I can't create any new tables that would be stored on the DB.
For multiple tables (models) to be connected as hasMany is probably not possible out of the box in Laravel.
However if you do really need to combine them all, I did once but it is not pretty.
First add following to each of your App\Records\Model in example is Green
class Green extends Model
{
protected $table = 'GreenRecords';
....
....
// this is to append/add extra fields into your model
// that not exist inside the table it self
protected $appends = [
'product_data',
'stuffs',
];
// relationship to App\Models\Customer
public function customer()
{
return $this->belongsTo(Customer::class, 'customerid', 'id');
}
// you can use it later as Green::query()->customer($id)->get()
public function scopeCustomer($query, $id)
{
return $query->where('customerid', $id);
}
// you can add here as many data as you like from table GreenRecords,
// this will make Green Orange and Blue as if they have common fields
// you can also separate them by adding add each custom field to $appends
public function getProductDataAttribute()
{
return [
'name' => $this->name,
'color' => $this->color,
'stuffs' => $this->stuffs,
'other_stuffs' => $this->other_stuffs,
];
}
public function getStuffsAttribute()
{
return $this->stuffs;
}
}
And now for the Customer model
class Customer extends Model
{
// your Customer relation to Orange
public function oranges()
{
return $this->hasMany(Orange::class, 'cid', 'id');
}
// your Customer relation to Green
public function greens()
{
return $this->hasMany(Green::class, 'customerid', 'id');
}
public function getRecords(): Collection
{
// we can not use collection merge due to same 'id' in Orange and Green
$collections = collect();
if ($oranges = Orange::query()
->customer($this->id)
->get()
) {
foreach ($oranges as $record) {
$collections->push($record);
}
}
if ($greens = Green::query()
->customer($this->id)
->get()
) {
foreach ($greens as $record) {
$collections->push($record);
}
}
return $collections;
}
}
So now you can do
$customer = Customer::find(3);
$records = $customer->getRecords(); // result will be a collection
And inside your blade you can access them
#foreach ($records as $record)
{{ $record->stuffs }}
{{ $records->product_data['other_stuffs'] }}
#endforeach
A collection can be filtered and sorted
$records->where('product_data.stuffs', 'abc')->sortBy('name');
The only problem here is the records id where Orange can have the same id as Green and Blue.
Best is to add new field into Orange Green and Blue
$table->uuid('record_id')->nullable();
Hope this can help you out.
None of these solutions quite achieved what I was looking for in terms of simplicity, and in my case my database is quite out of date and slow, so I ended up landing on a solution that is quite simple and faster than everything posted for my use case:
public function records()
{
return [
"Orange" => $this->hasMany(\App\Models\Records\Orange::class, 'CustomerId', 'Id')->get(),
"Blue" => $this->hasMany(\App\Models\Records\Blue::class, 'Customerid', 'Id')->get(),
"Purple" => $this->hasMany(\App\Models\Records\Purple::class, 'customerid', 'Id')->get(),
"Black" => $this->hasMany(\App\Models\Records\Black::class, 'CustomerId', 'Id')->get(),
"Green" => $this->hasMany(\App\Models\Records\Green::class, 'Customerid', 'Id')->get(),
"Cyan" => $this->hasMany(\App\Models\Records\Cyan::class, 'CustomerId', 'Id')->get()
];
}
This achieves what I was looking for in terms of setting up the relationships when the customerId field was subject to change on the other tables, and is pretty readable overall.

Laravel polymorphic type not stored correctly

I'm using Backpack to create an Admin Panel for my project. I have a SoldProduct model (basically a physical item that has been sold to a customer), a Shoe model and a Sweatshirt model.
There is a polymorphic relationship between them in order to have a separate table for each one of them, avoiding the repetition of the columns that they share and also avoiding the (kinda ugly) solution of storing them ina big table that has got all the necessary fields that would remain partially null based on the type of product it's being stored.
Since I'm using Backpack's CRUD, I created a custom store method that creates the specific types of product and then creates a SoldProduct object linked to the specific one (by filling the productable_type ansd productable_id fields).
The problem is that while productable_id is stored correctly, in the productable_type field instead of storing for example "App\Models\Shoe" I keep getting "App\Models\SoldProduct" (the parent model's name), and I don't know where it's getting it from. The data is passed correctly to the "final" store method, but in the process of storing it gets modified in "App\Models\SoldProduct".
Any clue on why this might be happening? Thanks everybody.
Here's my code
public function store(Request $request) {
$this->crud->setRequest($this->crud->validateRequest());
$productable_type = $request->request->get('productable_type');
switch ($productable_type) {
case "Shoe":
$product = new Shoe;
$product->imprinted_code = $request->request->get('imprinted_code');
$product->size = $request->request->get('shoe_size');
$product->sole = $request->request->get('sole');
$product->save();
$product_id = $product->id;
$this->crud->addField(['type' => 'hidden', 'name' => 'productable_id']);
$this->crud->getRequest()->request->add(['productable_id' => "$product_id"]);
$this->crud->getRequest()->request->add(['productable_type' => "App\Models\\$productable_type"]);
break;
// repeat for Sweatshirt too
}
$this->removeField('imprinted_code', $request);
$this->removeField('shoe_size', $request);
$this->removeField('sole', $request);
$this->removeField('sweatshirt_size', $request);
$this->removeField('shirt_size', $request);
$this->removeField('pants_size', $request);
$this->crud->unsetValidation();
return $this->traitStore();
}
sold_products_table
Schema::create('sold_products', function (Blueprint $table) {
$table->id();
$table->foreignId('product_model_id')->constrained('product_models');
$table->foreignId('owner_id')->nullable()->constrained('users')->onUpdate('cascade');
$table->integer('productable_id')->nullable();
$table->string('productable_type')->nullable();
$table->string('note')->nullable();
$table->timestamps();
});
SoldProduct
public function productable() {
return $this->morphTo();
}
Shoe
public function product() {
return $this->morphOne('App\Models\SoldProduct', 'productable');
}
Sweatshirt
public function product() {
return $this->morphOne('App\Models\SoldProduct', 'productable');
}

HasMany Relation through BelongsToMany Relation

Is it possible to make Laravel relation through belongsToMany relations?
I have 4 tables:
1)Restaurants (id , name) - uses hasManyRelation with Workers table
2)Directors (id , name)
3)Directors_Restaurants (id, director_id, restaurant_id) - pivot table for connecting belongsToMany Restaurants with Directors
3)Workers (id, name, restaurant_id)
With this function in Directors model i can get all connected restaurants
public function restaurants()
{
return $this->belongsToMany('App\Restaurant','director_restaurant');
}
With this function in my code i can get all workers of all restaurants of one director
$director = Director::find(1);
$director->load('restaurants.workers');
$workers = $director->restaurants->pluck('workers')->collapse();
So my question is : can i declare similar relation in my Director model to get all its workers of all its restaurants?
Of course you can have hasMany relationship method on Director model with Eager Loading
just like below
public function restaurants()
{
return $this->hasMany(Restaurant::class)->with('restaurants.workers');
}
i can suggest a solution like this:
Director Model OPTION 1
public function getAllRestaurants(){
return $this->hasMany(Restaurant::class)->with('restaurants.workers');
}
Director Model OPTION 2
public function getAllRestaurants(){
$this->load('restaurants.workers');
return $this->restaurants->pluck('workers')->collapse();
}
You can get all restaurants anywhere
$all_restaurants = Director::find(1)->getAllRestaurants();
You can define a direct relationship by "skipping" the restaurants table:
class Director extends Model
{
public function workers()
{
return $this->belongsToMany(
Worker::class,
'director_restaurant',
'director_id', 'restaurant_id', null, 'restaurant_id'
);
}
}
You can define an accessor method in your model to hide some of the logic
# App/Director.php
// You'll need this line if you want this attribute to appear when you call toArray() or toJson()
// If not, you can comment it
protected $appends = ['workers'];
public function getWorkersAttribute()
{
return $this->restaurants->pluck('workers')->collapse();
}
# Somewhere else
$director = Director::with('restaurants.workers')->find(1);
$workers = $director->workers;
But ultimately, you still have to load the nested relationship 'restaurants.workers' for it to work.
Given your table attributes you could also define a custom HasMany relationship that looks like this
# App/DirectorRestaurant.php
public function workers()
{
return $this->hasMany(Worker::class, 'restaurant_id', 'restaurant_id');
}
# Somewhere else
$director = Director::find(1);
$workers = DirectorRestaurant::where('director_id', $director->id)->get()->each(function($q) { $q->load('workers'); });
But I don't recommend it because it's not very readable.
Lastly, there's the staudenmeir/eloquent-has-many-deep package where you can define that sort of nested relationship.
https://github.com/staudenmeir/eloquent-has-many-deep

Problem in inserting data into many to many relationships using attach() function laravel

i have two tables (orders & products) and one pivot table (order_product).
i have made many to many relationship b\w them using following code.
class Product extends Model{
protected $fillable = ['name', 'price', 'is_iframe_product'];
public function orders() {
return $this->belongsToMany(Order::class);
}
}
class Order extends Model{
public $guaded = ['id'];
protected $fillable = ['is_iframe_order','price', 'status', 'address_id','user_id'];
public function products () {
return $this->belongsToMany(Product::class);
}
}
i am using following code to insert records in CheckoutController.php
$Product = array('name' => $item['item_name'], "price" => $item['price'], "is_iframe_product" => "1");
$saved = order()->products()->attach([$Product]);
but getting this error:
exception: "Symfony\Component\Debug\Exception\FatalThrowableError"
file: "C:\wamp3\www\jewellery\jewellery\app\Http\Controllers\CheckoutController.php"
line: 63
message: "Call to undefined function App\Http\Controllers\order()"
Here's what to do:
First save the product into the database
$product = Product::create(array('name' => $item['item_name'], "price" =>$item['price'], "is_iframe_product" => "1"));
Then attach the saved product with the product's id
$saved = $order->products()->attach($product->id);
What you need to do is create the order first:
$order = new Order;
// set some attributes
$order->save();
Then you can attach the specified product:
$product = Product::find(1);
$order->products()->attach($product->getKey());
If you are creating the product on the fly:
$product = Product::create(array('name' => $item['item_name'], "price" =>$item['price'], "is_iframe_product" => "1"));
$order->products()->attach($product->getKey());
You're using a function: order()->...
And the error says the function doesn't exist: Call to undefined function ... order()
Did you mean to reference a variable, like this? $order->...
It would be helpful if you included the rest of the controller, since I don't know what variables you're using.
Also, your Order model has a typo: $guaded should be $guarded.

Find Records Based on Distant belongsToMany > belongsTo Relationship

I'm attempting to display a product page with a list of assets that match a specific asset type. For example, for product "Acme Cream", there are two assets: nutrition-facts.pdf (of type Document) and marketing-video.mp4 (of type Video). On the product page, I'd like to display the first asset that match the 'Video' asset type (if any exist).
I have the following relationships:
The Product model includes a DB column asset_id.
class Product extends Model
{
/**
* The assets that belong to the product.
*/
public function assets()
{
return $this->belongsToMany('App\Asset', 'product_asset');
}
}
The Asset model includes DB columns id and asset_type_id.
class Asset extends Model
{
/**
* Get the asset type that owns the asset.
*/
public function asset_type()
{
return $this->belongsTo('App\AssetType');
}
/**
* The products that belong to the asset.
*/
public function products()
{
return $this->belongsToMany('App\Product', 'product_asset');
}
}
The AssetType model has two DB columns id and name.
class AssetType extends Model
{
/**
* A asset type can have many assets
*/
public function assets()
{
return $this->hasMany('App\Asset');
}
}
How can I efficiently fetch the one product asset by filtering on asset_type? Keep in mind, I've already queried the DB using Product::find(id) and passed that data into the view. Will this require another query (eager loading might help with that). I know I could use a foreach loop, but it seems to me there's gotta be a nicer, more 'eloquent' way.
I'm trying to use it in this situation (pseudo code) on the product detail page (show.blade.php):
if assets matching type 'Video', then display first in this div. Else, don't display the div.
It seems like it should be a simple line:
$product->assets()->asset_type()->where('name', '=', 'Video')->first()
The closest I've come so far to this is this ugly looking thing:
>>> $product = App\Product::with(['assets'])->find(1)
>>> $product->assets()->with(['asset_type' => function ($query) { $query->where('name', '=', 'Video'); }])->get()
However, it still returns all assets, except the "asset_type" attribute is null for those that don't match. Adding ->whereNotNull('asset_type')->get() only results in an error that asset_type column cannot be found.
Also, this sounds like a chance to use the "Has Many Through" relationship, but I'm unclear how to set this up.
Any help is greatly appreciated! Thanks.
You need to eager-load your relationship with filtering:
Assuming you fetch the relationship with your product info
$typeName = 'Video';
$product = App\Product::with([
'asset' => function($query) use($typeName) {
//Filter asset by type
//See: https://laravel.com/docs/5.6/eloquent-relationships#constraining-eager-loads
return $query->whereHas('asset_type',function($query) use($typeName) {
//Filter where the type's name equals..
//Each query is relative to its scope, in this case the 'type' relationship which refers to your 'type' Model
return $query->where('name','=',$typeName);
});
},
//Nested relationship loading: https://laravel.com/docs/5.6/eloquent-relationships#querying-relations
'assets.asset_type'
])
->find(1);
$assets = $product->assets;
Assuming you fetch only the assets
$productId = 1;
$typeName = 'Video';
//Returns a collection of eloquent models
$assets = Asset::whereHas('product',function($query) use ($productId) {
//Filter product by its id
return $query->where('id','=',$productId);
})
->whereHas('asset_type',function($query) use ($typeName) {
//Filter type by its name
return $query->where('name','=',$typeName);
})
->get();

Resources