Additional data members in Eloquent Collection not being copied - php

I have extended the basic Eloquent\Collection class as follows:
class BoogerCollection extends \Illuminate\Database\Eloquent\Collection {
// Metadata used by my collection
protected $some_array = [];
public function setArray(){
$this->some_array = [1,2,3];
}
public function getArray(){
return $this->some_array;
}
}
So, the following works as expected:
$collection = new BoogerCollection();
$collection->setArray();
print_r($collection->getArray()); // Prints out Array ( [0] => 1 [1] => 2 [2] => 3 )
However, if I then perform some operation on the collection which returns a modified copy of the collection, this metadata gets lost:
// Perform some operation on the collection that makes a copy
$collection = $collection->reverse();
print_r($collection->getArray()); // Prints out Array ( )
So, my $some_array member is not getting copied. I dug into the code for Eloquent\Collection and Support\Collection, and it looks like these methods all create new Collections by directly constructing them from the Collection's internal representation of the items:
in Eloquent's Support\Collection:
public function reverse()
{
// $this->items is the Collection's internal representation of the collection of items
return new static(array_reverse($this->items));
}
Does this mean that to have my metadata array copied as well, I must override every single method?

Extending Eloquent's Collection class doesn't mean it will be used by Eloquent to return a collection of models. For now, Eloquent will stick with it's Collection class, which is Illuminate\Database\Eloquent\Collection when returning a collection. If you look at Illuminate\Database\Eloquent\Builder's codes, you'll see the methods that returns a collection (get(), findMany(), etc.) will call the supplied model's newCollection() method which in turn returns an instance of Illuminate\Database\Eloquent\Collection and not your newly created subclass.
Your code: $collection = new BoogerCollection(); works because you're the one who instantiated your class. But when doing something like MyModel::all() won't return your BoogerCollection class as Eloquent will be the one to choose what Collection class it wants to return and right now it sticks with Illuminate\Database\Eloquent\Collection.

Related

Laravel Eloquent - Relationships and methods. When to use ->users() vs ->users [duplicate]

