Laravel Livewire Save to Collection and Show in Component View - laravel

I am using Laravel Livewire to choose an item from a dropdown list (which passes an ID), I then find the Model that ID belongs to and I want to add that Model to a collection. Then each additional time I choose something from the dropdown list I want to update the existing collection with that new Model.
public $workoutExercises;
public function mount()
{
$this->workoutExercises = collect([]);
}
public function submitExercise()
{
$newExercise = Exercise::where('id', $this->exercise)->first();
$this->workoutExercises->push($newExercise);
return;
}
In the component view:
#foreach ($workoutExercises as $workoutExercise)
{{ $workoutExercise->exercise_name }}
#endforeach
When I submit a new exercise it adds it to the collection and shows up in my view as intended. However, when I go to add an ADDITIONAL exercise I get this error..."Attempt to read property "exercise_name" on array".
I don't get it. It's like it adds the first one fine, but resorts to an array instead of a collection for any subsequent submissions?
TIA!

Behind the back, Livewire uses all the things like an array, so when the component first loads your $workoutExercises is a collection, but after the first request it turns into an array. What I usually do is create an array of id's selected and query the data as a computed property.
public array $workoutExerciseIds = [];
public function submitExercise(): void
{
array_push($workoutExerciseIds, $this->exercise);
}
public function getWorkoutExecisesProperty()
{
return Exercise::query()
->whereIn('id', $this->workoutExerciseIds)
->get();
}
Then on my front end:
#foreach ($this->workoutExercises as $workoutExercise)
{{ $workoutExercise->exercise_name }}
#endforeach

Related

Livewire: persist custom objects in Livewire component after wire:click?

Can I keep using a collection of custom objects throughout a Livewire lifecycle?
I create a collection, show them in a list, and take action when the user selects one.
At the moment they are still an object in the blade #foreach (i.e. {{ $item->name }}, but end up as array after the wire:click (i.e. $item['name']), which breaks running the same #foreach again after completing the wire:click method.
But more importantly, each custom object contains a collection of models and they are converted to plain array as well.
It seems this is currently expected behavior as Livewire does not know how to rehydrate them (unlike Eloquent models).
I was hoping that I could store the objects in protected property but these do not persist, just like the documentation says.
Is there a way to achieve a similar flow where I display a list (using data from custom objects) and take action on the selected custom object?
protected properties truly are only useful for variables that are consistent, such as rules, or variables that are set each request that cannot be public.
As for the collection issue, it seems like the answer is already in the github issue thread you linked, by simply re-initiating the arrays as object. It (for now) is expected behaviour as it cannot rehydrate. You can do a map on the collection:
$this->customCollection = $this->customCollection->map(function($item) {
return is_array($item) ? (object) $item : $item;
});
or a foreach like so:
foreach ($this->customCollection as $index => $item) {
if (is_array($item)) {
$this->customCollection[$index] = (object) $item;
}
}
For each nest of collections, you'll have to do the same looping if you specifically want custom objects. It'll probably lose out on performance and you're probably better off using Eloquent collections/models or pure arrays.

ManytoMany relations in Laravel, retrieve data from related tables and display in blade

