Let's consider the following example: a thread has posts, and the posts also have a "thread" relation. The title of each post must include the title of the parent thread.
class Thread extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}
class Post extends Model
{
public function thread()
{
return $this->belongsTo(Thread::class);
}
public function getTitleAttribute(string $title): string
{
return $this->thread->title . ': ' . $title;
}
}
What I want to achieve:
//when we load the posts using the thread...
$posts = $thread->posts;
//...I want the "thread" relation of each post to be automatically set to $thread, so that:
$posts->first()->thread === $thread //true
By default it's not true. And if we do this:
$array = $thread->posts->toArray();
this will cause loading of the thread for each post one by one from DB which is super non-optimal. Is there some elegant Laravel technique to setup relations of the just loaded models?
You can lazy load them like this
$posts = $thread->posts()->with('thread')->get();
If you dont want the extra query, you can use map()
$thread->posts->map(function($post) use ($thread) {
return $post->setRelation('thread', $thread);
});
This will lead to the same amount of object but will also lead to loop of references.
//this is defined and doesn't use more object or launch other queries
$thread->posts->first()->thread->posts()->first()->thread;
if you want to Automate it, I suggest you create a function on Thread model to get the posts threaded.
public function loadThreadedPosts()
{
$this->posts->map(function($post) {
return $post->setRelation('thread', $this);
});
return $this;
}
//then you can
$thread->loadThreadedPosts()->posts;
If you want it to automatically be done when you specifically call for the relation "posts" on the Thread::class model, add this method to your Thread::class to overwrite the function present in the Trait HasAttributes at your own risk
/**
* Get a relationship value from a method.
*
* #param string $method
* #return mixed
*
* #throws \LogicException
*/
protected function getRelationshipFromMethod($method)
{
$relation = $this->$method();
if (! $relation instanceof Relation) {
if (is_null($relation)) {
throw new LogicException(sprintf(
'%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?', static::class, $method
));
}
throw new LogicException(sprintf(
'%s::%s must return a relationship instance.', static::class, $method
));
}
return tap($relation->getResults(), function ($results) use ($method) {
if ($method == "posts") {
$results->map(function($post) {
return $post->setRelation('thread', $this);
});
}
$this->setRelation($method, $results);
});
}
Hope you understand that this overwrites a vendor method and might lead to future issues, also I dont think that this one method works with eager loading (for example: Thread::with('posts')->get()) and I dont know what else might get broken/have unexpected behavior.
As I said, at your own risk (bet/hope ->loadThreadedPosts() looks more interesting now)
I've decided to make some unit tests for my app and I have a problem.
I have logic part
private $model;
__construct($model)
{
$this->model = $model;
}
public function getData()
{
$data = $this->model->getData();
return $data;
}
model
public function $getData()
{
return self::where('id', '=', 1)->first();
}
and now I would like to make unit test.
I know how to mock right model
$collection = new Collection();
$collection->push(
new Model(
)
);
but have no idea how to put data into it. I've tried put data as attribute but it throws me MassAssigmentException
Would be grateful if someone know and share his knowledge.
if I override eloquent's first() method I can not call the method statically (Through facade) as I would expect. I would expect that implemented __callStatic() method will be used (implemented in Illuminate\Database\Eloquent\Model), but that's not the case.
So I tried to implement the magic method myself. I still cannot access overrided first() method statically.
ErrorException: Non-static method Entity::first() should not be
called statically, assuming $this from incompatible context.
class Entity extends Eloquent
{
public function first($columns = ['*'])
{
if (Cache::tags(static::getTags())->has('first')) :
return Cache::tags(static::getTags())->get('first');
endif;
$result = parent::first($columns);
if ($result) :
Cache::tags(static::getTags())->put('first', $result, with(new static)->ttl);
endif;
return $result;
}
public static function __callStatic($method, $parameters)
{
$instance = new static;
return call_user_func_array(array($instance, $method), $parameters);
}
}
What I'm missing here?
I've expressed my doubts about the necessity to override first() for caching purposes, but of course it is technically possible to do it.
You have to create your own Builder class for that:
class MyBuilder extends Illuminate\Database\Eloquent\Builder {
/**
* Execute the query and get the first result.
*
* #param array $columns
* #return \Illuminate\Database\Eloquent\Model|static|null
*/
public function first($columns = array('*'))
{
if ($someCondition) :
return 1;
endif;
return parent::first($columns);
}
}
And then make your model use this builder by implementing the newEloquentBuilder method:
public function newEloquentBuilder($query)
{
return new MyBuilder($query);
}
I would like to have in my applications many models/modules but some of them would be removed for some clients.
Now I have such relation:
public function people()
{
return $this->hasMany('People', 'model_id');
}
and when I run $model = Model::with('people')->get(); it is working fine
But what if the People model doesn't exist?
At the moment I'm getting:
1/1 ErrorException in ClassLoader.php line 386: include(...): failed
to open stream: No such file or directory
I tried with
public function people()
{
try {
return $this->hasMany('People', 'model_id');
}
catch (FatalErrorException $e) {
return null;
}
}
or with:
public function people()
{
return null; // here I could add checking if there is a Model class and if not return null
}
but when using such method $model = Model::with('people')->get(); doesn't work.
I will have a dozens of relations and I cannot have list of them to use in with. The best method for that would be using some empty relation (returning null) just to make Eloquent not to do anything but in this case Eloquent still tries to make it work and I will get:
Whoops, looks like something went wrong.
1/1 FatalErrorException in Builder.php line 430: Call to a member function
addEagerConstraints() on null
Is there any simple solution for that?
The only solution I could come up with is creating your own Eloquent\Builder class.
I've called it MyBuilder. Let's first make sure it gets actually used. In your model (preferably a Base Model) add this newEloquentBuilder method:
public function newEloquentBuilder($query)
{
return new MyBuilder($query);
}
In the custom Builder class we will override the loadRelation method and add an if null check right before addEagerConstraints is called on the relation (or in your case on null)
class MyBuilder extends \Illuminate\Database\Eloquent\Builder {
protected function loadRelation(array $models, $name, Closure $constraints)
{
$relation = $this->getRelation($name);
if($relation == null){
return $models;
}
$relation->addEagerConstraints($models);
call_user_func($constraints, $relation);
$models = $relation->initRelation($models, $name);
$results = $relation->getEager();
return $relation->match($models, $results, $name);
}
}
The rest of the function is basically the identical code from the original builder (Illuminate\Database\Eloquent\Builder)
Now simply add something like this in your relation function and it should all work:
public function people()
{
if(!class_exist('People')){
return null;
}
return $this->hasMany('People', 'model_id');
}
Update: Use it like a relationship
If you want to use it like you can with a relationship it gets a bit more tricky.
You have to override the getRelationshipFromMethod function in Eloquent\Model. So let's create a Base Model (Your model obviously needs to extend it then...)
class BaseModel extends \Illuminate\Database\Eloquent\Model {
protected function getRelationshipFromMethod($key, $camelKey)
{
$relations = $this->$camelKey();
if ( $relations instanceof \Illuminate\Database\Eloquent\Collection){
// "fake" relationship
return $this->relations[$key] = $relations;
}
if ( ! $relations instanceof Relation)
{
throw new LogicException('Relationship method must return an object of type '
. 'Illuminate\Database\Eloquent\Relations\Relation');
}
return $this->relations[$key] = $relations->getResults();
}
}
Now we need to modify the relation to return an empty collection
public function people()
{
if(!class_exist('People')){
return new \Illuminate\Database\Eloquent\Collection();
}
return $this->hasMany('People', 'model_id');
}
And change the loadRelation function in MyBuilder to check for the type collection instead of null
protected function loadRelation(array $models, $name, Closure $constraints)
{
$relation = $this->getRelation($name);
if($relation instanceof \Illuminate\Database\Eloquent\Collection){
return $models;
}
// ...
}
I created a model Game using a condition / constraint for a relation as follows:
class Game extends Eloquent {
// many more stuff here
// relation without any constraints ...works fine
public function videos() {
return $this->hasMany('Video');
}
// results in a "problem", se examples below
public function available_videos() {
return $this->hasMany('Video')->where('available','=', 1);
}
}
When using it somehow like this:
$game = Game::with('available_videos')->find(1);
$game->available_videos->count();
everything works fine, as roles is the resulting collection.
MY PROBLEM:
when I try to access it without eager loading
$game = Game::find(1);
$game->available_videos->count();
an Exception is thrown as it says "Call to a member function count() on a non-object".
Using
$game = Game::find(1);
$game->load('available_videos');
$game->available_videos->count();
works fine, but it seems quite complicated to me, as I do not need to load related models, if I do not use conditions within my relation.
Have I missed something? How can I ensure, that available_videos are accessible without using eager loading?
For anyone interested, I have also posted this issue on http://forums.laravel.io/viewtopic.php?id=10470
I think that this is the correct way:
class Game extends Eloquent {
// many more stuff here
// relation without any constraints ...works fine
public function videos() {
return $this->hasMany('Video');
}
// results in a "problem", se examples below
public function available_videos() {
return $this->videos()->where('available','=', 1);
}
}
And then you'll have to
$game = Game::find(1);
var_dump( $game->available_videos()->get() );
I think this is what you're looking for (Laravel 4, see http://laravel.com/docs/eloquent#querying-relations)
$games = Game::whereHas('video', function($q)
{
$q->where('available','=', 1);
})->get();
//use getQuery() to add condition
public function videos() {
$instance =$this->hasMany('Video');
$instance->getQuery()->where('available','=', 1);
return $instance
}
// simply
public function videos() {
return $this->hasMany('Video')->where('available','=', 1);
}
Just in case anyone else encounters the same problems.
Note, that relations are required to be camelcase. So in my case available_videos() should have been availableVideos().
You can easily find out investigating the Laravel source:
// Illuminate\Database\Eloquent\Model.php
...
/**
* Get an attribute from the model.
*
* #param string $key
* #return mixed
*/
public function getAttribute($key)
{
$inAttributes = array_key_exists($key, $this->attributes);
// If the key references an attribute, we can just go ahead and return the
// plain attribute value from the model. This allows every attribute to
// be dynamically accessed through the _get method without accessors.
if ($inAttributes || $this->hasGetMutator($key))
{
return $this->getAttributeValue($key);
}
// If the key already exists in the relationships array, it just means the
// relationship has already been loaded, so we'll just return it out of
// here because there is no need to query within the relations twice.
if (array_key_exists($key, $this->relations))
{
return $this->relations[$key];
}
// If the "attribute" exists as a method on the model, we will just assume
// it is a relationship and will load and return results from the query
// and hydrate the relationship's value on the "relationships" array.
$camelKey = camel_case($key);
if (method_exists($this, $camelKey))
{
return $this->getRelationshipFromMethod($key, $camelKey);
}
}
This also explains why my code worked, whenever I loaded the data using the load() method before.
Anyway, my example works perfectly okay now, and $model->availableVideos always returns a Collection.
If you want to apply condition on the relational table you may use other solutions as well.. This solution is working from my end.
public static function getAllAvailableVideos() {
$result = self::with(['videos' => function($q) {
$q->select('id', 'name');
$q->where('available', '=', 1);
}])
->get();
return $result;
}
public function outletAmenities()
{
return $this->hasMany(OutletAmenities::class,'outlet_id','id')
->join('amenity_master','amenity_icon_url','=','image_url')
->where('amenity_master.status',1)
->where('outlet_amenities.status',1);
}
I have fixed the similar issue by passing associative array as the first argument inside Builder::with method.
Imagine you want to include child relations by some dynamic parameters but don't want to filter parent results.
Model.php
public function child ()
{
return $this->hasMany(ChildModel::class);
}
Then, in other place, when your logic is placed you can do something like filtering relation by HasMany class. For example (very similar to my case):
$search = 'Some search string';
$result = Model::query()->with(
[
'child' => function (HasMany $query) use ($search) {
$query->where('name', 'like', "%{$search}%");
}
]
);
Then you will filter all the child results but parent models will not filter.
Thank you for attention.
Model (App\Post.php):
/**
* Get all comments for this post.
*/
public function comments($published = false)
{
$comments = $this->hasMany('App\Comment');
if($published) $comments->where('published', 1);
return $comments;
}
Controller (App\Http\Controllers\PostController.php):
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function post($id)
{
$post = Post::with('comments')
->find($id);
return view('posts')->with('post', $post);
}
Blade template (posts.blade.php):
{{-- Get all comments--}}
#foreach ($post->comments as $comment)
code...
#endforeach
{{-- Get only published comments--}}
#foreach ($post->comments(true)->get() as $comment)
code...
#endforeach