Laravel 5.6: Invoking eloquent relationships change the collection data - php

Is there a way to invoke eloquent relationship methods without changing the original eloquent collection that the method runs on? Currently I have to employ a temporary collection to run the method immutable and to prevent adding entire related record to the response return:
$result = Item::find($id);
$array = array_values($result->toArray());
$temp = Item::find($id);
$title = $temp->article->title;
dd($temp); //This prints entire article record added to the temp collection data.
array_push($array, $title);
return response()->json($array);

You are not dealing with collections here but with models. Item::find($id) will get you an object of class Item (or null if not found).
As far as I know, there is no way to load a relation without storing it in the relation accessor. But you can always unset the accessor again to delete the loaded relation (from memory).
For your example, this process yields:
$result = Item::find($id);
$title = $result->article->title;
unset($result->article);
return response()->json(array_merge($result->toArray(), [$title]));
The above works but is no very nice code. Instead, you could do one of the following three things:
Use attributesToArray() instead of toArray() (which merges attributes and relations):
$result = Item::find($id);
return response()->json(array_merge($result->attributesToArray(), [$result->article->title]));
Add your own getter method on the Item class that will return all the data you want. Then use it in the controller:
class Item
{
public function getMyData(): array
{
return array_merge($this->attributesToArray(), [$this->article->title]);
}
}
Controller:
$result = Item::find($id);
return response()->json($result->getMyData());
Create your own response resource:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ItemResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'title' => $this->article->title,
'author' => $this->article->author,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
Which can then be used like this:
return new ItemResource(Item::find($id));
The cleanest approach is option 3. Of course you could also use $this->attributesToArray() instead of enumerating the fields, but enumerating them will yield you security in future considering you might extend the model and do not want to expose the new fields.

I see two ways you can achieve that.
First, you can use an eloquent Resource. Basically it'll allow you to return exactly what you want from the model, so in your case, you'll be able to exclude the article. You can find the documentation here.
The second way is pretty new and is still undocumented (as fas i know), but it actually works well. You can use the unsetRelation method. So in your case, you just have to do:
$article = $result->article; // The article is loaded
$result->unsetRelation('article'); // It is unloaded and will not appear in the response
You can find the unsetRelation documentation here

There is not as far as I know. When dealing with Model outputs, I usually construct them manually like this:
$item = Item::find($id);
$result = $item->only('id', 'name', 'description', ...);
$result['title'] = $item->article->title;
return $result;
Should you need more power or a reusable solution, Resources are your best bet.
https://laravel.com/docs/5.6/eloquent-resources#concept-overview

Related

Call to a member function getNbr() on array

