Laravel Eloquent - setup both-ways relationships after loading? - laravel

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)

Related

Checking Model Relationships From Nested Resources

A common setup in Laravel routing is to use nested resources with route model binding. This allows great logical urls that represent the actual relationships that the models have with each other in the database. An example of this might be /library/section/book/. The book is owned by the section, the section is owned by the library. But when using route model binding, the ids of these resources are turned into models without any knowledge of each other. /1/7/234 would return the models of these resources but there is no guarantee that they are properly related. book 234 might not be owned by section 7 and section 7 might not be owned by library 1. I often have a method at the top of each controller that handles checking what I call relationship tests. This function would be found in the Book controller.
private function relationshipCheck($library, $section, $book)
{
if(library->id == $section->library_id) {
if($book != false) {
if($section->id == $book->section_id) {
return true;
} else {
return response()->json(["code" => 401], 401);
}
} else {
return true;
}
} else {
return response()->json(["code" => 401, 401);
}
}
What is the proper way to handle using these sorts of routes that represent relationships? Is there a more automated way to do this? Is there a good reason to just ignore everything but the last resource when the relationships are all one to many?
It's an old question, but still relevant today. There is a good answer here, which suggests explicitly binding the models in question. It's similar to another answer here, but with less abstraction.
Route::bind('section', function ($section, $route) {
return Section::where('library_id', $route->parameter('library'))->findOrFail($section);
});
Route::bind('book', function ($book, $route) {
return Book::where('Section_id', $route->parameter('section'))->findOrFail($book);
});
This will automatically work everywhere. If required, you could test for the upstream parameter to be found, and only perform the test in those cases (e.g. to cater for routes where only a book is specified).
Route::bind('book', function ($book, $route) {
$section = $route->parameter('section');
return $section ? Book::where('Section_id', $route->parameter('section'))->findOrFail($book) : $book;
});
...when using route model binding, the ids of these resources are turned into models without any knowledge of each other.
I am just starting to deal with this and here is how I've decided to make the approach.
Make it easier to check a model's relations
Laravel 5.3 has a method to determine if two models have the same ID and belong to the same table. is()
I submitted a pull request that would add relationship tools. You can see the changes to Illuminate\Database\Eloquent\Model that I am using in my project.
Create a middleware for nested routes with model binding.
Middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Exception\HttpResponseException;
/**
* Class EntityChain
*
* Determine if bound models for the route are related to
* each other in the order they are nested.
*
* #package App\Http\Middleware
*/
class EntityChain
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
// Array of the bound models for the route.
$parameters = array_filter($request->route()->parameters(),
function ($v) {
if ($v instanceof Model) return true;
return false;
});
// When there are two or more bound models.
if (count($parameters) > 1) {
// The first model is the parent.
$parent = array_shift($parameters);
while (count($parameters) > 0) {
// Assume the models are not related.
$pass = false;
// Set the child model.
$child = array_shift($parameters);
// Check if the parent model is related to the child.
if ($parent->is_related($child)) {
$pass = true;
}
$parent = $child;
// Fail on no relation.
if (!$pass) {
throw new HttpResponseException(response()->json('Invalid resource relation chain given.', 406));
}
}
}
return $next($request);
}
}
I've come across the need to do this before. This is how I've done it:
In my RouteServiceProvider.php file I have the following method:
private function addSlugBindingWithDependency(Router $router, $binding, $className, $dependency, $dependencyClassName, $dependencyField)
{
$router->bind($binding, function($slug, $route) use($className, $dependency, $dependencyClassName, $dependencyField) {
if (!is_string($slug)) {
throw new NotFoundHttpException;
}
$params = $route->parameters();
if (!$params || !isset($params[$dependency]) || get_class($params[$dependency]) != $dependencyClassName) {
throw new NotFoundHttpException;
}
$dependencyInstance = $params[$dependency];
$item = $className::where('slug', $slug)->where($dependencyField, $dependencyInstance->id)->first();
if (!$item) {
throw new NotFoundHttpException;
}
return $item;
});
}
It's a function that helps me set up a route/model binding for a slug, where that slug depends on another part of the URL/path. It works by taking a look at the already bound parts of the route and grabbing the instance of the model it had previously bound and uses it to check that the two are linked together.
I also have another, more basic helper function, addSlugBinding that I use to bind a slug to an object too.
You would use it in the boot method of the RouteServiceProvider class like this:
public function boot(Router $router)
{
parent::boot($router);
$this->addSlugBinding($router, 'librarySlug', 'App\Library');
$this->addSlugBindingWithDependency($router, 'sectionSlug', 'App\Section', 'librarySlug', 'App\Library', 'library_id');
$this->addSlugBindingWithDependency($router, 'bookSlug', 'App\Book', 'sectionSlug', 'App\Section', 'section_id');
}
Then in my routes file I might have the following:
Route::get('{librarySlug}/{sectionSlug}/{bookSlug}', function($librarySlug, $sectionSlug, $bookSlug) {
});
Note: I've done this when I've wanted nested URLs by slug rather than ID, but it can easily be adapted to use IDs.

Laravel relationships avoid query where foreign key is null

