Most efficient way to bind livewire input with looped/nested properties - laravel

I have a livewire component that has about 25 properties.
public $prop1=[], $prop2=[], $prop3=[] etc. In my view, I'm looping over a collection and displaying a form that would normally have all these properties bound to it with wire:model. But the idea of the loop is throwing me off. I have something like wire:model="prop1.{{$key}}" but because I'm not declaring the property value in the render/mount functions, binding to the property array obviously isn't working. My question is what's the most efficient way to treat this scenario? Should I have a loop in the render or mount function that loops over the collection like so:
public function render()
{
$this->collection_items = Items::all();
foreach($this->collection_items as $key=>$item)
{
$this->prop1[$key] = $item->prop1;
$this->prop2[$key] = $item->prop2;
$this->prop3[$key] = $item->prop3;
..........
}
return view('livewire.my-view');
}
and then in my view I would be able to do wire:model="prop1.{{$key}}"? Or is there some other fancy way to do this without the loop in the render method that could accomplish this in a cleaner way?

Instead of defining each and every property of your model separately, you can wrap them in an array:
public $items = [];
Then, you can use your key as subkey, and use fill to make quick work of your properties:
foreach($this->collection_items as $key => $item) {
$this->fill(["items.{$key}" => $item->getAttributes()]);
}
Then, all your $attributeKey => $attributeValue will be set under $this->items[$key]. In your view, you can then set your wire:model with dot notation in whatever way you wish to work these properties:
wire:model="items.{{$key}}.prop1"
You can also fill it as collection, simply by calling $this->fill([$items => Items::all()]), but then you can't wire:model to an attribute.

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.

Laravel How to pass parameter to Accessor method in model via where condition while query building?

I have a Accessor method in Collection Model getSizesAttribute, which returns array of available sizes eg: ['S','L'], Now I need to get Models with have size 'S'. like:
$collections = $collections->where('sizes','S');
But sizes is array, could I manipulate this anyhow so that I could check returns only if sizes have specific size.
I tried making another method getIsSizeAttribute, like:
public function getIsSizeAttribute($size){
return in_array($size,$this->sizes);
}
Now How could I user this in Where condition like
$collections = $collections->where('is_size','S');
Mutators and Accessors only run skin-deep, after the query's already been executed. You could use Collection::filter() as Bangnokia suggests, but that wouldn't give you any performance benefit of actually applying the condition to the initial request.
I think what you're looking for here is a Query Scope. Add something like this to your Model class:
public function scopeSize(\Illuminate\Database\Eloquent\Builder $query, $size)
{
return $query->whereIn('sizes', $this->sizes[$size]);
}
And access it like this:
$collection = $model->size('S')->get();
You should use filter on collection
$collections = $collections->filter(function($item, $index) {
return in_array('S', $item->sizes);
});

'forget' a collection's item by key within each

I was wondering if you could remove or forget an item by key when using each() method in Laravel.
I have tried this:
$Items = Items::all();
$Items->each(function($Item, $Key){
if($Item->removable){
$this->forget($Key);
}
});
And I have also tried this:
$Items = Items:all();
$Items->each(function($Item, $Key) use (&$Items){
if($Item->removable){
$Items->forget($Key);
}
});
And it seems to just be removing all items instead of the ones that would have my true conditional values (in this e.g. $Item->removable).
Can anyone shine some light on this?
Collections are usually immutable, which means that you can't actually change the underlying data in an object. Operations on a collection often return a new collection containing new items based on the original collection.
You can achieve what you're after using filter. By passing in a closure, which returns true for items that you want to keep and false for items you want to forget.
$Items = $Items->filter(function ($Item, $Key) {
return ! $Item->removable;
});
Or you can use reject which does the same as filter but in reverse, returning true forgets the item from the collection and returning false keeps the item in the collection.
$Items = $Items->reject(function ($Item, $Key) {
return $Item->removable;
});
These two methods will create a new collection, (in the above examples overwriting $Items), containing only values that are not considered to be removable.
You could just use the forget (The forget method is an exception to the immutable rule in that it does modify the underlying data) method from outside the collection to remove an item by its key but I don't think that's what you're after.
$Items->forget('your_key');

Yii2: How to set default attribute values in ActiveRecord?