I have two tables related by many to many relation in Laravel Framework. I can display data from each table separately, but not through relation by taking one record from the 1st table and checking related records in the 2nd table. In tinker it accesses data fine.
Relations:
public function underperformances() {
return $this->belongsToMany(Underperformance::Class);
}
...
public function procedures() {
return $this->belongsToMany(Procedure::class);
}
My resource controller part:
...
use App\Underperformance;
use App\Procedure;
...
public function index()
{
$books = Underperformance::orderBy('id','desc')->paginate(9);
$procedures = Procedure::all();
return view('underpcon.underps', compact('books', 'procedures'));
}
Route:
Route::get('/underps', 'UnderpsController#index');
If I try to display data like this:
#foreach($procedures as $procedure)
<li>{{$procedure->underperformances}}</li>
#endforeach
I get such format to the browser:
[{"id":1,"title":"Spare part not taken before service","description":"tekstas","level":"1","costs":600 ...
This is correct data from related table, but I cannot select further the specific column from that table. For example this does not work:
#foreach($procedures as $procedure)
<li>{{$procedure->underperformances->id}}</li>
#endforeach
Nor this one:
#foreach ($procedures->underperformances as $underperformance)
<li>{{$underperformance->id}}</li>
#endforeach
How do I select records of the related table and display specific data from that table?
What would be a conventional way to do this?
#foreach($procedures as $procedure)
<li>{{$procedure->underperformances->id}}</li>
#endforeach
^ This right there $procedure->underperformances will return a collection, not a single item, so you need to treat it as array, you will not be able to access the id directly, you can either #foreach that, or use the pluck method in Laravel Collections.

Laravel - Display data from multiple tables in one page

I have 3 tables in my database. I want to display all 3 table data on a single page.
Student.php
public function hobbies() {
return $this->hasMany(Student::class);
}
Hobbie.php
public function student()
{
return $this->belongsTo(Student::class);
}
Controller.php
$students->name= request('name');
$students->address = request('address');
if($students->save())
for ($i=0; $i < count(request('hob')); ++$i)
{
$hobbies = new Hobby;
$hobbies->name= request('hob')[$i];
$students->hobbies()->save($hobbies);
}
View.blade.php
$students->hobbies()->hob;
But it does not show any hobby. What is the proper solution?
Your entire controller code would be helpful.
In your view $students->hobbies()->hob; you are accessing the property hob of the Collection $students->hobbies(). You want to access the property hob of an element of the Collection $students->hobbies() so you probably want to access $students->hobbies()[0]->hob or loop through the Collection with a foreach.
Accessing $students->hobbies() in your view actually queries your database, so you would be better off setting a $hobbies variable in your controller and passing that to the view, then looking through your variable.
#foreach ($hobbies as $hobby)
{{ $hobby->hob }}<br/>
#endforeach

Laravel saving ordered list of eloquent models

I'm creating a food menu which the administrator can order/sort by dragging and dropping. This menu consists of multiple categories (ProductCategory) and products (Product).
I'm using HTML5Sortable on the client-side to allow nested d&d. The markup is pretty simple:
<div class="categories">
#foreach($categories as $category)
<div class="category">
#foreach($category->products as $product)
<div class="products">
<div class=""product" data=id="{{ $product->id }}">
{{ $product->name }}
</div>
</div><!-- /products !-->
#endforeach
</div><!-- /category !-->
#endforeach
</div>
And the corresponding javascript:
$('.categories').sortable({
items: '.category'
});
$('.products').sortable({
items: '.product'
});
// Will be called when the user is done repositioning the products and categories
function getOrderedList() {
var data = {};
$('.categories').find('.category').map(function(i) {
var category = $(this);
data[i] = {};
data[i].id = category.data('id');
data[i].products = category.find('.product').map(function() {
return $(this).data('id');
}).get();
});
data = JSON.stringify(data); // Send data to server
}
The function getOrderedList will send a JSON string back to Laravel, which contains the sorted category id's and product id's:
{"0":{"id":1,"products":[2,3,1,4,5,6,7,8,9,10]},"1":{"id":2,"products":[11,12,13,14]},"2":{"id":3,"products":[15,16,17,18]}}
How would I make this work on the back-end? I guess I must store this array somewhere in the database and later find and order the models by the id's?
In short: What is a clean and flexible solution for sorting (nested) models (within Laravel)?
A common convention is Weight, add a field called (Int)Weight on the products table, which is used to define the order of the items.
Once a change in the order occurs you only update the weight field.
When you retrieve the items, you sort them by Weight.
it becomes similar to an Array
Id Name Weight
01 'product 1' 2
02 'product 2' 0
03 'product 3' 1
when you order it by weight you get
product 2
product 3
product 1
it's similar to an array because
$products[0] = 'product 2'
$products[1] = 'product 3'
$products[2] = 'product 1'
Note that if you want to make it even more dynamic, you can create a polymorphic model that can satisfy multiple models.
Please refer to https://laravel.com/docs/5.1/eloquent-relationships#many-to-many-polymorphic-relations
Polymorphic Relations example
Create table Weights (migration example)
$table->increments('id');
$table->integer('value');
$table->integer('weightable_id')->unsigned();
$table->string('weightable_type');
Create model Weight
class Weight extends Eloquent
{
public function weightable()
{
return $this->morphTo();
}
}
now with any other model
class Products extends Eloquent
{
...
public function weight()
{
return $this->morphOne(Weight::class);
}
}
this way you can just add that method to any model you want then you can sort your model with it.
P.S. make sure any model that uses it, creates that relation immediately after creating the model
i do not recommend this method, it's much better if you explicitly define the weight field in the Products table, i understand how much you want your code to be dynamic, but everything comes at a cost
Performance goes down, it's not easy to visualize your code once you establish polymorphic relations, its more like starting to use Jumps instead of Functions
First, the JSON that you are producing shouldn't be an object where the keys are just array indices. Instead it should be an array of objects that looks like this:
[{"id":1,"products":[2,3,1,4,5,6,7,8,9,10]},{"id":2,"products":[11,12,13,14]},{"id":3,"products":[15,16,17,18]}]
Since the products table to product_categories table has an obvious many to one relationship, you'd just use the product_categories_id foreign key on the products table to represent the relationships laid out in your JSON.
In the nested objects of your JSON, every value in the products key array will have a foreign key that corresponds to the id key value in the same nested object (this is the product_category_id column on your products table).
Your API endpoint function would then look something like this:
public function myApiEndpoint(){
$input = Input::get('input');
$input = json_decode($input, true);
foreach($input as $category){
Product::whereIn('id', $category['products'])->update(array(
'product_category_id' => $category['id']
));
}
}
I am updating the model directly in the API controller here, but you should really do any model changes through a repository that's also implementing an interface.
The above will work if you only ever have one menu (with it's categories and products). If you want multiple menus, then you'll need a menus table along with a three way pivot table (with columns menu_id, product_id, and product_category_id).
I just implement this behavior using this library:
https://github.com/spatie/eloquent-sortable
It is very simple to implement, basically you need an extra column to keep the order and the library will do the rest, here is a part of the documentation:
Implement the Spatie\EloquentSortable\Sortable interface.
Use the trait Spatie\EloquentSortable\SortableTrait.
Optionally specify which column will be used as the order column. The default is order_column.
use Spatie\EloquentSortable\Sortable;
use Spatie\EloquentSortable\SortableTrait;
class MyModel extends Eloquent implements Sortable
{
use SortableTrait;
public $sortable = [
'order_column_name' => 'order_column',
'sort_when_creating' => true,
];
...
}

laravel access model properties

I am looking for solution how to access eloquent model items by 'alias' field.
There is no problem accessing items by 'id'. But building a custom query I find myself unable to access item properties.
This piece of code works perfect
$cat = Category::find(1);
return $cat->title;
But if I am querying items with any other argument - properties are inaccessible
This code
$cat = Category::where('alias','=','vodosnab')->get();
return $cat->title;
throws an exception
Undefined property: Illuminate\Database\Eloquent\Collection::$title
Could you please help.
You already got the answer but here are some insights, when you use get() or all(), it returns a collection of model objects, which is an instance of Illuminate\Database\Eloquent\Collection, so here you'll get a Collection object
$cat = Category::where('alias','=','vodosnab')->get();
Now, you can use, $cat->first() to get the first item (Category Model) from the collection and you may also use $cat->last() to get the last item or $cat->get(1) to get the second item from the collection. These methods are available in the Collection object.
Using the first() method like Category::where('alias','=','vodosnab')->first(); will return you only a single (the first mathing item) model which is an instance of your Category model. So, use all() or get() to get a collection of model objects and you can loop through the collection like:
foreach(Category::all() as $cat) { // or Category::get()
$cat->propertyName;
}
Or you may use:
$categories = Category::where('alias','=','vodosnab')->get();
foreach($categories as $category) {
$category->propertyName;
}
Also, you may use:
$categories = Category::where('alias','=','vodosnab')->get();
$firstModel = $categories->first();
$lastModel = $categories->last();
$thirdModel = $categories->get(2); // 0 is first
If you need to get only one then you may directly use:
$category = Category::where('alias','=','vodosnab')->first();
$category->fieldname;
Remember that, if you use get() you'll get a collection of Model objects even if there is only one record available in the database. So, in your example here:
$cat = Category::where('alias','=','vodosnab')->get();
return $cat->title;
You are trying to get a property from the Collection object and if you want you may use:
$cat = Category::where('alias','=','vodosnab')->get();
return $cat->first()->title; // first item/Category model's title
return $cat->last()->title; // last item/Category model's title
return $cat->get(0)->title; // first item/Category model's title
You may read this article written on Laravel's Collection object.
get() returns a Collection of items. You probably need first() that returns a single item.

Resources