This is my model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Folder extends Model
{
protected $with = ['children'];
public function children()
{
return $this->hasMany('App\Models\Folder','parent_id')/*->select('title','path','size','ext','isFolder')*/;
}
public function user()
{
return $this->belongsTo('App\User','user_id');
}
public static function tree() {
return static::with(implode('.', array_fill(0, 4, 'children')))->where('parent_id', '=', NULL)->get();
}
}
and i'm displaying the results with this function :
$folders = Folder::tree();
I'm trying to display every folder and it's content whether it's a file or another directory is there a way to do this without having to do a foreach loop for every level ?
This can be done with local scopes. If you are also familiar with recursion the code I'm going to write will be easy to understand. Anwyay everything is explained with the comments.
/**
* Retrieve recursively the related children building a tree.
* Pay attention! this scope may cause A LOT of performance problems. Expecially when there are a lot of children / nephews
*
* #param Buider $query
* #param integer $deep
* #param integer $index
* #return Builder
*/
public function scopeTree($query, $deep, $index = 0) {
// Increment index
$index++;
// Current model is root, let's find the id for base case.
$id = $this->getKey();
// Now build the recursive nested query
return $query->whereHas('children', function($query) use ($deep, $index, $id) {
// If deep is lower than index, it means we have to add a node to the tree
if($deep < $index) {
// Recursion
$query->tree($deep, $index);
} else {
// Base case
$query->where('parent_id', '=', $id);
}
});
}
Anyway this function may cause A LOT of performance problems, so it should be better not to go "too deep" if there are a lot of records inside the database.
Related
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 have this relationship
A Movement can have multiples steps
A Step can belongs to multiples Movements
So a had to create a pivot table and a belongsToMany relationship, but my pivot table have some extras columns, like finished and order
I want to have two relationships, one to get all steps from a movement and another one to get the current step from the movement (the last finished step)
I know how to get all steps
public function steps()
{
return $this->belongsToMany(MovementStep::class, 'movement_movement_steps')
->withPivot('order', 'finished')
->orderBy('pivot_order');
}
But how about the current step? I need this kind of relationship, but returning only one record and be able to eager load it cause I'm passing it to vue.js
public function current_step()
{
return $this->belongsToMany(MovementStep::class, 'movement_movement_steps')
->withPivot('order', 'finished')
->where('finished', true)
->orderBy('pivot_order', 'desc');
}
Notice, I'd like to do that without extras packages
alternative solution, but with extra package: Laravel hasOne through a pivot table (not the answer marked as correct, the answer from #cbaconnier)
A different approach from the answer provided by #mrhn is to create a custom relationship. Brent from Spatie did an excellent article about it
Although my answer will do the exact same queries than the one provided by staudenmeir's package it makes me realized that either you use the package, this answer or #mrhn answer, you may avoid the n+1 queries but you may still ends up will a large amount of hydrated models.
In this scenario, I don't think it's possible to avoid one or the other approach. The cache could be an answer though.
Since I'm not entirely sure about your schema, I will provide my solution using the users-photos example from my previous answer.
User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function photos()
{
return $this->belongsToMany(Photo::class);
}
public function latestPhoto()
{
return new \App\Relations\LatestPhotoRelation($this);
}
}
LastestPhotoRelation.php
<?php
namespace App\Relations;
use App\Models\User;
use App\Models\Photo;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
class LatestPhotoRelation extends Relation
{
/** #var Photo|Builder */
protected $query;
/** #var User */
protected $user;
public function __construct(User $user)
{
parent::__construct(Photo::query(), $user);
}
/**
* #inheritDoc
*/
public function addConstraints()
{
$this->query
->join(
'user_photo',
'user_photo.photo_id',
'=',
'photos.id'
)->latest();
// if you have an ambiguous column name error you can use
// `->latest('movement_movement_steps.created_at');`
}
/**
* #inheritDoc
*/
public function addEagerConstraints(array $users)
{
$this->query
->whereIn(
'user_photo.user_id',
collect($users)->pluck('id')
);
}
/**
* #inheritDoc
*/
public function initRelation(array $users, $relation)
{
foreach ($users as $user) {
$user->setRelation(
$relation,
null
);
}
return $users;
}
/**
* #inheritDoc
*/
public function match(array $users, Collection $photos, $relation)
{
if ($photos->isEmpty()) {
return $users;
}
foreach ($users as $user) {
$user->setRelation(
$relation,
$photos->filter(function (Photo $photo) use ($user) {
return $photo->user_id === $user->id; // `user_id` came with the `join` on `user_photo`
})->first() // Photos are already DESC ordered from the query
);
}
return $users;
}
/**
* #inheritDoc
*/
public function getResults()
{
return $this->query->get();
}
}
Usage
$users = \App\Models\User::with('latestPhoto')->limit(5)->get();
The main difference from Brent's article, is that instead of using a Collection we are returning the latest Photo Model.
Laravel has a way to create getters and setters that act similar to columns in the database. These can perfectly solve your problem and you can append them to your serialization.
So instead your current_step is gonna be an accessor (getter). The syntax is getCurrentStepAttribute() for the function which will make it accessible on the current_step property. To avoid N + 1, eager load the steps when you retrieve the model(s) with the with('steps') method. Which is better than running it as a query, as it will execute N times always.
public function getCurrentStepAttribute() {
return $this->steps
->where('finished', true)
->sortByDesc('pivot_order')
->first();
}
Now you can use the append property on the Movement.php class, to include your Eloquent accessor.
protected $appends = ['current_step'];
Ok, this is weird... You ready?
I have an item type on my site, lets call it SomeItem
It can have tags associated with it via a one-to-many relationship.
The sorts of queries that Laravel builds when dealing with tags for SomeItem are like this, for instance in response to route api/someitem/10:
select `tags`.*, `someitem_tag`.`someitem_id` as `pivot_someitem_id`, `someitem_tag`.`tag_id` as `pivot_tag_id` from `tags` inner join `someitem_tag` on `tags`.`id` = `someitem_tag`.`tag_id` where `someitem_tag`.`someitem_id` in (10)
When I create a second Item with identical settings - let's call it AnotherItems - it treats the database query for extracting tags in a different manner, using a different syntax in the queries. Extremely weird.
(and yes, I have an s at the end of the model name...)
For instance, this route api/anotheritems/1
produces this error:
Base table or view not found: 1146 Table 'mysite.tag_anotheritems' doesn't exist (SQL: select `tags`.*, `tag_anotheritems`.`anotheritems_id` as `pivot_anotheritems_id`, `tag_anotheritems`.`tag_id` as `pivot_tag_id` from `tags` inner join `tag_anotheritems` on `tags`.`id` = `tag_anotheritems`.`tag_id` where `tag_anotheritems`.`anotheritems_id` in (1))
See what is happening? Of course I am getting this error - in the database this tag table for AnotherItems is created as anotheritems_tag. That is analogous to SomeItem.
How on earth can Laravel be using syntax someitem_tag for one item but tag_anotheritems for another item??? WTF?
First let me show you how SomeItem is set up.
Here is the database structure related to Tags:
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSomeItemTagTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('someitem_tag', function (Blueprint $table) {
$table->integer('tag_id')->unsigned();
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
$table->integer('someitem_id')->unsigned();
$table->foreign('someitem_id')->references('id')->on('someitems')->onDelete('cascade');
$table->primary(array('tag_id', 'someitem_id'));
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::drop('someitem_tag');
}
}
There is a Tags model/class that has this:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
protected $fillable = ['name'];
protected $hidden = [];
public $timestamps = false;
public function someitems()
{
return $this->belongsToMany(SomeItem::class);
}
}
And here is some relevant lines for SomeItem model/class:
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use App\Presenters\Presentable;
use Illuminate\Notifications\Notifiable;
use Auth;
class Exercise extends Model
implements Presentable
{
use Traits\SerializesUniversalDate;
use Traits\Presents;
use Notifiable;
protected $presenter = 'App\Presenters\SomeItemPresenter';
protected $fillable = ['title', etc];
protected $hidden = [];
public function parentitem()
{
return $this->belongsTo(ParentItem::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class);
}
/**
* Update lesson tag array.
*
* #param array \App\Tag $tags
* #return void
*/
public function updateTags($tagsArray)
{
foreach ($tagsArray as &$value)
{
$tag = Tag::where('name', $value['name'])->first();
if (is_null($tag))
{
$tag = new Tag([
'name' => $value['name']
]);
$tag->save();
}
if (!$this->tags->contains($tag->id))
{
$this->tags()->attach($tag->id);
}
}
foreach($this->tags as &$existingTag)
{
if (!self::arrayContains($tagsArray, 'name', $existingTag->name))
{
$this->tags()->detach($existingTag->id);
}
}
$this->load('tags');
}
private static function arrayContains($array, $key, $value)
{
foreach ($array as $item)
{
if($item[$key] == $value) return true;
}
return false;
}
}
And here is some relevant code for SomeItem API controller:
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Input;
class SomeItemController extends Controller
{
public function index(Request $request)
{
$query = \App\SomeItem::query();
return $query->get()->load('parentitem')->load('tags');
}
//show item for editing
public function show($id)
{
$someitem = \App\SomeItem::find($id);
$someitem->load('parentitem')->load('tags');
$someitem->attachKindToFiles();
return $someitem;
}
//store new entry to db
public function store()
{
$someitem = \App\SomeItem::create(Input::all());
isset(Input::all()['tags']) ? $someitem->updateTags(Input::all()['tags']) : '';
return $someitem;
}
//update/save
public function update($id)
{
$someitem = \App\SomeItem::find($id);
$someitem->update(Input::all());
$someitem->updateTags(Input::all()['tags']);
$someitem->load('tags');
return $someitem;
}
There is also a SomeItem presenter and composer but they don't do anything with tags.
With AnotherItems, I literally I duplicated everything from SomeItem and just changed names as needed.
So in the Tag model there is
public function anotheritems()
{
return $this->belongsToMany(AnotherItems::class);
}
In AnotherItems model there is this, for instance
public function tags()
{
return $this->belongsToMany(Tag::class);
}
In the AnotherItems API controller there is this, for instance (which is for route api/anotheritems/1):
public function index(Request $request)
{
$query = \App\AnotherItems::query();
if ($request->has('id')) {
$query->where('id', $request['id']);
}
return $query->get()->load('parentitem')->load('tags');
}
So, this is a total mystery. I have been trying to figure this out for 2 days now. And I continue asking myself
How on earth can Laravel be using syntax someitem_tag for one item but tag_anotheritems for another item???
I upgraded from laravel 5.2 to 5.3 and it is after the upgrade that I added this AnotherItems. But I can't figure out how that could possibly alter things in terms of these database queries.
I have tried a ton of artisan commands for clearing everything imaginable, but somewhere in the framework it wants to handle SomeItem and AnotherItems differently when building these join queries to extract/save tags.
Thoughts?
thanks,
Brian
Decided to step through code in debugger. Seems things are breaking down in Str.php in various snake related function, and I also noticed a snakeCache call, whatever the heck that is. Not sure why such a strange methodology to determine table names... Also in these functions there is some pluralizing related checks, so maybe this is related to me using an s at the end of my item name. Pretty messed up stuff if an s at the end of a model name can cause two different logic branches...
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.
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