Laravel update hasMany relationship using saveMany - php

As the title suggests, i'm trying to update a hasMany() relationship. I have a Contacts and a ContactsProperties model in my application.
Contacts can have many properties associated with them.
My models are setup like so:
Contacts Model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Contacts extends Model
{
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'contacts';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'contact_name', 'first_name', 'last_name', 'email', 'telephone', 'created_at', 'updated_at'
];
/**
* Get properties associated with contact
*/
public function properties()
{
return $this->hasMany(ContactsProperties::class, 'contact_id');
}
}
ContactsProperties Model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ContactsProperties extends Model
{
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'contacts_properties';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'property_type', 'contact_id', 'address_line_1', 'address_line_2', 'city', 'county', 'postcode'
];
/**
* Get the contact that is assigned to the properties
*/
public function properties()
{
return $this->belongsTo(Contacts::class);
}
}
I can create new properties for a contact by doing:
$contact->properties()->createMany($request->address);
Which works perfectly.
My problem is when I want to update the properties, when editing the contact. I have tried this:
$contact = Contacts::find($request->id);
// Update properties
$contact->properties()->saveMany($request->address);
Which gives the following error:
Type error: Argument 1 passed to Illuminate\Database\Eloquent\Relations\HasOneOrMany::save() must be an instance of Illuminate\Database\Eloquent\Model, array given, called in /var/www/MyApp/2016/public/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php on line 237
The value of $request->address is an array:
Array
(
[0] => Array
(
[address_line_1] => test
[address_line_2] => Lane
[city] => Test
[county] => test
[postcode] => test
[property_type] => site
)
)
Does anyone know how to achieve what I am trying to do?
Thank you in advance.

As error says it expects model instance, not an array. Try to create a ContactsProperties instance from that request input, then you able to save it, f.e.
$contactProperties = new ContactProperties($request->address);
$contact->properties()->saveMany($contactProperties);

The saveMany() methods takes a collection of models as the parameter (see the documentation). You cannot pass it an associative array. The error you received in from the save() method, which is called once for each model in the collection, and expects a Model as the parameter.
The saveMany() method will insert new rows for new models and update rows for existing models.
It seems that that you are saving one ContactProperties model at a time, in which case you could call the save() method directly, instead of saveMany(), but you need to pass it a ContactProperties model, not the associative array from the request.
$contactProperties = new ContactProperties($request->address);
$contact->properties()->save($contactProperties);

Related

Laravel nested resource retrieve model by slug instead of id