This may seem like a trivial question, however all of the obvious solutions that I can think of have their own flaws.
What we want is to be able to set any default ActiveRecord attribute value for new records only, in a way that makes it readable before and during validation and does not interfere with derived classes used for search.
The default values need to be set and ready as soon as we instantiate the class, so that (new MyModel)->attr returns the default attr value.
Here are some of the possibilities and the problems they have:
A) In MyModel override the init() method and assign default value when isNewRecord is true like so:
public function init() {
if ($this->isNewRecord) {
$this->attr = 'defaultValue';
}
parent::init();
}
Problem: Search. Unless we explicitly unset our default attribute in MySearchModel (very error-prone because it is too easy to forget), this will also set the value before calling search() in the derived MySearchModel class and interfere with searching (the attr attribute will already be set so search will be returning incorrect results). In Yii1.1 this was resolved by calling unsetAttributes() before calling search(), however no such method exists in Yii2.
B) In MyModel override the beforeSave() method like so:
public function beforeSave($insert) {
if ($insert) {
$this->attr = 'defaultValue';
}
return parent::beforeSave();
}
Problem: Attribute is not set in unsaved records. (new MyModel)->attr is null. Worse yet, even other validation rules that rely on this value will not be able to access it, because beforeSave() is called after validation.
C) To ensure the value is available during validation we can instead override the beforeValidate() method and set the default values there like so:
public function beforeValidate() {
if ($this->isNewRecord) {
$this->attr = 'defaultValue';
}
return parent::beforeValidate();
}
Problem: Attribute is still not set in unsaved (unvalidated) records. We need to at least call $model->validate() if we want to get the default value.
D) Use DefaultValidator in rules() to set a default attribute value during validation like so:
public function rules() {
return [
[
'attr', 'default',
'value' => 'defaultValue',
'on' => 'insert', // instantiate model with this scenario
],
// ...
];
}
Problem: Same as B) and C). Value is not set until we actually save or validate the record.
So what is the right way to set default attribute values? Is there any other way without the outlined problems?
There's two ways to do this.
$model => new Model();
Now $model has all the default attributes from the database table.
Or in your rules you can use:
[['field_name'], 'default', 'value'=> $defaultValue],
Now $model will always be created with the default values you specified.
You can see a full list of core validators here http://www.yiiframework.com/doc-2.0/guide-tutorial-core-validators.html
This is a hangup with Yii's bloated multi-purpose ActiveRecords
In my humble opinion the form models, active records, and search models would be better off split into separate classes/subclasses
Why not split your search models and form models?
abstract class Creature extends ActiveRecord {
...
}
class CreatureForm extends Creature {
public function init() {
parent::init();
if ($this->isNewRecord) {
$this->number_of_legs = 4;
}
}
}
class CreatureSearch extends Creature {
public function search() {
...
}
}
The benefits of this approach are
You can easily cater for different validation, set up and display cases without resorting to a bunch of ifs and switches
You can still keep common code in the parent class to avoid repetition
You can make changes to each subclass without worrying about how it will affect the other
The individual classes don't need to know about the existence of any of their siblings/children to function correctly
In fact, in our most recent project, we are using search models that don't extend from the related ActiveRecord at all
I know it is answered but I will add my approach.
I have Application and ApplicationSearch models. In Application model I add init with a check of the current instance. If its ApplicationSearch I skip initializations.
public function init()
{
if(!$this instanceof ApplicationSearch)
{
$this->id = hash('sha256', 123);
}
parent::init();
}
also as #mae commented below you can check for existence of search method in current instance, assuming you didn't add any method with name search to the non-search base model so the code becomes:
public function init()
{
// no search method is available in Gii generated Non search class
if(!method_exists($this,'search'))
{
$this->id = hash('sha256', 123);
}
parent::init();
}
I've read your question several times and I think there are some contradictions.
You want the defaults to be readable before and during validation and then you try init() or beforeSave(). So, assuming you just want to set the default values in the model so they can be present during the part of the life cycle as long as possible and not interfere with the derived classes, simply set them after initialising the object.
You can prepare separate method where all defaults are set and call it explicitly.
$model = new Model;
$model->setDefaultValues();
Or you can create static method to create model with all default values set and return the instance of it.
$model = Model::createNew();
Or you can pass default values to constructor.
$model = new Model([
'attribute1' => 'value1',
'attribute2' => 'value2',
]);
This is not much different from setting the attributes directly.
$model = new Model;
$model->attribute1 = 'value1';
$model->attribute2 = 'value2';
Everything depends on how much transparent would you like your model be to your controller.
This way attributes are set for the whole life cycle except the direct initialisation and it's not interfering with derived search model.
Just override __construct() method in your model like this:
class MyModel extends \yii\db\ActiveRecord {
function __construct(array $config = [])
{
parent::__construct($config);
$this->attr = 'defaultValue';
}
...
}
If you want to load default value from database you can put this code in your model
public function init()
{
parent::init();
if(!method_exists($this,'search')) //for checking this code is on model search or not
{
$this->loadDefaultValues();
}
}
You can prepare separate method where all defaults are set and call it explicitly.
$model = new Model;
if($model->isNewRecord())
$model->setDefaultValues();