i want to select all users in the database that have the role ROLE_USER only but i get this problm when i call the function they say "Call to a member function getNbr() on null" i think bcoz i use Findby() , bcoz i use the same function in another call and it works great look at the code :
public function indexAction(Request $request)
{
$us = $this->getDoctrine()->getManager();
$locationus = $us->getRepository('AppBundle:Usr')->findBy(
[ 'roles' => ["ROLE_USER"] ]);
echo $nb_us = $locationus->getNbr();
if($authChecker->isGranted(['ROLE_ADMIN']))
{
return $this->render('settingAdmin/profiladmin.html.twig' , array(
'nb_us' => $nb_us,
));
}
and this is the other function in the UserRepository:
class UserRepository extends \Doctrine\ORM\EntityRepository
{
public function getNbr() {
return $this->createQueryBuilder('l')
->select('COUNT(l)')
->getQuery()
->getSingleScalarResult();
}
}
getNbr is method of UserRepository class, so it can be called only for this UserRepository class instance. This method returns total users count.
findBy returns array of entities (in you case all users with role ROLE_USER), not UserRepository class instance, so you can't use getNbr in context of this variable
If you want to get the length of array of entities (in you case all users with role ROLE_USER), just use count function:
echo $nb_us = count($locationus);
if($authChecker->isGranted(['ROLE_ADMIN']))
{
return $this->render('settingAdmin/profiladmin.html.twig' , array(
'nb_us' => $nb_us, 'locationus' => $locationus
));
}
There looks to be quite many things going on in the code there:
1) $us->getRepository('AppBundle:Usr') is probably typoed and should be $us->getRepository('AppBundle:User') instead (?) In general it would be safer to use $us->getRepository(AppBundle\User::class) so that syntax errors can be caught easier/earlier.
2) You are trying to invoke repository method on array with $locationus->getNbr() which is incorrect on multiple accounts (you cannot invoke functions on arrays - and repository methods cannot be invoked from entities either).
3) why is the code using echo?
4) as an additional note (assuming that this is roughly the full intended code), it would make sense to move all the getters & handling inside the if section so that the code will perform better (it doesn't do unnecessary database queries etc when the user doesn't have enough rights to access the view/information).
If I understood the intention correctly, in this case, the second repository function getNbr is superfluous here. If that is intending to just calculate the number of instances returned by the first find:
$locationus = $us->getRepository('AppBundle:User')->findBy(['roles' => ["ROLE_USER"] ]);
$nb_us = count($locationus);
Or alternatively (if you want to use and fix the getNbr repository function) then you don't need the first repository getter. This will require some rewriting of the repository function as well though:
$nb_us = $us->getRepository('AppBundle:User')->getNbr("ROLE_USER");

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

Yii2 ArrayHelper::toArray doesn't work recursively

Yii2 ArrayHelper's helper method toArray doesn't convert nested objects.
Here is my test code.
public function actionTest()
{
$product = \common\models\Product::find()
->where(['id' => 5779])
->with('firstImage')
->one();
$product = \yii\helpers\ArrayHelper::toArray($product);
print_r($product);
}
Recursive property is enabled by default.
public static array toArray ( $object, $properties = [], $recursive =
true)
So this piece of code should work correctly but it doesn't.
Action returns one level array without firstImage object.
What I'm doing wrong here?
PS:
Code was simplified for test purposes. I know that in this certain situation one can simply use asArray() method to get AR record in array.
You should use this instead :
$product = \common\models\Product::find()
->where(['id' => 5779])
->with('firstImage')
->asArray()
->one();
Read more about Retrieving Data in Arrays.
If your really want to use toArray(), and since a relation is not really an attribute or property, you should simply use the second parameter, e.g. :
$product = \yii\helpers\ArrayHelper::toArray($product, [
'common\models\Product' => [
// add needed properties here
// ...
'firstImage',
],
]);
Or, if you are using REST, you could override extraFields() in your model :
public function extraFields()
{
return ['firstImage'];
}
Read more about REST fields.

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);
}

Laravel Custom Model Methods