There is some basic understanding/theory here that I am missing.I don't understand the difference between these function calls:
$distributors = $store->distributors();
$distributors = $store->distributors;
$distributors = $store->distributors()->get();
$distributors = $store->distributors->get();
What I am trying to accomplis here is to get a list of the distributors for a store (a many to many relationship), and they get each distributors list of beers into one giant list.
foreach ($distributors as $distributor)
{
$available_beers = array_merge($distributor->beers(), $available_beers);
}
I don't know if that is the best way to do this and I can't get it to work. Similar to the first list of methods, I don't know if I need ->$beers or ->$beers()
Update
Thanks to everyone who answered! This will be a good reference for me going forward. My biggest lesson was the difference between getting a collection back, vs getting the query builder/relationship object back. For future reference to those who find this question, here is what I set up in my controller:
$store = $this->store->find($id)->first();
$distributors = $store->distributors;
$beers = [];
foreach ($distributors as $distributor){
$beers = array_merge($distributor->beers->lists('name', 'id'), $beers);
}
Short answer
$model->relation() returns the relationship object
$model->relation returns the result of the relationship
Long answer
$model->relation() can be explained pretty simple. You're calling the actual function you defined your relation with. Yours for distributor probably looks somewhat like this:
public function distributors(){
return $this->hasMany('Distributor');
}
So when calling $store->distributors() you just get the return value of $this->hasMany('Distributor') which is an instance of Illuminate\Database\Eloquent\Relations\HasMany
When do you use it?
You usually would call the relationship function if you want to further specify the query before you run it. For example add a where statement:
$distributors = $store->distributors()->where('priority', '>', 4)->get();
Of course you can also just do this: $store->distributors()->get() but that has the same result as $store->distributors.
Which brings me to the explanation of the dynamic relationship property.
Laravel does some things under the hood to allow you to directly access the results of a relationship as property. Like: $model->relation.
Here's what happens in Illuminate\Database\Eloquent\Model
1) The properties don't actually exist. So if you access $store->distributors the call will be proxied to __get()
2) This method then calls getAttribute with the property name getAttribute('distributors')
public function __get($key)
{
return $this->getAttribute($key);
}
3) In getAttribute it checks if the relationship is already loaded (exists in relations). If not and if a relationship method exists it will load the relation (getRelationshipFromMethod)
public function getAttribute($key)
{
// code omitted for brevity
if (array_key_exists($key, $this->relations))
{
return $this->relations[$key];
}
$camelKey = camel_case($key);
if (method_exists($this, $camelKey))
{
return $this->getRelationshipFromMethod($key, $camelKey);
}
}
4) In the end Laravel calls getResults() on the relation which then results in a get() on the query builder instance. (And that gives the same result as $model->relation()->get().
The direct answer to your question:
$store->distributors() will return the actual relationship object (\Illuminate\Database\Eloquent\Relations\BelongsToMany).
$store->distributors will be a collection containing the results of the relationship query (\Illuminate\Database\Eloquent\Collection).
$store->distributors()->get() will be a collection containing the results of the relationship query (\Illuminate\Database\Eloquent\Collection).
$store->distributors->get() should return an error since you're calling get() on a Collection object and the first parameter is not optional. If not an error, it should at least return null.
More information:
Given the following model:
class Store extends Eloquent {
public function distributors() {
return $this->belongsToMany('Distributor');
}
}
Calling the relationship method ($store->distributors()) will return to you the relationship (\Illuminate\Database\Eloquent\Relations\BelongsToMany) object. This is basically a query object which you can continue to modify, but you still need to call some type of method to get the results (e.g. get(), first(), etc).
However, accessing the relationship attribute ($store->distributors) will return to you a collection (\Illuminate\Database\Eloquent\Collection) object containing the results from executing the relationship query.
By default, the relationship attribute is created and assigned a value the first time it is accessed (known as "lazy loading"). So, the first time you access $store->distributors, behind the scenes it is executing the relationship query, storing the results in the $store->distributors attribute, and then returning those results. However, it only does this once. The next time you access $store->distributors, the attribute already contains the data, so that is what you are accessing.
To illustrate this:
// the following two statements will run the query twice
$r1 = $store->distributors()->get();
$r2 = $store->distributors()->get();
// the following two statements will run the query once.
// the first statement runs the query, populates $store->distributors, and assigns the variable
// the second statement just accesses the data now stored in $store->distributors
$r3 = $store->distributors;
$r4 = $store->distributors;
// at the end, $r1 == $r2 == $r3 == $r4
Relationships can also be "eager" loaded, using the with() method on the query. This is done to alleviate all of the extra queries that may be needed for lazy loading (known as the n+1 problem). You can read more about that here.
When you work with relationships with Eloquent the property is a collection (Illuminate\Database\Eloquent\Collection) of your relation white the method is a start of a new query.
Say your model looks like this:
class User extends Eloquent {
public function roles()
{
return $this->belongsToMany('Role');
}
}
If you try to access $user->roles, Eloquent will run the query and fetch all roles related to that user thanks to magic methods and returns an instance of Illuminate\Database\Eloquent\Collection. That class has a method called get, that's why $user->roles->get() works for you.
If you try to access the method, $user->roles(), you will instead get a query builder object so you can fine tune your query.
$user->roles()->whereIn('role_id', [1, 3, 4])->get();
That would only return roles where role_id is 1, 3 or 4.
So, the property returns a complete query and it results (Illuminate\Database\Eloquent\Collection) while the method lets you customize your query.
$distributors = $store->distributors();
Result of a method (function)
$distributors = $store->distributors;
Value of property (variable)
$distributors = $store->distributors()->get();
Take the first one, where it's the result of a method, if the method returns an object, this is a method in that object that was returned.
$distributors = $store->distributors->get();
If the property is an object, then it's calling a method in that property that's an object.
Re ->$beers vs ->$beers() that's a dynamic name of a property/method depending on what you're for. Just make a really rough guess at what you're doing, in your class you're going to have
$this->beers = array('bud','miller','sam');
and in your code using the $store object, you're actually going to go something like
$drink_type = 'beers';
$drink_list = $store->$drink_type;
And that will return $this->beers from $store, the same as writing $store->beers;
Imagine that the store class looks like this:
<?php
class Store {
public $distributors;
function __construct($distributors = array()) {
$this->distributors = $distributors;
}
public function distributors() {
return $this->distributors;
}
}
So the difference is:
$store = new Store(array('some guy', 'some other guy'));
$guys = $store->distributors; # accesing the $distributors property
$more = $store->distributors(); # calling the distributors() method.
The main difference is:
$distributors = $store->distributors() return instance of the relationship object like Illuminate\Database\Eloquent\Relations\BelongsToMany. You can use other conditions such as where after call this.
$store->distributors return instance of the collection Illuminate/Database/Eloquent/Collection. Laravel call the magic method __get under the hood. It will return a result of query relationship.
Maybe this will be usefull.
Access to method:
$object->method();
Access to property:
$object->property;

How to set doctrine associations?

I know that association property in entity is implements \Doctrine\Common\Collections\Collection. I know that in constructor such properties should be initialized:
$this->collection = new \Doctrine\Common\Collections\ArrayCollection()
I know that I can modify collections using ArrayCollection#add() and ArrayCollection#remove(). However I have a different case.
Suppose I have a new simple array of associative entities. Using existing methods I need to check every element in array: if entity collection has it. If no - add array element to entity collection. In addition to this, I need to check every element in entity collection. If any collection element is absent in new array, then I need to remove it from collection. So much work to do trivial thing.
What I want? To have the setProducts method implemented:
class Entity {
private $products;
// ... constructor
public function setProducts(array $products)
{
// synchronize $products with $this->products
}
}
I tried: $this->products = new ArrayCollection($products). However this makes doctrine remove all products and add those ones from $products parameter. I want similar result but without database queries.
Is there any built in solution in Doctrine for such case?
Edit:
I would like to have a method in ArrayCollection like fromArray which would merge elements in collections removing unneeded. This would just duplicate using add/remove calls for each element in collection argumen manually.
Doctrine collections do not have a "merge"-feature that will add/remove entities from an array or Collection in another Collection.
If you want to "simplify" the manual merge process you describe using add/remove, you could use array_merge assuming both arrays are not numeric, but instead have some kind of unique key, e.g. the entity's spl_object_hash:
public function setProducts(array $products)
{
$this->products = new ArrayCollection(
array_merge(
array_combine(
array_map('spl_object_hash', $this->products->toArray()),
$this->products->toArray()
),
array_combine(
array_map('spl_object_hash', $products),
$products->toArray()
)
)
);
}
You might want to use the product id instead of spl_object_hash as 2 products with the same id, but created as separate entities - e.g. one through findBy() in Doctrine and one manually created with new Product() - will be recognized as 2 distinct products and might cause another insert-attempt.
Since you replace the original PersistentCollection holding your previously fetched products with a new ArrayCollection this might still result in unneeded queries or yield unexpected results when flushing the EntityManager, though. Not to mention, that this approach might be harder to read than explicitly calling addElement/removeElement on the original Collection instead.
I would approach it by creating my own collection class that extends Doctrine array collection class:
use Doctrine\Common\Collections\ArrayCollection;
class ProductCollection extends ArrayCollection
{
}
In the entity itself you would initialise it in the __constructor:
public function __construct()
{
$this->products = new ProductCollection();
}
Here, Doctrine will you use your collection class for product results. After this you could add your own function to deal with your special merge, perhaps something:
public function mergeProducts(ProductCollection $products): ProductCollection
{
$result = new ProductCollection();
foreach($products as $product) {
$add = true;
foreach($this->getIterator() as $p) {
if($product->getId() === $p->getId()) {
$result->add($product);
$add = false;
}
}
if($add) {
$result->add($product);
}
}
return $result;
}
It will return a brand new product collection, that you can replace your other collection in the entity. However, if the entity is attached and under doctrine control, this will render SQL at the other end, if you want to play with the entity without risking database updates you need to detach the entity:
$entityManager->detach($productEntity);
Hopes this helps

Select specific fields from array of Eloquent collecton | Laravel

I have a collection object which includes the array of Model objects and I would like to select specific fields from the model.
Illuminate\Database\Eloquent\Collection Object
(
[items:protected] => Array
(
[0] => App\Model Object
[1] => App\Model Object
[2] => App\Model Object
)
)
Now I would like to select some fields from the model object. When I try to do the following syntax
print_r($collection->select('filed a', 'field b'));
then the following error occurs.
BadMethodCallException in Macroable.php line 74: Method select does
not exist.
I think select can work directly with the eloquent model but not with a collection.
Are you looking for only()
$filtered = $collection->only(['list', 'of', 'fields', 'to', 'keep']);
or perhaps mapWithKeys()
You are correct that select is not present on the Collection class.
What you can do is map, filter or transform the collection yourself e.g.
$whiteList = ['filed a', 'field b'];
$filledOnly = $collection->map(function ($item) use ($whiteList) {
$properties = get_object_vars($item);
foreach ($properties as $property) {
if(!in_array($property, $whiteList) {
unset($item->{property});
}
}
return $item;
});
The problem is in PHP once a property (or field) is set on an object, you really have to unset it or create new objects of the same class). This is why I came up with this solution.
Question is: How did you retrieve this collection in the first place, could you not add the select to the query itself?
The best would have been to select the fields you need before you execute the query on the model. However, you can use map() if you want to preserve the initial collection or transform() if you want to override the collection (for example):
$selected_fields = ['filed a', 'field b']
$models->map(function ($zn) use ($selected_fields) {
return $zn->newInstance(array_only($zn->getAttributes(), $selected_fields));
})->toArray();
newInstance() method creates a new empty instance of that model then getAttributes() retrieves the attributes present in the model. So the initial model is preserved in this process.
For reference sake, the implementation of newInstance() can be found on at Illuminate\Database\Eloquent\Model class and it is as follows (on Laravel 5.2):
/**
* Create a new instance of the given model.
*
* #param array $attributes
* #param bool $exists
* #return static
*/
public function newInstance($attributes = [], $exists = false)
{
// This method just provides a convenient way for us to generate fresh model
// instances of this current model. It is particularly useful during the
// hydration of new objects via the Eloquent query builder instances.
$model = new static((array) $attributes);
$model->exists = $exists;
return $model;
}

Difference between method calls $model->relation(); and $model->relation;

There is some basic understanding/theory here that I am missing.I don't understand the difference between these function calls:
$distributors = $store->distributors();
$distributors = $store->distributors;
$distributors = $store->distributors()->get();
$distributors = $store->distributors->get();
What I am trying to accomplis here is to get a list of the distributors for a store (a many to many relationship), and they get each distributors list of beers into one giant list.
foreach ($distributors as $distributor)
{
$available_beers = array_merge($distributor->beers(), $available_beers);
}
I don't know if that is the best way to do this and I can't get it to work. Similar to the first list of methods, I don't know if I need ->$beers or ->$beers()
Update
Thanks to everyone who answered! This will be a good reference for me going forward. My biggest lesson was the difference between getting a collection back, vs getting the query builder/relationship object back. For future reference to those who find this question, here is what I set up in my controller:
$store = $this->store->find($id)->first();
$distributors = $store->distributors;
$beers = [];
foreach ($distributors as $distributor){
$beers = array_merge($distributor->beers->lists('name', 'id'), $beers);
}
Short answer
$model->relation() returns the relationship object
$model->relation returns the result of the relationship
Long answer
$model->relation() can be explained pretty simple. You're calling the actual function you defined your relation with. Yours for distributor probably looks somewhat like this:
public function distributors(){
return $this->hasMany('Distributor');
}
So when calling $store->distributors() you just get the return value of $this->hasMany('Distributor') which is an instance of Illuminate\Database\Eloquent\Relations\HasMany
When do you use it?
You usually would call the relationship function if you want to further specify the query before you run it. For example add a where statement:
$distributors = $store->distributors()->where('priority', '>', 4)->get();
Of course you can also just do this: $store->distributors()->get() but that has the same result as $store->distributors.
Which brings me to the explanation of the dynamic relationship property.
Laravel does some things under the hood to allow you to directly access the results of a relationship as property. Like: $model->relation.
Here's what happens in Illuminate\Database\Eloquent\Model
1) The properties don't actually exist. So if you access $store->distributors the call will be proxied to __get()
2) This method then calls getAttribute with the property name getAttribute('distributors')
public function __get($key)
{
return $this->getAttribute($key);
}
3) In getAttribute it checks if the relationship is already loaded (exists in relations). If not and if a relationship method exists it will load the relation (getRelationshipFromMethod)
public function getAttribute($key)
{
// code omitted for brevity
if (array_key_exists($key, $this->relations))
{
return $this->relations[$key];
}
$camelKey = camel_case($key);
if (method_exists($this, $camelKey))
{
return $this->getRelationshipFromMethod($key, $camelKey);
}
}
4) In the end Laravel calls getResults() on the relation which then results in a get() on the query builder instance. (And that gives the same result as $model->relation()->get().
The direct answer to your question:
$store->distributors() will return the actual relationship object (\Illuminate\Database\Eloquent\Relations\BelongsToMany).
$store->distributors will be a collection containing the results of the relationship query (\Illuminate\Database\Eloquent\Collection).
$store->distributors()->get() will be a collection containing the results of the relationship query (\Illuminate\Database\Eloquent\Collection).
$store->distributors->get() should return an error since you're calling get() on a Collection object and the first parameter is not optional. If not an error, it should at least return null.
More information:
Given the following model:
class Store extends Eloquent {
public function distributors() {
return $this->belongsToMany('Distributor');
}
}
Calling the relationship method ($store->distributors()) will return to you the relationship (\Illuminate\Database\Eloquent\Relations\BelongsToMany) object. This is basically a query object which you can continue to modify, but you still need to call some type of method to get the results (e.g. get(), first(), etc).
However, accessing the relationship attribute ($store->distributors) will return to you a collection (\Illuminate\Database\Eloquent\Collection) object containing the results from executing the relationship query.
By default, the relationship attribute is created and assigned a value the first time it is accessed (known as "lazy loading"). So, the first time you access $store->distributors, behind the scenes it is executing the relationship query, storing the results in the $store->distributors attribute, and then returning those results. However, it only does this once. The next time you access $store->distributors, the attribute already contains the data, so that is what you are accessing.
To illustrate this:
// the following two statements will run the query twice
$r1 = $store->distributors()->get();
$r2 = $store->distributors()->get();
// the following two statements will run the query once.
// the first statement runs the query, populates $store->distributors, and assigns the variable
// the second statement just accesses the data now stored in $store->distributors
$r3 = $store->distributors;
$r4 = $store->distributors;
// at the end, $r1 == $r2 == $r3 == $r4
Relationships can also be "eager" loaded, using the with() method on the query. This is done to alleviate all of the extra queries that may be needed for lazy loading (known as the n+1 problem). You can read more about that here.
When you work with relationships with Eloquent the property is a collection (Illuminate\Database\Eloquent\Collection) of your relation white the method is a start of a new query.
Say your model looks like this:
class User extends Eloquent {
public function roles()
{
return $this->belongsToMany('Role');
}
}
If you try to access $user->roles, Eloquent will run the query and fetch all roles related to that user thanks to magic methods and returns an instance of Illuminate\Database\Eloquent\Collection. That class has a method called get, that's why $user->roles->get() works for you.
If you try to access the method, $user->roles(), you will instead get a query builder object so you can fine tune your query.
$user->roles()->whereIn('role_id', [1, 3, 4])->get();
That would only return roles where role_id is 1, 3 or 4.
So, the property returns a complete query and it results (Illuminate\Database\Eloquent\Collection) while the method lets you customize your query.
$distributors = $store->distributors();
Result of a method (function)
$distributors = $store->distributors;
Value of property (variable)
$distributors = $store->distributors()->get();
Take the first one, where it's the result of a method, if the method returns an object, this is a method in that object that was returned.
$distributors = $store->distributors->get();
If the property is an object, then it's calling a method in that property that's an object.
Re ->$beers vs ->$beers() that's a dynamic name of a property/method depending on what you're for. Just make a really rough guess at what you're doing, in your class you're going to have
$this->beers = array('bud','miller','sam');
and in your code using the $store object, you're actually going to go something like
$drink_type = 'beers';
$drink_list = $store->$drink_type;
And that will return $this->beers from $store, the same as writing $store->beers;
Imagine that the store class looks like this:
<?php
class Store {
public $distributors;
function __construct($distributors = array()) {
$this->distributors = $distributors;
}
public function distributors() {
return $this->distributors;
}
}
So the difference is:
$store = new Store(array('some guy', 'some other guy'));
$guys = $store->distributors; # accesing the $distributors property
$more = $store->distributors(); # calling the distributors() method.
The main difference is:
$distributors = $store->distributors() return instance of the relationship object like Illuminate\Database\Eloquent\Relations\BelongsToMany. You can use other conditions such as where after call this.
$store->distributors return instance of the collection Illuminate/Database/Eloquent/Collection. Laravel call the magic method __get under the hood. It will return a result of query relationship.
Maybe this will be usefull.
Access to method:
$object->method();
Access to property:
$object->property;

How to manually create a new empty Eloquent Collection in Laravel 4

How do we create a new Eloquent Collection in Laravel 4, without using Query Builder?
There is a newCollection() method which can be overridden by that doesn't really do job because that is only being used when we are querying a set result.
I was thinking of building an empty Collection, then fill it with Eloquent objects. The reason I'm not using array is because I like Eloquent Collections methods such as contains.
If there are other alternatives, I would love to hear them out.
It's not really Eloquent, to add an Eloquent model to your collection you have some options:
In Laravel 5 you can benefit from a helper
$c = collect(new Post);
or
$c = collect();
$c->add(new Post);
OLD Laravel 4 ANSWER
$c = new \Illuminate\Database\Eloquent\Collection;
And then you can
$c->add(new Post);
Or you could use make:
$c = Collection::make(new Post);
As of Laravel 5. I use the global function collect()
$collection = collect([]); // initialize an empty array [] inside to start empty collection
this syntax is very clean and you can also add offsets if you don't want the numeric index, like so:
$collection->offsetSet('foo', $foo_data); // similar to add function but with
$collection->offsetSet('bar', $bar_data); // an assigned index
I've actually found that using newCollection() is more future proof....
Example:
$collection = (new Post)->newCollection();
That way, if you decide to create your own collection class for your model (like I have done several times) at a later stage, it's much easier to refactor your code, as you just override the newCollection() function in your model
Laravel >= 5.5
This may not be related to the original question, but since it's one of the first link in google search, i find this helpful for those like me, who are looking for how to create empty collection.
If you want to manually create a new empty collection, you can use the collect helper method like this:
$new_empty_collection = collect();
You can find this helper in Illuminate\Support\helpers.php
snippet:
if (! function_exists('collect')) {
/**
* Create a collection from the given value.
*
* #param mixed $value
* #return \Illuminate\Support\Collection
*/
function collect($value = null)
{
return new Collection($value);
}
}
Just to add on to the accepted answer, you can also create an alias in config/app.php
'aliases' => array(
...
'Collection' => Illuminate\Database\Eloquent\Collection::class,
Then you simply need to do
$c = new Collection;
In Laravel 5 and Laravel 6 you can resolve the Illuminate\Database\Eloquent\Collection class out of the service container and then add models into it.
$eloquentCollection = resolve(Illuminate\Database\Eloquent\Collection::class);
// or app(Illuminate\Database\Eloquent\Collection::class). Whatever you prefer, app() and resolve() do the same thing.
$eloquentCollection->push(User::first());
For more information about understanding resolving objects out of the service container in laravel take a look here:
https://laravel.com/docs/5.7/container#resolving
I am using this way :
$coll = new Collection();
$coll->name = 'name';
$coll->value = 'value';
$coll->description = 'description';
and using it as normal Collection
dd($coll->name);
It is better to use the Injection Pattern and after $this->collection->make([]) than new Collection
use Illuminate\Support\Collection;
...
// Inside of a clase.
...
public function __construct(Collection $collection){
$this->collection = $collection;
}
public function getResults(){
...
$results = $this->collection->make([]);
...
}
What worked for me was to name the use namespace and instantiate it directly:
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
# Usage
$this->latest_posts = new EloquentCollection();
Allowed me to merge two data subsets of eloquent collection results, this maintains the relationships - a regular collection (collect()) loses relationship and probably some more metadata.
$limit = 5;
$this->latest_posts = new EloquentCollection();
$pinned_posts = PinnedPostReference::where('category', $category)->get();
if($pinned_posts->count() > 0) {
foreach($pinned_posts as $ppost) {
$this->latest_posts->push($ppost->post);
}
}
# Another Eloquent result set ($regular_posts)
foreach($regular_posts as $regular_post) {
$this->latest_posts->push($regular_post);
}

Categories