When eager loading, is it possible to avoid doing an extra query when the foreign key in a relationship is null and therefore does not match any related record? In my example I have a Product and User.
A Product is owned by a User but can also be optionally edited by a User. So my model looks like this:
class Product extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function editor()
{
return $this->belongsTo(User::class, 'editor_id');
}
}
When a product has not been edited, it's editor_id attribute is NULL.
If I hadn't been eager loading I know I could do something like the following:
$product = Product::find(1);
if (!is_null($product->editor_id)) {
// Load the relation
}
However, this isn't an option for me and I would ideally like to avoid an extra, unnecessary query being run when eager loading:
Query: select * from `users` where `users`.`id` in (?)
Bindings: [0]
I was wondering if something similar to the following would be possible?
public function editor()
{
if (!is_null($this->editor_id)) {
return $this->belongsTo(User::class, 'editor_id');
}
}
When doing the above I get this error:
Call to a member function addEagerConstraints() on a non-object
I'm guessing that this is because it's not guaranteed that this method returns a Relation object.
Thanks
I solved this by creating a new Relation subclass that implements the required methods but simply returns null when actually obtaining results:
namespace My\App;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class NullRelation extends Relation {
public function __construct() {}
public function addConstraints() {}
public function addEagerConstraints(array $models) {}
public function initRelation(array $models, $relation) {}
public function match(array $models, Collection $results, $relation) {
return [];
}
public function getResults() {
return null;
}
}
Then inside your relation method(s) you can check for null and return an instance of NullRelation instead:
public function editor() {
if ($this->editor_id === null) {
return new NullRelation();
} else {
return $this->belongsTo(User::class, 'editor_id');
}
}
It's a bit ugly and you'd be repeating youself a lot, so if I were using this in more than one place I'd probably create a subclass of the Model, then create versions of the belongsTo, hasOne methods and perform the check there:
public function belongsToIfNotNull($related, $foreignKey = null, $otherKey = null, $relation = null) {
if ($this->$foreignKey === null) {
return new NullRelation();
} else {
return parent::belongsTo($related, $foreignKey, $otherKey, $relation);
}
}
Finally in your modal that inherits the new subclass, your relation method simply becomes
public function editor() {
return $this->belongsToIfNotNull(User::class, 'editor_id');
}
Laravel Docs
Querying Relations When Selecting
When accessing the records for a model, you may wish to limit your results based on the existence of a relationship.
$posts= Post::has('editor')->get();
You may also specify an operator and a count:
$posts = Post::has('editor', '>=', 3)->get();

Eloquent relationships for not-existing model class

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;
}
// ...
}

Global filtering - how to use global scope in Laravel Eloquent

I have a published filter that I use for my articles. Guests can only view published articles, logged in users can view and apply filter (?published=0/1):
public function scopePublishedFilter($query)
{
if(!Auth::check()) $query->where('published', '=', 1);
else
{
$published = Input::get('published');
if (isset($published)) $query->where('published', '=', $published);
}
return $query;
}
I apply this in my ArticlesController:
public function index()
{
return View::make('articles.index', [
'articles' => Article::with('owner')
->with('category')
->with('tags')
->publishedFilter()
->get()
]);
}
And on the article relationships:
public function articles()
{
return $this->hasMany('Article')->publishedFilter();
}
But ideally I would like to only define it in the Article model itself, since it's easy to forget to include this filter when implementing new features or views.
How can I make sure that all returned articles from the Article model are run through this filter before returned?
UPDATE: Just use this: https://github.com/jarektkaczyk/laravel-global-scope for global scopes in L5+
Better way is a bit too long to paste it and works like SoftDeleting thing in the core.
Read this if you want it http://softonsofa.com/laravel-how-to-define-and-use-eloquent-global-scopes/
Short way: you need global scope for this. And here's how you do it in 2 steps (squashed a bit):
1 Create a class PublishedScope that implements ScopeInterface
class PublishedScope implements ScopeInterface {
public function apply(Builder $builder)
{
$table = $builder->getModel()->getTable();
$builder->where($table.'.published', '=', 1);
$this->addWithDrafts($builder);
}
public function remove(Builder $builder)
{
$query = $builder->getQuery();
$column = $builder->getModel()->getTable().'.published';
$bindingKey = 0;
foreach ((array) $query->wheres as $key => $where)
{
if ($this->isPublishedConstraint($where, $column))
{
unset($query->wheres[$key]);
$query->wheres = array_values($query->wheres);
$this->removeBinding($query, $bindingKey);
}
// Check if where is either NULL or NOT NULL type,
// if that's the case, don't increment the key
// since there is no binding for these types
if ( ! in_array($where['type'], ['Null', 'NotNull'])) $bindingKey++;
}
}
protected function removeBinding(Builder $query, $key)
{
$bindings = $query->getRawBindings()['where'];
unset($bindings[$key]);
$query->setBindings($bindings);
}
protected function addWithDrafts(Builder $builder)
{
$builder->macro('withDrafts', function(Builder $builder)
{
$this->remove($builder);
return $builder;
});
}
2 Boot that class in your Eloquent model by calling static::addGlobalScope(new AbcScope)
// the model
public static function boot()
{
parent::boot();
static::addGlobalScope(new PublishedScope);
}
If I were you I would use published_at column and check it for null instead of = 1, but that's up to you.
edit remove method updated - thanks to #Leon for pointing out unexpected behaviour, when using this scope together with SoftDeletingTrait. The problem is a bit deeper:
when you use this one with SoftDeletingScope or another one, that utilizes NULL or NOT NULL constraint and this scope is not the first one used (yes, order of use statements matters here), remove method will not work as expected. It will not remove any binding or not the one, that it should.
you can use trait and add your method or filter thing in booting method check the following
http://laravel.com/docs/4.2/eloquent#global-scopes

How to access model hasMany Relation with where condition?

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

Resources