Whenever I add additional logic to Eloquent models, I end up having to make it a static method (i.e. less than ideal) in order to call it from the model's facade. I've tried searching a lot on how to do this the proper way and pretty much all results talk about creating methods that return portions of a Query Builder interface. I'm trying to figure out how to add methods that can return anything and be called using the model's facade.
For example, lets say I have a model called Car and want to get them all:
$cars = Car::all();
Great, except for now, let's say I want to sort the result into a multidimensional array by make so my result may look like this:
$cars = array(
'Ford' => array(
'F-150' => '...',
'Escape' => '...',
),
'Honda' => array(
'Accord' => '...',
'Civic' => '...',
),
);
Taking that theoretical example, I am tempted to create a method that can be called like:
$cars = Car::getAllSortedByMake();
For a moment, lets forget the terrible method name and the fact that it is tightly coupled to the data structure. If I make a method like this in the model:
public function getAllSortedByMake()
{
// Process and return resulting array
return array('...');
}
And finally call it in my controller, I will get this Exception thrown:
Non-static method Car::getAllSortedByMake() should not be called statically, assuming $this from incompatible context
TL;DR: How can I add custom functionality that makes sense to be in the model without making it a static method and call it using the model's facade?
Edit:
This is a theoretical example. Perhaps a rephrase of the question would make more sense. Why are certain non-static methods such as all() or which() available on the facade of an Eloquent model, but not additional methods added into the model? This means that the __call magic method is being used, but how can I make it recognize my own functions in the model?
Probably a better example over the "sorting" is if I needed to run an calculation or algorithm on a piece of data:
$validSPG = Chemical::isValidSpecificGravity(-1.43);
To me, it makes sense for something like that to be in the model as it is domain specific.
My question is at more of a fundamental level such as why is all()
accessible via the facade?
If you look at the Laravel Core - all() is actually a static function
public static function all($columns = array('*'))
You have two options:
public static function getAllSortedByMake()
{
return Car::where('....')->get();
}
or
public function scopeGetAllSortedByMake($query)
{
return $query->where('...')->get();
}
Both will allow you to do
Car::getAllSortedByMake();
Actually you can extend Eloquent Builder and put custom methods there.
Steps to extend builder :
1.Create custom builder
<?php
namespace App;
class CustomBuilder extends \Illuminate\Database\Eloquent\Builder
{
public function test()
{
$this->where(['id' => 1]);
return $this;
}
}
2.Add this method to your base model :
public function newEloquentBuilder($query)
{
return new CustomBuilder($query);
}
3.Run query with methods inside your custom builder :
User::where('first_name', 'like', 'a')
->test()
->get();
for above code generated mysql query will be :
select * from `users` where `first_name` like ? and (`id` = ?) and `users`.`deleted_at` is null
PS:
First Laurence example is code more suitable for you repository not for model, but also you can't pipe more methods with this approach :
public static function getAllSortedByMake()
{
return Car::where('....')->get();
}
Second Laurence example is event worst.
public function scopeGetAllSortedByMake($query)
{
return $query->where('...')->get();
}
Many people suggest using scopes for extend laravel builder but that is actually bad solution because scopes are isolated by eloquent builder and you won't get the same query with same commands inside vs outside scope. I proposed PR for change whether scopes should be isolated but Taylor ignored me.
More explanation :
For example if you have scopes like this one :
public function scopeWhereTest($builder, $column, $operator = null, $value = null, $boolean = 'and')
{
$builder->where($column, $operator, $value, $boolean);
}
and two eloquent queries :
User::where(function($query){
$query->where('first_name', 'like', 'a');
$query->where('first_name', 'like', 'b');
})->get();
vs
User::where(function($query){
$query->where('first_name', 'like', 'a');
$query->whereTest('first_name', 'like', 'b');
})->get();
Generated queries would be :
select * from `users` where (`first_name` like ? and `first_name` like ?) and `users`.`deleted_at` is null
vs
select * from `users` where (`first_name` like ? and (`id` = ?)) and `users`.`deleted_at` is null
on first sight queries look the same but there are not. For this simple query maybe it does not matter but for complicated queries it does, so please don't use scopes for extending builder :)
for better dynamic code, rather than using Model class name "Car",
just use "static" or "self"
public static function getAllSortedByMake()
{
//to return "Illuminate\Database\Query\Builder" class object you can add another where as you want
return static::where('...');
//or return already as collection object
return static::where('...')->get();
}
Laravel model custom methods -> best way is using traits
Step #1: Create a trait
Step #2: Add the trait to model
Step #3: Use the method
User::first()->confirmEmailNow()
app/Model/User.php
use App\Traits\EmailConfirmation;
class User extends Authenticatable
{
use EmailConfirmation;
//...
}
app/Traits/EmailConfirmation.php
<?php
namespace App\Traits;
trait EmailConfirmation
{
/**
* Set email_verified_at to now and save.
*
*/
public function confirmEmailNow()
{
$this->email_verified_at = now();
$this->save();
return $this;
}
}

Categories