Eloquent resulting single search result as array not collection - laravel

I have some instances where a eloquent is resulting an single array not a collection. Although dd shows it as a collection with a single entry.
For example I have a query in a controller:
$pg = Page::with('getPanels')->where('slug',$slug)->get();
This will return a single result and works fine, so I pass this to a blade template. My complete function is
$pg = Page::with('getPanels')->where('slug',$slug)->get();
return view('front.page',['pg' => $pg]);
As soon as he template is brought in it will fall over at
if (!is_null($pg->headImage))
{$img = asset('images/pages')."/".$pg->headImage;}
and I will get
Property [headImage] does not exist on this collection instance.
If I change the line to
if (!is_null($pg[0]['headImage']))
it will continue OK. This is of course a pain as I would much rather use $pg->headImage.
Can someone enlighten me please?

I have sorted this and I hope it will help other people.
If I use
$pg = Page::with('getPanels')->where('slug',$slug)->first();
it will be just one result (naturally) and therefore
$pg->headImage
will fail as it wants
$pg[0]['headImage']
but if I change the eloquent instead of get(0 to first() (still just one result)
$pg = Page::with('getPanels')->where('slug',$slug)->first();
I can use $pg->headImage or what field I want.

Related

Why does my forget() method not remove object from collection?

I have a collection of "Tickets", using the random collection utility method I select one from the list. The "Tickets" collection should now remove (or forget) that randomly selected ticket so I can further process that collection. Using the forget method doesn't appear to do what is described in the documentation or (more likely I'm missing something).
Can someone spot whats wrong in my code?
$tickets = Tickets::all();
$total_winners = 5;
$selected_tickets = $tickets->random($total_winners);
$jackpot_winner = $selected_tickets->random();
$selected_tickets->forget($jackpot_winner->id); // this line should remove the $jackpot_winner
When I print the contents of $selected_tickets on lines 3 and lines 5, they have the exact same items, including the $jackpot_winner.
Forget function uses the collection key not the id from the model. To achieve what you want you may use this method:
$selected_tickets = $selected_tickets->except($jackpot_winner->id);
https://laravel.com/docs/8.x/collections#method-except

Is it inefficient to use count() in Laravel Blade?

This is more a "best practice" question.
My website has articles, and articles can have comments.
public function comments() {
return $this->hasMany('App\Comment', 'submission_id');
}
On the front page, where the articles are listed, the amount of comments are shown:
#if ($article->comments->count())
{{ $article->comments->count() }}
{{ $article->comments->count() == 1 ? 'comment' : 'comments' }}
#endif
And I was thinking to myself, in this example, is my database being queried 3 times for this simple code snippet?
Every time I get the comment count through this relationship, is it loading the entire array of comments and all of its columns? Because sometimes articles can have upwards of 1k+ comments.
If so, would it not be best practice to simply have a column in my articles database table that increments every time a comment is posted and then fetching that directly rather than through the relationship?
Not the way you're doing it. It depens on when you're using ->count(), and whether or not it's the Collection version, or the Builder version. See the following examples:
Example one - new query with comments() method:
$article = Article::first();
$article->comments()->count();
In this example, using $article->comments() causes a new query to be started (using the Builder class). No matter how you finish this query, i.e. using ->first(), ->get() or ->count() a new query will be executed.
Now, example two - new query with comments property:
$article = Article::first();
$article->comments->count();
In this example, $article->comments will be executing a new query, as relationship accessors act differently if the relationship has been loaded or not. In this case, it hasn't been loaded, so $article->comments returns a Builder instance instead of a Collection, so finishing the query will execute a new query.
Finally, example three - Collection access with comments property:
$article = Article::with(["comments"])->first();
$article->comments->count();
In this last example, $article->comments has been specified as loaded via the ->with(["comments"]) clause, so it is a Collection, and not a Builder instance. For this reason, calling ->count() is using the Collection count method, and does not perform an additional query

select certain columns from eloquent collection after the query has executed

Using Laravel 5.3, I have a model with the following function
public function myData() {
return $this->hasMany(MyData::class);
}
and in my collection I have the following
$my_data = MyModel->myData()->get();
All good so far. If I return $my_data I get an eloquent collection with three items.
What I need now though is to create a duplicate of that collection but containing only three of the fields.
I have tried several different things, each of which return an error. The following is the closest I have got, but this returns an empty array - I assume because the fields are located one level deeper than the collection object.
$new_collection = $my_data->only(['field_1', 'field_2', 'field_3']);
What would be the correct way to create a new collection containing all three items, each with only the three selected fields?
Thanks for your help
You could use map:
$slimmed_down = $collection->map(function ($item, $key) {
return [
'field_1' => $item->field_1,
'field_2' => $item->field_2,
'field_3' => $item->field_3
];
});
This will return a new Collection with just the values you want. As far as I know there isn't any other method that does what you want, so iterating over every item and selecting the fields this way is one of the few solutions.
The advantage of using map instead of a standard foreach loop is that when you use map it returns a new instance of Collection.
Edit:
After some thoughts and research about this, the problem you'll have created is that the all the values in the Collection aren't instances of anything anymore. If you don't mind this effect, an even prettier and faster way would be to do this:
$slimmed_down = $collection->toArray()->only(['field_1', 'field_2', 'field_3']);
This basically has the same result.
Using Laravel 9, I just had the same issue :
$my_data->only(['field_1', 'field_2', 'field_3']);
returning an empty array.
I solved it with :
$my_data->map->only(['field_1', 'field_2', 'field_3']);

