Assume two Eloquent models in Laravel - user and location.
User:
class User extends Model
{
/**
* Relations that should be automatically loaded.
*
* #var array
*/
public $with = [
'locations'
];
/**
* Returns relation to the locations table.
*/
public function locations() {
return $this->belongsTo('App\Entities\Location');
}
}
Location:
class Location extends Model
{
/**
* Relations that should be automatically loaded.
*
* #var array
*/
public $with = [
'users'
];
/**
* Returns relation to the users table.
*/
public function users() {
return $this->hasMany('App\User');
}
}
As you can see, they both have the public attribute with - because as these models are driving an API, I want the users to always be returned with their location, and locations to always be returned with a list of their users.
However, as I thought might happen, this has led to the request timing out due to (I think) recursion. In other words, the location loads its users, and they load their locations, which then load their users, and so on.
Is there any way in Laravel to force it not to recurse into the same model that is doing the 'initial loading'?
I should mention that user also has another relation, profile, that I want to load in every query. So if, for example, there's a way to simply kill with eager-loading after a certain 'depth', that won't do it.
EDIT: I am aware that I can use User::with('location')->find(); to eager-load the relations, however this solution requires me to use that extra code everywhere I am retrieving users. I am looking for a solution that will let me always eager-load the relationships automatically.
It makes a infinite loop of loading for example you called a User now it is loading Location but when Location loaded then it again called User it has been loaded and the same loading process is going on so it makes a loop which is not even breaking. that why it is give a error
you can do like this
User::with('location')->find(..) // or get or any
You can resolve this problem, just put other model in belongsTo method.
Create new empty JustLocation model extends Location model.
Set protected $with = []; in JustLocation model
Change return $this->belongsTo('App\Entities\Location'); to return $this->belongsTo('App\Entities\JustLocation'); in the User model.
Related
I have developed a package to enable search method on Eloquent models with JSON.
The engine currently works in a way that a query is appended for supported parameters depending on values provided.
Builder::macro('search', function (Request $request) {
/**
* #var $this Builder
*/
$searcher = new Searcher($this, $request);
$searcher->search();
return $this;
});
Which in turn runs the following:
/**
* Perform the search
*
* #throws Exceptions\SearchException
*/
public function search(): void
{
$this->appendQueries();
Log::info('[Search] SQL: ' . $this->builder->toSql());
}
/**
* Append all queries from registered parameters
*
* #throws Exceptions\SearchException
*/
protected function appendQueries(): void
{
foreach ($this->requestParametersConfig->registered as $parameter) {
$requestParameter = $this->createRequestParameter($parameter);
$requestParameter->appendQuery();
}
}
protected function createRequestParameter($parameter): AbstractParameter
{
return new $parameter($this->request, $this->builder, $this->modelConfig);
}
Request parameters are registered within configuration file and point to request parameter classes to have everything modular.
Now this works great if used on a something like index endpoint or a custom search endpoint, but I would like to extend the functionality to show as well. The issue being here that with route model binding, the model is already loaded when I fetch it.
Let's take relationships for example. I have a request parameter which when used loads relationships on a given model:
www.example.com/api/contacts?relations=(phones)
will load phones relations on Contact model by doing this:
/**
* Append the query to Eloquent builder
* #throws SearchException
*/
public function appendQuery(): void
{
$arguments = $this->getArguments();
$this->builder->with($arguments);
}
But when I do a ->with on already loaded model followed by ->get(), I just get list of all models with loaded relation.
Is there a way to "tap into" already loaded model with query builder, or is it at this point already done deal, and I should pick up the ID on show route instead of resolving it, and then assemble the query?
After a lot of research of Laravel inner workings, what I asked initially isn't possible.
At the point where you call the method to interact with DB in some way, a query builder instance is made and depending on the method you called on the original model, the query is formed. After execution there is nothing to tap into as the object no longer exists.
Also, from the logical point of view, I have concluded that search makes no sense on already loaded models, so I did a concept switch there and disabled the package for already loaded models.
I have a table which will be addressed by a model in Laravel for stock movements. I would like to establish a relates to field which can be related to a number of different tables.
As per the attached diagram I need the relates_to_id to be one of the following...
Purchases.purchase_id
Invoices.invoice_id
And there may be more being added in the future as we find more records which may result in a stock movement occurring.
Now I have added a relates_to_type field to the database schema for the stock movements table so that I can specify which record type the particular movement record relates to. But I have been struggling to figure out how to establish the model for such a relationship to work or if it will even work that way or if I will need to have a separate field for each type of relationship as I wish to be able to read the related record through the ORM's related record scheme.
This is the exact use case for polymorphic relations, they supply your model as you wished for with an column indicating the relation type and the id of the related model.
See also this example from the laravel documentation:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Image extends Model {
/**
* Get all of the owning imageable models.
*/
public function imageable() {
return $this->morphTo();
}
}
class Post extends Model {
/**
* Get the post's image.
*/
public function image(){
return $this->morphOne('App\Image', 'imageable');
}
}
class User extends Model {
/**
* Get the user's image.
*/
public function image()
{
return $this->morphOne('App\Image', 'imageable');
}
}
Here every user and post can have an image and every image has an imageable which is either a user or a post.
I'm creating a system in which there are users, and user have many user roles. The user roles also contain permissions. Some fields are protected, so that they cannot be overwritten without the user possessing a specific permission.
For example, a user may have the attribute "email" which cannot be changed by the user, unless the user has the permission "update-email-address".
I originally intended to implement this concept as a trait or an abstract class, but I can't figure a way of doing this which doesn't involve either overloading the Eloquent Model constructor method, or else completely overloading another method.
What I'm hoping to do, is to be able to specify an array in a model like below, and by using a tract or extention, somehow prevent updating a model attribute:
/**
* The attributes that should only be updatable by given user with the
* specified permission
*
*/
public $update_only_by_permission = [
'email' => ['update-email-address'],
];
Is there a way to achieve this?
I stumbled across a way to provide a boot method for a trait extending a model, and was able to achieve this through the following:
Trait used on many Eloquent Models:
use Auth;
trait AttributeVisibleByPermissionTrait {
/**
* Stops updating of the object attributes if the authenticated
* user does not have the given permission provided in the
* $object->update_only_by_permission array.
*/
public static function bootAttributeVisibleByPermissionTrait(){
static::updating(function($object){
foreach ($object->update_only_by_permission as $attribute => $permissions) {
foreach ($permissions as $permission) {
if(!Auth::User()->can($permission)) {
unset($object->$attribute);
}
}
}
});
}
}
User Model:
class User extends Authenticatable
{
use AttributeVisibleByPermissionTrait;
/**
* The attributes that should only be updated by given user auth permissions
*
* #var array
*/
public $update_only_by_permission = [
'email' => ['update-email-address'],
];
}
I have 2 eloquent models:
EloquentUser
and
SharedEvents
They are both related by user_id
I'm attempting to set up and appends attribute in the SharedEvents model that will append the full_name of the user with whom the event has been shared.
For the sake of readability, I'm only including the appends components of my class
class SharedEvents extends Model {
protected $appends = ['fullName'];
/**
* #return BelongsTo
*/
public function user() : BelongsTo {
return $this->belongsTo('Path\To\EloquentUser', 'shared_with_id', 'user_id');
}
/**
* #return mixed
*/
public function getFullNameAttribute(){
return $this->user->user_full_name;
}
Unfortunately when I run this I'm getting back both the full name and the entire user model when I only want the full name.
Is there a way to avoid attaching the content of the user model?
It feels like you're trying to make columns from your EloquentUser model first class citizens in your SharedEvent model. You're getting close, but consider...
When working with relationships, this is a good way to be explicit:
Assuming user_full_name is an accessor on your User model:
// EloquentUser.php
// This will automatically add your accessor to every query
// as long as you select the columns the accessor is made
// up of
protected $appends = ['user_full_name'];
/**
* User Full Name Accessor
* #return string
*/
public function getUserFullNameAttribute()
{
return $this->first_name . ' ' . $this->last_name;
}
// SharedEvent.php
/**
* A SharedEvent belongs to an Eloquent User
* #return BelongsTo
*/
public function user() : BelongsTo
{
return $this->belongsTo('Path\To\EloquentUser', 'shared_with_id', 'user_id');
}
// Somewhere in your controller or wherever you want to access the data
$sharedEvents = SharedEvent::with(['user' => function ($query) {
$query->select('user_id', 'first_name', 'last_name');
}])->where(...)->get(['shared_with_id', ...other columns]); // you must select the related column here
This should get you the closest to what you want, but there are a couple of things you should know:
If user_full_name is an accessor, you need to select all of the columns that make up that accessor (as I mention above)
You must select the related keys (user_id in EloquentUser and shared_with_id in SharedEvent)
The $appends is necessary in EloquentUser here because you can't directly add an accessor to your sub query inside the closure.
Try to get comfortable with using a closure as the 2nd argument in your relationships. It's the best way to really be precise as to which columns you're selecting when you're loading relationships — Eloquent makes it really easy to be lazy and just do:
SharedEvent::with('user')->get();
which as you've see will just do a select * on both SharedEvent and your user relationship.
Another thing I've noticed when working with complex queries that use relationships is that you can quickly reach a point where it feels like you're fighting the framework. That's often a sign to consider simplifying ot just using raw SQL. Eloquent is powerful, but is just another tool in your programming tool belt. Remember that you have other tools at your disposal.
I was running into the same problem, and my first idea was to explicitly hide the (entire) associated model when fetching SharedEvent.
In your case, this would look like:
class SharedEvents extends Model {
protected $appends = ['fullName'];
protected $hidden = ['user'];
...
}
I actually decided against this since I am returning a lot of other attached models, so I decided on attaching the entire user, but I tested it and it works.
FYI, I'm not sure if the discrepancy is due to an older version of Laravel, but Eloquent actually expects $appends attributes to be in snake-case (https://laravel.com/docs/5.6/eloquent-serialization#appending-values-to-json):
Note that attribute names are typically referenced in "snake case", even though the accessor is defined using "camel case"
Can you try this?
public function user() : BelongsTo {
return $this->belongsTo('Path\To\EloquentUser', 'shared_with_id', 'user_id')->select('fullName','user_id');
}
After doing some research and experimentation, it doesn't look like there's a way to do this with Eloquent. The solution I ended up going with unfortunately is running an additional query.
/**
* This is inefficient, but I could not find a way to get the full name
* attribute without including all of user.
*
* #return string
*/
public function getFullNameAttribute(){
return EloquentUser::select('user_full_name')
->where('user_id', $this->shared_with_id)
->first()
->user_full_name;
}
I have an Eloquent model. Whenever it is retrieved from the database I would like to check whether a condition is fulfilled and set a model attribute if this is the case.
EDIT: I initially thought that the restoring event would be the right place to put the relevant logic, but as Tyler Crompton points out below, restoring is fired before a soft-deleted record is restored.
You have two valid options:
You can subclass \Illuminate\Datebase\Eloquent\Model to add such an event.
You can modify your copy of \Illuminate\Datebase\Eloquent\Model to add this (and possibly send an (unsolicited) pull request to Laravel on GitHub). According to Issue 1685, it looks as though they do not want it.
If I were you, I'd go with the first option and this is how I'd do it:
<?php namespace \Illuminate\Database\Eloquent;
abstract class LoadingModel extends Model {
/**
* Register a loaded model event with the dispatcher.
*
* #param \Closure|string $callback
* #return void
*/
public static function loaded($callback)
{
static::registerModelEvent('loaded', $callback);
}
/**
* Get the observable event names.
*
* #return array
*/
public function getObservableEvents()
{
return array_merge(parent::getObservableEvents(), array('loaded'));
}
/**
* Create a new model instance that is existing.
*
* #param array $attributes
* #return \Illuminate\Database\Eloquent\Model|static
*/
public function newFromBuilder($attributes = array())
{
$instance = parent::newFromBuilder($attributes);
$instance->fireModelEvent('loaded', false);
return $instance;
}
}
Just make sure the models in question subclass from LoadingModule. I have confirmed this to work as I found a great use case for it. Older versions of PHP returned MySQL values as strings. Normally, PHP will silently cast these to their respective numeric types in numeric operations. However, converting to JSON is not considered a numeric operation. The JSON values are represented as strings. This can cause problems for clients of my API. So I added a loaded event to my models to convert values to the correct type.
You could do this on the way in, or the way out. It seems like you wanted it stored in the database, so you could use mutators.
class Foo extends Eloquent {
public function setBAttribute($value)
{
if ($this->attributes['a'] == $this->attributes['b']) {
$this->attributes['b'] = 1;
}
}
}
When ever B is set, it will check against A, and store 1 in B.
Side note: Note the B between set and attribute
I think this is the best option you can use.
The retrieved event will fire when an existing model is retrieved from the database. for example, if you have a User model in your application you must define code like below in User Model.
self::retrieved(function (self $model){
//all your code here
});