I'm working on a Laravel 8 project, it's being used as an API for a frontend. My API contains Brands and Forms, a brand is created by a user and a brand can have a form.
My brand schema contains a slug column, it's this column that's present in my front-end URLs, the slug column is unique, e.g:
/account/brands/my-brand/forms/
/account/brands/my-brand/forms/create/
A form has a brand_id column, this is later used as part of Laralve's hasOne and hasMany relationship for automatic joining since it's easier this way and means I don't have to have an ugly URL.
The trouble I'm having is when I want to show the user a list of their forms for the brand they're on I don't have access to the brand_id in the request, only the slug as this is part of the URL, whereas to retrieve my forms I need the brand_id.
How could I easily (without having another function) obtain this whilst still ensuring my relationships in my model work correctly?
Retrieving forms for a brand that belong to a user
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index($brand)
{
// $brand -> this is the slug, unsure how to utilise this on the joined brand
$forms = Form::where('user_id', Auth::id())->with('brand')->get();
return response()->json([
'forms' => $forms
], 200);
}
My Brand model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Brand extends Model
{
use HasFactory, SoftDeletes;
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'brands';
/**
* The attributes that are mass assignable.
*
* #var string[]
*/
protected $fillable = [
'uuid',
'brand',
'url',
'telephone',
'link_terms',
'link_privacy',
'seo_description',
'base64_logo',
'base64_favicon',
'text_marketing',
'text_promos',
'text_broker',
'text_footer_1',
'text_footer_2',
'text_credit_disclaimer',
'analytics_internal_affiliate'
];
/**
* The relationships that should always be loaded.
*
* #var array
*/
protected $with = [
'form'
];
/**
* Get the form associated with the user.
*/
public function form()
{
return $this->hasOne(Form::class);
}
/**
* Get the brand that owns the comment.
*/
public function brand()
{
return $this->belongsTo(User::class);
}
}
My Form model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Form extends Model
{
use HasFactory, SoftDeletes;
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'forms';
/**
* The attributes that are mass assignable.
*
* #var string[]
*/
protected $fillable = [
'name',
'loan_amount',
'loan_min_amount',
'loan_max_amount',
'loan_term',
'loan_min_term',
'loan_max_term'
];
/**
* Get the brand that owns the comment.
*/
public function brand()
{
return $this->belongsTo(Brand::class);
}
}
The path of least resistance would be to harness the whereRelationship() method on your query.
I believe that would look like this:
$forms = Form::where('user_id', Auth::id())
->whereRelationship('brand', '=', $brand)
->with('brand')
->get();
https://laravel.com/docs/master/eloquent-relationships
An alternative which would be more work to set up but would likely make your life easier long term would be to use route model binding. https://laravel.com/docs/master/routing#route-model-binding
You can ask Laravel's IoC container to resolve the full record for you by typehinting it and naming the variable correctly in the method signature.
Since you are using a column other than id to identify it, you need to inform Laravel of this. There are a couple of different ways to do this, discussed at https://laravel.com/docs/master/routing#customizing-the-default-key-name
Then, you can ask Laravel's IoC container to build an instance of the model by searching in the database for you right in the method signature. You simply need to typehint the parameter and ensure it's name matches that in the route definition.

Sorting of paginator object through a model relation column

