Querying laravel polymorphic many to many relationships - laravel

I have estate_object table which holds information about estate, for example: address, city, region etc.
Then i have a seperate tables which holds just specific property of an object, for example on of the tables is objects_near.
I made seperate tables for that because each of those models can hold multiple values(they are checkboxes)for example objects_near cant hold values like - parking, airport, see, store etc.
So to escape unnecessary columns and null fields in estate_object table i made polymorphic many to many relationship between estate_objects table and tables which hold properties.
In my view i want to make a filter and querying things from estate_object table works fine. To filter properties which belong to one or more estate objects i have checkboxes. For example to query objects near estate i have checkboxes with multiple options. I can't figure out and can't find solution as well on how to query objects_near properties which are related just to specific estate_object.
This is how my checkboxe looks like:
<div class="form-group align-items-center">
<label class="col-form-label text-md-right">{{ trans('admin.estate-obj.label.columns.objects_near') }}
</label><br>
#foreach($estate_objects_near as $objects_near)
<input type="checkbox" id="{{ $objects_near->name }}" name="objects_near[]" value="{{
$objects_near->id }}">
<label for="{{ $objects_near->name }}">{{ $objects_near->name }} </label>
#endforeach
/div>
Estate_object_near model:
public function estateObj()
{
return $this->morphToMany(EstateObj::class, 'estateable');
}
EstateObj model:
public function objectsNear()
{
return $this->morphedByMany(Estate_objects_near::class, 'estateable');
}
EstateObjController:
function( $query) use ($request) {
if($request->has('address')){
$query->where('address', $request->address);
}
if($request->has('price')){
$query->whereBetween('price', [$request->price['min'], $request->price['max']]);
}
if($request->has('room_qty') ){
$query->where('room_qty', $request->room_qty);
}
// trying to access properties which are selected in checkbox and related to estate
object
if($request->has('objects_near')){
$objNear = Estate_objects_near::find(1);
$obj = $objNear->estateObj;
$query->where($obj, $request->objects_near);
}
});
If i dump out $obj i am getting relations with pivot table 'estateable' and i can see which property is related with specific estate obj, but when i try to execute it shows me SQLSTATE[HY093]: Invalid parameter number: mixed named and positional parameters.
I have tried to use whereHas function as well, but then i get this error - Argument 1 passed to Illuminate\Database\Eloquent must be an instance.
Have tried whereHasMorph as well:
$query->whereHasMorph('estateable', [Estate_objects_near::class], function ($query) {
$query->where('id', $request->objects_near);
})->get();
This leads to 'Call to undefined method App\Models\EstateObj::estateable()'
Would appreciate suggestions very much.

Following query worked for me:
if($request->has('objects_near')){
$query->whereHas('objectsNear', function ($query) use ($request) {
$query->whereIn('estate_objects_near.id', [$request->get('objects_near')]);
})->get();
}
As it turned out whereHasMorph works just with morphTo relations.
If anybody has the same problem and would like to know more of my code structure let me know.

Related

How to get Unique value in blade file laravel

I want to fetch unique value for filters of all products.
My db structure as follows
id Product category_id format attribute
1 demo1 5 HD test1
2 demmo2 4 SD test3
3 demmo3 4 HD test2
4 demmo4 3 HD test3
I want add filters of format and attribute in product page. But in that HD comming 3 times. I want to display that one only.
I am not getting how to display only single time.
Below is my controller code:
$item = Item::where('active_status', 1)->where('status', "1");
$data['item_count'] = $item;
$data['item'] = $item->paginate(20);
return view('frontend.pages.explore', compact('data'));
Below is blade file
<div class="filter-btn">
#foreach($data['item'] as $resolution)
<a class="btn btn-white-outline display-4" href="">{{array_unique($resolution->format)}}</a>
#endforeach
</div>
I am not getting how to display unique value only. Anyone have idea then let me know
since you are paginating your data, your "first page" might not have all the formats, so you have to do another query to your database:
$formats = DB::table('items')->select('format')->distinct()->get()
...
view(..., compact('data', 'formats'))
in the blade table:
#foreach($formats as $resolution)
<a class="btn btn-white-outline display-4" href="">{{$resolution->format}}</a>
#endforeach
If I am correct about your query, then you need to use groupby to list the items in your controller.
$items = Item::groupBy('format')->get();
The solution would be to create a sperate model for Formatand relationship between it and Product or whatever model that needs format , then fetch formats from its table and apply eager load .
this may look longer than your solution , but this is standards shoiuld be taken for vrious reasons :
less sql queries
more flexibility and options regarding our new table
better and less code to crud format
single source for change
less database data and faster load
...
Product.php
public function format(){
return $this->belongsTo(Format::class);
}
Format.php
public function products(){
return $this->hasMany(Product::class);
}
Usage
in controller
// get all formts eager loaded with products
$formats = Format::with('products')->get();
//get all products
$product = Format::latest()->paginate(20);
return view('frontend.pages.explore', compact( 'products' ,'formats'));
in your view
// all prodcuts
#foreach($formats as $format)
<a class="btn btn-white-outline display-4" href="">{{$format->name}}</a>
#endforeach
// in filter ( at clicking ) no additional query
#foreach($format->products as $product)
...
#endforeach