laravel database query Does `where` always need `first()`?

I am new to laravel and confused about some query methods.
find($id) is useful and returns a nice array, but sometimes I need to select by other fields rather than id.
The Laravel document said I could use where('field', '=', 'value') and return a bunch of data, which is fine.
What I can't understand is why I need to add ->first() every time, even if I am pretty sure there is only one single row matches the query.
It goes like this:
$query->where(..)->orderBy(..)->limit(..) etc.
// you can chain the methods as you like, and finally you need one of:
->get($columns); // returns Eloquent Collection of Models or array of stdObjects
->first($columns); // returns single row (Eloquent Model or stdClass)
->find($id); // returns single row (Eloquent Model or stdClass)
->find($ids); // returns Eloquent Collection
// those are examples, there are many more like firstOrFail, findMany etc, check the api
$columns is an array of fields to retrieve, default array('*')
$id is a single primary key value
$ids is an array of PKs, this works in find method only for Eloquent Builder
// or aggregate functions:
->count()
->avg()
->aggregate()
// just examples here too
So the method depends on what you want to retrieve (array/collection or single object)
Also the return objects depend on the builder you are using (Eloquent Builder or Query Builder):
User::get(); // Eloquent Colleciton
DB::table('users')->get(); // array of stdObjects
even if I am pretty sure there is only one single row matches the query.
Well Laravel cant read your mind - so you need to tell it what you want to do.
You can do either
User::where('field', '=', 'value')->get()
Which will return all objects that match that search. Sometimes it might be one, but sometimes it might be 2 or 3...
If you are sure there is only one (or you only want the first) you can do
User::where('field', '=', 'value')->first()
get() returns an array of objects (multiple rows)
while
first() returns a single object (a row)
You can of course use get() when you know it will return only one row, but you need to keep that in mind when addressing the result:
using get()
$rez = \DB::table('table')->where('sec_id','=','5')->get();
//will return one row in an array with one item, but will be addressed as:
$myfieldvalue = $rez[0]->fieldname;
using first()
$rez = \DB::table('table')->where('sec_id','=','5')->first();
// will also return one row but without the array, so
$myfieldvalue = $rez->fieldname;
So it depends on how you want to access the result of the query: as an object or as an array, and also depends on what "you know" the query will return.
first() is the equivalent of LIMIT 1 at the end of your SELECT statement. Even if your query would return multiple rows, if you use first() it will only return the first row

To Array or Not To Array?

In my model I get data using something like:
$this->all();
This is then returned to my controller which makes a view:
return View::make('myView')
->with('data', $this->myModel->getAll());
My question is, what's best practice, should the model return eloquent object or an array? By calling ->toArray()?
Short answer is to leave as objects. It seems silly to me to convert to arrays when the objects can be used in the view.
Consider if you might need to foreach through a model's relationships - using an array you either don't have this or have to preload it (even if you won't always have to use it), using an object, you can choose to use the relationship if you want to.
Now, I'm aware this is related to your previous question regarding passing arrays vs. object to the view, but that is a different question entirely. In that question you're basically saying "sometimes I have one object and sometimes I have a collection of objects, how do i handle this in the view". To which my answer would be that you sure ensure the view always sees a collection (or array or whatever), but never to actually convert an object to an array.
In that situation, in the case where you only get one object, just wrap that object in a collection (or array) before it goes to the view and there you go - normalised data done easily.
To wrap your result in a collection
There are many ways of doing this which depend on how you're getting the data in the first place. If you're doing your own Eloquent calls then the simplest solution is to always use ->get() rather than sometimes using ->find() or ->first(). If you use ->get() even in times you expect a single result, it'll return a single result wrapped in a collection already.
However, if you're provided with this someties-object-sometimes-array then you'll have to manually do it. Again this has two different but very similar tehcniques which depends on whether the data is compatible with Eloquent and Collection or whether it's more raw PHP objects and arrays.
Eloquent-compatible
if ($data instanceof \Illuminate\Databse\Eloquent\Model) {
$data = new \Illuminate\Database\Eloquent\Collection($data);
}
Standard objects and arrays
if (!is_array($object)) {
$data = array($data);
}
It's really as simple as that.
In my opinion, the cleaner, elegant way is always returning an object.
In a model I would do something like this
class Employee extends Eloquent {
protected $table = 'employees';
}
In the controller
public function index(){
$employees= Employee::get();
return View::make('index')->with('employees', $employees);
}
In the view:
#foreach($employees as $employee)
{{ $employee->name }}
#endforeach
Simply, it depends what do you want to do with returned data. In most cases, we are traversing returned array in a view (with blade), and appending it to some HTML list. I had some issues with passed data to view and unit testing.
Simply, test is not accepting object, so I explicitly had to pass array.

Resources