I have three tables: products, product_inventories and product_inventory_details. The ORM of each model is shown below,
Product Model
class Product extends Model{
...
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
...,
'title',
'selected_inventory_id',
...
];
/**
* Get the inventories those belongs to this model.
*/
public function inventory(){
return $this->hasMany('App\Models\ProductInventory');
}
/**
* Get the selected product_inventory_detail that owns this model.
*/
public function selected(){
return $this->hasOne('App\Models\ProductInventoryDetail', 'id', 'selected_inventory_id');
}
...
}
ProductInventory Model
class ProductInventory extends Model{
...
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'product_id',
...
];
/**
* Get the inventory_details those belongs to this model.
*/
public function items(){
return $this->hasMany('App\Models\ProductInventoryDetail');
}
...
}
ProductInventoryDetail Model
class ProductInventoryDetail extends Model{
...
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'product_inventory_id',
'price',
...
];
}
I'm sorting and limiting the results through user input of Sort by dropdown and Show per page dropdown. When sorting by Alphabetical: High to Low option I'm running the query builder method to order the results:
$products = $products->orderBy($sort['column'], $sort['order'])->paginate($limit);
Now with sorting by Price, I can't run the query builder method orderBy() since I'm not using joins and getting the data through relationship properties. Instead I'm using the Laravel's collection method to sort it out:
$products = $products->paginate($limit);
$products = $products->sortBy(function($prod, $key){
return $prod->selected->price;
});
The above block is working fine if I don't use pagination methods. But, I need to use the pagination as well since the user can also limit the results per page. I'm also using a Paginator object's method to append some parameters to each page URL:
$products->appends($paramsArr);
Since running the sortBy() method returns a collection instead of Paginator object, it's giving me undefined method exception.
My Question
How can I sort the result set by price in my current scenario without having to implement the joins? Is there a way to achieve that??
I would use QueryBuilder package of Spatie. It will make your life easier for creating sortable and filterable grid table. You use that package this way:
$query = Product::with(['inventory', 'selected']);
$products = \Spatie\QueryBuilder\QueryBuilder::for($query)
->allowedFilters([
'name' => 'name', // name column in the products DB table.
'selected.price' => 'product_inventory_details.column_price', // price column in the product_inventory_details DB table.
])
->defaultSort('name')
->allowedSorts([
'name',
\Spatie\QueryBuilder\AllowedSort::custom('selected.price', new SortSelectedPrice())
])
->paginate(20)
->appends(request()->query());
External custom sort class looks like this:
class SortSelectedPrice implements \Spatie\QueryBuilder\Sorts\Sort
{
public function __invoke(Builder $query, bool $descending, string $property)
{
$direction = $descending ? 'DESC' : 'ASC';
$query->leftJoin('product_inventory_details', 'products.id', '=', 'product_inventory_details.product_id');
$query->orderBy('product_inventory_details.column_price', direction);
}
}
Make sure your URL containing the query string like this for sorting name from A to Z and sorting price from 1xxxxxxxx to 0:
domain.com/products?sort=name,-selected.price
I installed the Spatie package using composer. Don't forget to do that.
I found a way to handle it without having to implement the joins. I added a new variable and stored the Paginator object's items() method result set into it.
$products = $products->paginate($limit);
$productItems = $products->items(); // returns the items array from paginator
And sort that particular variable instead of sorting the whole paginator object. That way my links and URLs are untouched and unmodified in the $products variable and the data of the products are in a separate variable.
if($sort['column'] == 'price'){
if($sort['order'] == 'DESC'){
$productItems = $products->sortByDesc(function($prod, $key){
return $prod->selected->price;
});
} else{
$productItems = $products->sortBy(function($prod, $key){
return $prod->selected->price;
});
}
}
I also had to change my rendering variable from $products to $productItems and accessed the pagination links from the old $products variable.
#forelse ($productItems as $product)
#include('site.components.product-grid')
#empty
<div class="col text-center">...</div>
#endforelse
...
{{ $products->links() }}
I'm posting it here for the community to benefit/discuss/criticize if there is a better way.

Non-static method should not be called statically, assuming $this from incompatible context -Laravel 5.2

Hi I have the following models.
Venues Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Venues extends Model
{
public $timestamps = false;
/**
* The database table used by the model.
** #var string
*/
protected $table = 'locations';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['name', 'country','city','client_id'];
public function survey(){
return $this->belongsToMany('App\Survey', 'location_to_survey','location_id', 'survey_id')->withPivot('is_review');
}
}
Survey Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Survey extends Model
{
//
public function venues(){
return $this->belongsToMany('App\Venues', 'location_to_survey','location_id', 'survey_id')->withPivot('is_review');
}
}
When I am trying to get the surveys of a venue, I am getting following error.
$survey = Venues::survey()->where('id','=', $id)->orderBy('surveys.id','DESC' )->first();
Non-static method App\Venues::survey() should not be called
statically, assuming $this from incompatible context
I have created Models and relations same like this in Laravel 5.1 without this issue. I want to know if I am missing something in Laravel 5.2.
survey() is a relation method that is definitely not static, I don't really know what you are trying to accomplish but you have tow options, you either get the survey you want directly from the Survey model like so:
Survey::where('id','=', $id)->orderBy('surveys.id','DESC' )->first();
or you do that on an instance of the Venues model like so:
$venue->survey()->where('id','=', $id)->orderBy('surveys.id','DESC' )->first();
and in the second case of course it will be searching through the surveys that belong that specific venue.
If you are trying to get the surveys of the venue, then you should do something like this...
$surveys = Venue::find($id)->venues()->orderBy('id', 'DESC')->first()

Laravel 5.2: Retrieving list of unique values in a many to many relationship

