Checking Model Relationships From Nested Resources - laravel

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.

Related

Laravel Eloquent - setup both-ways relationships after loading?

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)

Laravel/Livewire: Use withTrashed() on model route binding on to show deleted records

In the list I display the latest topic, including those that is deleted.
function latest()
{
return Topic::withTrashed()->latest();
}
For displaying a single topic I have a Livewire component with that topic passed into it.
class ShowTopic extends Component
{
public $topic;
public function mount(Topic $topic)
{
$this->topic = $topic;
}
public function render()
{
return view('livewire.show-topic', [
'topic' => $this->topic,
]);
}
}
But when I go to a single topic that is deleted, it doesn't show. How can I use withTrashed() on model route bindings to show deleted records with my Livewire component?
You can overwrite the resolveRouteBinding() method on your Eloquent model, and conditionally remove the SoftDeletingScope global scope.
Here I'm using a policy for that model to check if I can delete the model - and if the user can delete it, they can also see it. You could implement any logic you want, or remove the global scope for all requests if that is more suitable for your application.
use Illuminate\Database\Eloquent\SoftDeletingScope;
class Topic extends Model {
// ...
/**
* Retrieve the model for a bound value.
*
* #param mixed $value
* #param string|null $field
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null)
{
// If no field was given, use the primary key
if ($field === null) {
$field = $this->getKey();
}
// Apply where clause
$query = $this->where($field, $value);
// Conditionally remove the softdelete scope to allow seeing soft-deleted records
if (Auth::check() && Auth::user()->can('delete', $this)) {
$query->withoutGlobalScope(SoftDeletingScope::class);
}
// Find the first record, or abort
return $query->firstOrFail();
}
}

Please how do I share data from show method to two different views

Please I need to know if there is a way I can share data from one controller method to multiple views,
This is the CategoryController show method
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function show($id)
{
$category = Category::find($id);
$users = $category->user;
return view('categories.show')->with('users', $users);
}
what I want is to share the same data with another page profile-display.blade.php without creating another controller.
#Adam this will help you.
route.php
Route::get('users/{id}', 'CategoryController#show')->name('profile');
Route::get('Category/{id}', 'CategoryController#show')->name('category');
CategoryController.php
public
function show($request, $id) {
$category = Category::find($id);
$users = $category->user;
if ($request::route()->getName() == 'category') {
return view('categories.show')->with('users', $users);
} else {
return view('profile.show')->with('users', $users);
}
}
Different routes can use the same controller, but you are going to have a harder time having a single controller present multiple views without a chaos of conditions or at least just make a new controller/method.
You can shave of some of your of your code to make a slimmer approach, though, by using model binding with routes:
routes.php
Route::get('categories/{category}', 'CategoryController#show');
and then in CategoryController.php:
public function show(Category $category)
{
return view('categories.show')->with('users', $category->user);
}
This will enable model binding to auto-instantiate the model if it exists and return a 404 if not.
You can reiterate the process for a second view, but just reuse the same controller (which I personally wouldn't prefer at all). Just add the route and the new method in the same controller:
routes.php
Route::get('categories/{category}', 'CategoryController#show');
Route::get('profile-display', 'CategoryController#profileDisplay');
and then in CategoryController.php:
public function show(Category $category)
{
return view('categories.show')->with('users', $category->user);
}
public function profileDisplay()
{
return view('profile-display');
}
I haven't added any model bindings to the profileDisplaymethod as I couldn't really understand how you would like to attache the category to a profile display page (as the name suggests), but that could give you some rough ideas.
you can use \Illuminate\Support\Facades\View::share('foo', 'bar');

How do i switch a custom implementation of a interface on a condition in http controller in Laravel

I have a method in my controller which basically is just storing a form to the database.
My form is very big and it has more than 30-40 fields.
So i need to store this information in different 3 tables on a condition.
For example :
foreach($request->all() as $answer):
if($answer->employeeType === 1){
//store data to type_one_table
} else if($answer->employeeType === 2){
//store data to type_two_table
} else if($answer->employeeType === 3){
//store data to type_two_table
} else {
//store data to some_other_table
}
endforeach;
So i was thinking to use some custom StoreInterface which will have some store method.
and extract above logic to its own implementation.
And then in my Controllers constructor receive that interface instance.
But how i can call which implementation do i need for particular condition.
Can i do this a controller ?
Or Should i use different strategy in this case.
please guide me.
Thanks.
You can have a general interface like this:
interface EmployeeStoreContract
{
public function saveAnswer();
}
Then implement this for all of the different ways you can save the answer, e.g.:
class FirstTypeEmployee implements EmployeeStoreContract
{
public function saveAnswer()
{
// do something
}
}
And in your controller to figure out how to save the request data:
public function store()
{
$employeeTypesMap = [
1 => 'FirstTypeEmployee',
2 => 'SecondTypeEmployee',
3 => 'ThirdTypeEmployee',
];
foreach ($request->all() as $answer) {
$employeeType = $answer->employeeType;
if (!array_key_exists($employeeType, $employeeTypesMap)) {
throw new \InvalidArgumentException('Answer type is not available.');
}
$employeeStrategyClass = "App\\Service\\Employee\\{$employeeTypesMap[$employeeType]}";
$employeeStrategyObject = new $employeeStrategyClass;
$employeeStrategyObject->saveAnswer($answer);
}
}
Please note the namespace where you have these classes App\Service\Employee, could be anything you want. Also feel free to make an abstract class or a trait to reuse some of the functionality of each employee class.
There can be many approaches.
I will suggest 2 ways that i would personally use:
1) Create dedicated eloquent models one for each case.
For example:
if($answer->employeeType === 1){
EmployeeOne::create($answer);
}
And your eloquent model can look like:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use DB;
class EmployeeOne extends Model
{
protected $table = 'type_one_table';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
// your db field names
];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
];
public $timestamps = false;
}
Make sure that the array keys of the array you pass to the create function must match your db fields. I just gave you an example of the first if so you can take it from here.
2)
Another approach would be to create a model that accepts as a parameter the table name. So based on your if you are going to pass the table name you want to insert and the data to be inserted.
if($answer->employeeType === 1){
$this->myModel->insertData($answer,'type_one_table');
}
And in your model you are going to have something like:
public function insertData($answer,$table){
$query = DB::table($table)->insertGetId($answer); // returns the id of the new record
return $query;
}
So in every if-else statement you just change the table name passing to your model function and that's it.

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