Can't modifiy eloquent query result

So I got the following code in my controller's show function which just returns a page with the tags:
$page = Post::with('tags')->findOrFail($id);
$page->tags->lists('name');
return response($page);
When I try to to execute this, it won't change the tags key, which is an array with the tags from the eloquent belongsToMany relationship.
Why isn't this working? To me it seems pretty handy to just change a value like this.
When I change it to $page->test = $page->tags->lists('name') it will add the test key as usual.
How would I modify a eloquent value in a easy way?
What works pretty well for such cases is overriding toArray in your Model:
public function toArray(){
$array = parent::toArray();
$array['tags'] = $this->tags->lists('name');
return $array;
}
After the $page = Post::with('tags')->findOrFail($id); line is executed, $page->tags is going to be an Illuminate\Database\Eloquent\Collection object containing all the related Tags for the Post. From your provided code and question, it sounds like you want to then change $page->tags to be an array containing just the related tag names.
The statement $page->tags->lists('name') is only going to return an array of all the names of the related tags; it does not modify the underlying collection. If you wanted to modify the $page->tags attribute, you would need to assign it the result of your statement:
$page->tags = $page->tags->lists('name');
However, $page->tags was an attribute that was dynamically created and assigned by the Model, and is expected to hold the contents of a relationship. Manually modifying the contents like this may have unintended consequences, but I do not know.
Edit
The Model::toArray() method merges in the relationship information over the attribute information. So, you can change the attribute, but if you echo the model, the relationship information will show up over your attribute change.
$page->tags = $page->tags->lists('name');
// this will echo the tags attribute, which is now the array of tags
echo print_r($page->tags, true);
// this will echo the model, with the tags attribute being
// overwritten with the related data
echo $page;
One option would be to unset the attribute (which also unsets the relationship) and then reassign the attribute to your desired data:
$page = Post::with('tags')->findOrFail($id);
$temp = $page->tags;
unset($page->tags); // you must unset the attribute before reassigning it
$page->tags = $temp->lists('name');
return response($page);
A little bit cleaner would be to use a different attribute name:
$page = Post::with('tags')->findOrFail($id);
$page->tagNames = $page->tags->lists('name');
unset($page->tags);
return response($page);
And another option is to do what #lukasgeiter suggested and override the Model::toArray method:
class Post extends Model {
public function toArray() {
// call the parent functionality first
$array = parent::toArray();
if (isset($this->tags)) {
$array['tags'] = $this->tags->lists('name');
}
return $array;
}
}
If you want to change the output of one of the relationships in the toArray/toJson methods, then use accessor:
// in order to not show the underlying collection:
protected $hidden = ['tags'];
// in order to append accessor to toArray output
protected $appends = ['allTags'];
// mutate the collection to be just an array of tag names
public function getAllTagsAttribute()
{
$collection = return $this->getRelation('tags');
return ($relation) ? $collection->lists('name') : [];
}
then you will get simple array instead of collection when you do $page->allTags or in the toArray/toJson output, while not showing the real collection.
It is allTags not `tags, since the latter should remain eloquent dynamic property, so you can work with it as usual before outputting anything.
not sure if this helps. To be honest, I do not get your point. But I guess there is something wrong with this line:
$page->tags->lists('name');
If $page->tags is a belongsToMany relationship and you want to add more query conditions after this relationship, you should query like this:
$page->tags()->lists('name');

Resources