So I have a pension type:
class Type extends Base
{
use RecordStatusActiveTrait;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'pn_pension_type';
/**
* The actual primary key for the model.
*
* #var string
*/
protected $primaryKey = 'pension_type_id';
// Belongs To -----
public function pensionClass() {
return $this->belongsTo('App\Models\Control\PensionClass', 'pension_class_id');
}
// Belongs To Many -----
public function qualifiers()
{
return $this->belongsToMany('App\Models\Pension\Qualifier', 'pn_pension_type_qualifier', 'type_id', 'qualifier_id');
}
}
And a pension Qualifier:
class Qualifier extends Base
{
use RecordStatusActiveTrait;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'pn_pension_qualifier';
/**
* The attributes that should be casted to native types.
*
* #var array
*/
protected $casts = [
'rules' => 'array',
];
// Belongs To Many -----
public function types()
{
return $this->belongsToMany('App\Models\Pension\Type', 'pn_pension_type_qualifier', 'qualifier_id', 'type_id');
}
}
They have a many to many relationship with a table in between as normal. I would like to grab a set of pension types and find the unique Qualifiers between all of them. I COULD just foreach through each of their qualifiers and end up with a list of unique qualifiers but I was wondering if their was an "Eloquent" way.
Using whereHas will allow you to restrict the results based on the relation. In regards to what you want, you'd want to query from your Qualifiers like so.
Qualifiers::whereHas('pensions', function($query) use ($pensionIds) {
$query->whereIn('id', $pensionIds);
})->distinct()->get();
Adding on ->distinct() will then get you the unique entries.

Laravel Nested Eager Loading with Constraints