Use vue to access a Laravel model method in a v-for loop

I am learning how to use vue with laravel. I have basic loops working well to pull direct model relationships, but I can't figure out how to access model methods in a loop. Many of my Larvel models have basic information formulated with a method pulling data from related models. I've tried to research it and think the answer might be some combination of eager loading, preformating the answer as a json response or maybe something with axios, but the snipits I've found aren't clear on what goes where, or what needs to be in place for them to work correctly. I've tried both eager loading and using a json response and neither has worked. I can access methods in simple vue components that are just text, but not in a loop where the variable isn't part of the page.
Example: I want to use Vue to display a list of ingredients on a recipe's page. The ingredient "title" is a method pulling the information from a related model.
RecipeController.php
public function show(Recipe $recipe)
{
$ingredients = $recipe->ingredients;
$view = $this->view('recipes.show');
//(variable in the view, variable defined in current function)
$view->with('recipe', $recipe);
$view->with('ingredients', $ingredients);
return $view;
}
Recipe.php
public function ingredients()
{
return $this->hasMany('App\Models\Ingredient', 'recipe_id', 'recipe_id');
}
Ingredient.php
public function title()
{
$title = $this->item->title();
return $title;
}
public function vueTitle()
{
$title = Ingredient::title()->get();
return response()->json($title );
}
Recipes/show.php
<div>
<ul>
<li
is="test-li"
v-for="ingredient in {{ $ingredients }}"
v-bind:key="ingredient.ingredient_id"
v-bind:title= "ingredient.vueTitle"
v-bind:id="ingredient.ingredient_id"
></li>
</ul>
</div>
I'd prefer to reuse the same methods, but created a new one to try converting to json first but that didn't work (or I'm doing it wrong). I tried eager loading, but it either did nothing, or generated an error (Call to a member function on null) if I tried to eager load the specific method. I've tried various combinations of binding and not binding the title component. I've also tried title= "{{ingredient->title()}}" but that syntax errors.
How can I get the result of the Laravel method in a Vue loop?
After more searching, I found this post which described how to add an accessor to a model. Doing so allowed me to access my custom method as if it were a standard direct relationship. It was a straightforward modification and will reduce complexity in a number of places. I made the following modifications:
Ingredient.php
Added the get..Attribute() function and appended the array
...
protected $table = 'ingredients';
...
protected $appends = array('title');
// Access methods as direct relationships
public function getTitleAttribute()
{
return $this->title();
}
Recipes/show.php
Bound the title prop to the ingredient title as if it were a direct relationship.
<div>
<ul>
<li
is="test-li"
v-for="ingredient in {{ $ingredients }}"
v-bind:key="ingredient.ingredient_id"
v-bind:title= "ingredient.title"
v-bind:id="ingredient.ingredient_id"
></li>
</ul>
</div>
Another example, hope one may find it helpful:
Model.php
/**
* Accessor for Age.
*/
protected $appends = ['age'];
public function getAgeAttribute()
{
return Carbon::parse($this->attributes['dob'])->age;
}
VueFile.vue
<td>
<span v-bind:age="user.age"> {{user.age}} </span>
</td>

Laravel Livewire wire:click creates infinite loop

I have a Livewire component that's a product filter. The queries all work fine, but sometimes it creates an infinite loop of requests.
You can see that happening in the GIF below which is a capture of the Laravel Debugbar. I'm clicking some filters and then suddenly it goes into this request loop.
I specifically use wire:loading.attr="disabled" on the filters in the view so someone can not select a filter while a request is still processing.
My code and some background:
Livewire Component
use App\Models\Product;
use App\Models\Brand;
use App\Models\Color;
class SearchProducts extends Component
{
public ?array $brand = [];
public ?array $color = [];
protected $queryString = ['brand', 'color'];
public function render()
{
$products = Product::query();
$products = $products->with('brand');
$products = $products->with('colors');
$products = $this->filterBrands($products);
$products = $this->filterColors($products);
$products = $products->paginate(24);
return view('livewire.search-products', [
'all_brands' => Brand::where('status', 'active')->get(),
'all_colors' => Color::where('status', 'active')->get(),
])->extends('app');
}
public function filterBrands($query)
{
$queryFilterBrand = array_filter($this->brand);
return empty($queryFilterBrand) ? $query : $query->whereIn('brand_id', $queryFilterBrand);
}
public function filterColors($query)
{
$queryFilterColor = array_filter($this->color);
return empty($queryFilterColor) ? $query : $query->whereHas('colors', function ($q) use ($queryFilterColor) {
$q->whereIn('color_id', $queryFilterColor);
});
}
}
The reason that I use array_filter is because if I unselect a color value and use a character in the key (wire:model="brand.b{{ $brand->id }}"), instead of removing that from the array Livewire will set that key value to false. So then this false value will be put into the query which will give inaccurate results.
Livewire views and the issue
This works fine:
#foreach($all_brands as $brand)
<input type="checkbox" value="{{ $brand->id }}" id="brand.{{ $brand->id }}" wire:model="brand.{{ $brand->id }}" wire:loading.attr="disabled">
<label class="search-label search-wide-label mb-2" for="brand.{{ $brand->id }}">{{ $brand->title }} <i class="fal fa-times float-right selected-icon"></i></label>
#endforeach
But this creates an infinite loop when I select 2 or more colors after each other, or if I select 1 color and then deselect it. So it seems that issue occurs after the 2nd interaction:
#foreach($all_colors as $color)
<input type="checkbox" value="{{ $color->id }}" id="color.{{ $color->id }}" wire:model="color.{{ $color->id }}" wire:loading.attr="disabled">
<label class="search-label search-wide-label mb-2" for="color.{{ $color->id }}">{{ $color->title }} <i class="fal fa-times float-right selected-icon"></i></label>
#endforeach
This is weird because this blade snippet is exactly the same as for $brands as shown above:
The only thing that different is that the colors relationship is a hasMany vs a belongsTo for brand.
I'm now thinking that this is where the problem is...
The things I've tried and didn't work
Remove the #foreach loop for $all_colors and just put the filters in plain HTML (to check if the issue is related to the loop)
Adding wire:key="brand.{{ $brand->id }}" to the input element
Adding wire:key="brand.{{ $brand->id }}" to a div around the input element
Using wire:model="brand.{{ $brand->id }}" or wire:model="brand.{{ $loop->id }}" as was suggested in the comments (and what I thought solved the problem)
Using wire:model="brand.b{{ $brand->id }}" so there's a unique key name
Removing the array_filter approach (seems unlikely that this is the problem but just to test)
Using buttons instead of checkboxes
Using defer, lazy and/or debounce
Paying an expert to try and fix it...
Console error
Last piece, I get this error in my console only when the infinite loop happens so it's very likely either a cause or effect.
TypeError: null is not an object (evaluating 'directive.value.split')
Unhandled Promise Rejection: TypeError: null is not an object (evaluating 'directive.value.split')
Both in LoadingStates.js which I think is a Livewire Javascript file.
The error there seems to be happening here:
function startLoading(els) {
els.forEach(({ el, directive }) => {
if (directive.modifiers.includes('class')) {
let classes = directive.value.split(' ').filter(Boolean)
Answered on GitHub issue, copied here for others to be able to find.
The problem is a morphdom issue.
The code that is triggering the errors and the loop is the wire:loading on your heading row and products row.
The reason is, when you select two or more colours, there are no results shown. What happens then is you're swapping from showing heading/products/total to showing an empty state.
But morphdom doesn't know by default that it should delete the old divs and add the new one. Instead it is trying to "morph" the old first div into the new one. That means that the wire:loading listeners are still registered when they shouldn't be. Hence why the error and the loop.
It's a simple fix though. You need to add wire keys to the divs defining what they are, so morphdom knows that they have actually changed completely, and to delete the old ones and add new ones.
Have a look at this screenshot below of the diff for what I did to get it working. I added a wire key for all the top level divs inside this file.
It's recommended whenever using conditionals like that to add wire:keys to any elements that are first level inside the conditional, so morphdom knows there has been a change. It's the same problem VueJS has, where keys are required inside loops.
Screenshot of diff
So it turns out that if you're using wire:model in a foreach loop like this, you have to write it like this: wire:model="brand.{{ $brand->id }}". Couldn't find it in the docs so hopefully it helps others here.
Update
The infinite loop is solved by this, but what's happening now is that the array values are set to zero instead of removed from the array once you select a checkbox and then click it again to unselect. So then the whereIn is going to look for brand IDs with value 0 which will not return results.
Update 2
The loop is actually not solved... See original question. Slapping a bounty on this $%$£# because too many hours and coffee were wasted.
A couple issues I see:
You're using the same wire:key for your <div> element and your <input> element. I would keep those unique.
Also, you're using the same name on the input element. This will cause issues with checkboxes as the value passed to the backend has the same name and I believe livewire will attempt to sync based on that. Changing the name field in your input to be unique is likely the answer.

laravel 5.5 multiple controller inside one view

How to show TeamController#index and ProductController#index both show list of team and product inside one view main.blade.php
Looks like you want to show two datasets on one page. Basically, it means you have to execute two controller methods but it's not necessary to follow each and everything that official documentation says.
For example, if Products belong to a team, you can execute only TeamController#index and show products as given below.
#foreach($teams as $team)
#foreach($team->products as $product)
{{ $product->name }}
#endforeach
#endforeach
If no teams and products are two different entities and does not have any relation, you can just pass teams and products like this:
TeamController.php
public function index()
{
$teams = Team::all();
$products = Product::all(); // Don't forget to include 'use App\Product'
return view('index',compact(['teams','products']);
}
and then you can show teams and products like this:
index.blade.php
#foreach($teams as $team)
{{ $team->name }}
#endforeach
#foreach($products as $product)
{{ $product->name }}
#endforeach
Getting information from two different models does not mean you have to execute two different controller functions.
Still, if you want to get data from two different controllers, you can setup index.blade.php and create two ajax requests that will get data from two different URLs (two different controller methods).
Let me know if you have any more questions.
You can't show results from two controllers like that. Create a view that includes both the view that TeamController#index and ProductController#index return. be aware that both might be extending a layout which will probably try to load your page twice, so keep in mind to split the views into smaller components and include only those.
More info here
https://laravel.com/docs/5.6/views#creating-views

Laravel - get id from the right database record

I have laravel project. When I click my view button, I want to see full description of my record. But I don't know how to pass the right id. My database table is called - csstable.
I have model:
<?php
class CssTable extends Eloquent
{
protected $table = 'csstable';
}
View button on each post (I get all of my posts from database, so each of them have id):
<div class="view">
<a href="{{ action('CssController#show') }}" ></a>
</div>
CssController with this show function:
public function show()
{
$csstable = CssTable::all();
return View::make('cssposts/right_post', compact('csstable'));
}
My Route:
Route::get('/css/id_of_right_post', 'CssController#show' );
Right_post, where I want information from description column from row, with id, that i clicked (In this field, I see just last record's description:
<h1 style="color:#fff">{{ $css->description }}</h1>
I have tried to put something like this
public function show($id)
{
$csstable = CssTable::find($id);
return View::make('cssposts/right_post', compact('csstable'));
}
But then there is an error - missing 1 argument in show function. So I want to know, how to pass correct id!
The way to do this involves three steps. First let's go for the route:
Route::get('css/{id}', 'CssController#show');
The {id} there means it's a matching parameter - it'll match a full URI segment (basically anything between slashes) and use that to pass into he method passed. So on to the controller:
class CssController
{
public function show($id)
{
$csstable = CssTable::findOrFail($id);
return View::make('cssposts/view', compact('csstable));
}
}
That controller method accepts a (required) single parameter. You can call it whatever you want, but here I'm going for id as it's an ID for a model. Finally, the last part of the puzzle is how to link to such a route:
// view button for each csstable
<div class="view">
{{ link_to_action('CssController#show', $item->title, array($item->getKey())) }}
</div>
As you can see, I'm using the link_to_action helper, but your method with <a href="{{{ action('CssController#show', array($item->getKey())) }}}"> will work too. After the controller action name, you pass an array that contains all of the parameters in the URI to fill in (in order). In our case we have one parameter, to it's an array with one item. I think in these cases you could also use a string and Laravel will turn it into an array with one element for you. I prefer to be explicit.
Hopefully that's helped you work out how to use the parameter-based routing system in Laravel.

Resources