I have a Question Eloquent Model, a Course Eloquent Model, a University Eloquent Model. A One to Many relationship exists between the University and the Course. A Many to Many relationship exists between the Question and the Course. The Three models are shown below:
Question Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Question extends Model
{
/**
* The database table that the Model uses
* #var string
*/
protected $table = "questions";
/**
* The fields that are mass assignable
* #var array
*/
protected $fillable = ['title','body','images','quality_score','deactivated','creator_id','reviewer_id'];
/**
* Images is stored as serialized json.
* So we cast it to a PHP array.
* See: http://laravel.com/docs/5.1/eloquent-mutators#attribute-casting
*/
protected $casts = [
'images' => 'array',
];
public function courses(){
return $this->belongsToMany('App\Course');
}
}
Course Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Course extends Model
{
/**
* The database table used by the model
* #var string
*/
protected $table = "courses";
/**
* The fields that can be mass assigned
* #var array
*/
protected $fillable = ['name', 'instructor', 'acronym', 'university_id', 'creator_id', 'reviewer_id'];
/**
* There exists a many to one relationship between the Course and User
* This user is the creator of the course
*
* #method void
*
*/
public function creator(){
return $this->hasOne('App\User','creator_id');
}
/**
* There exists a many to one relationship between the Course and User
* This user is the reviewer of the course
* The reviewer of the Course will always be an admin
* If an Admin is the creator, then the reviewer is also the same admin
*
* #method void
*/
public function reviewer(){
return $this->hasOne('App\User','reviewer_id');
}
/**
* There exists a one to many relationship between the University and the Course
* This university is where the course is held
* Courses may float i.e. not be associated to any university
*
* #method void
*/
public function university(){
return $this->belongsTo('App\University');
}
/**
* This method is an accessor. It automatically changes the acronym to be all capitals
* regardless of how it is stored in the database.
* See: http://laravel.com/docs/5.1/eloquent-mutators#accessors-and-mutators
* #param $value (String from Database)
* #return string (Capitalized String)
*/
public function getAcronymAttribute($value){
return strtoupper($value);
}
}
University Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class University extends Model
{
/**
* The database table used by the model
* #var string
*/
protected $table = "universities";
/**
* The fields that can be mass assigned
* name = Name of the University (Example: University of Illinois at Urbana Champaign)
* acronym = Acronym of the University (Example: UIUC)
* creator_id = Id of User that created the University
* reviewer_id = Id of User that reviewed and approved the University
*
* Universities will not be displayed to users without admin role unless they have been reviewed.
*
* #var array
*/
protected $fillable = ['name','acronym','creator_id','reviewer_id'];
/**
* This method is an accessor. It automatically changes the acronym to be all capitals
* regardless of how it is stored in the database.
* See: http://laravel.com/docs/5.1/eloquent-mutators#accessors-and-mutators
* #param $value (String from Database)
* #return string (Capitalized String)
*/
public function getAcronymAttribute($value){
return strtoupper($value);
}
}
On my home page I am showing a list of questions and allow filters for course and university. The controller method is shown here:
public function getHome(Request $request){
/**
* Eager Load with Course and University
*/
$questions = Question::with('courses.university')->get();
/*
* Filter Questions to remove unwanted entries based on course id
*/
if($request->has('course_id') && $request->input('course_id') != -1){
$questions = $questions->filter(function($question) use ($request){
foreach($question->courses as $course){
if ($course->id == $request->input('course_id')){
return true;
}
}
});
}
/*
* Filter Questions to remove unwanted entries based on university id
*/
if($request->has('university_id') && $request->input('university_id') != -1){
$questions = $questions->filter(function($question) use ($request){
foreach($question->courses as $course){
if ($course->university->id == $request->input('university_id')){
return true;
}
}
});
}
/*
* Return the Welcome View with Pagination on the Questions Displayed
* List of Courses and List of Universities
*/
return view('welcome',[
'questions' => $questions,
'courses' => Course::all(),
'universities' => University::all(),
'selected_university_id' => $request->input('university_id',-1),
'selected_course_id' => $request->input('course_id',-1)
]);
}
What I am doing above is returning all the questions from the database and them combing them through to remove all the ones that don't match the filters. This is obviously quite inefficient. I want to use Nested Eager Loading with constraints except I am having a lot of trouble defining what the constraint would look like. Further, I want to use server side paginate to make the client experience better on lower speed internet connections.
Here is one of my attempts:
$questions = Question::with(['courses.university' => function($query) use ($request){
if($request->has('university_id') && $request->input('university_id') != -1) {
$query->where('id', $request->input('university_id'));
}
if($request->has('course_id') && $request->input('course_id') != -1){
$query->where('courses.id',$request->input('course_id'));
}
}])->paginate(10);
This works fine when I don't have any filters.
When I do have a university_id defined, I get the error: Trying to get property of non-object (View: /var/www/testing.com/resources/views/welcome.blade.php)
When I do have a course_id defined, I get the error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'courses.id' in 'where clause' (SQL: select * from universities where universities.id in (1, 2) and courses.id = 1)
I expected the error when I have course_id defined (because I took a blind stab at the first argument of the $query->where method.
I am looking for help in defining the Nested Eager Loading Constraints.
I found the solution in a medium article. The solution works for later versions of laravel as it uses whereHas.
// If you want to put the constraint on the second relation
$questions = Question::with(['courses' => function($query) use($request){
return $query->whereHas('university', function($inner_query) use($request){
return $inner_query->where('id', $request->input('university_id'));
});
}, 'courses.university'])->paginate(10);
For your case, a simple whereHas ought to do the trick.
$questions = Question::whereHas('courses', function($query) use ($request){
return $query->where('university_id', $request->input('university_id'));
})->with(['courses.university'])->paginate(10);
I would also recommend using when clauses to reduce the amount of code.
$questions = Question::when(($request->has('course_id') && $request->input('course_id') != -1), function ($query) use($request){
return $query->where('course_id', $request->input('course_id'));
})->when($request->has('university_id') && $request->input('university_id') != -1, function ($outer_query) use($request){
return $outer_query->whereHas('courses', function($query) use($request){
return $query->where('university_id', $request->input('university_id'));
})->with(['courses.university']);
})->with(['courses.university'])->paginate(10);

Categories