Laravel Eloquent - Loading query scopes eagerly - php

I have a data structure in which I need objects to be aware of their needed dependencies for loading.
What I can do
Currently, I can do this to load the first layer of relationships, this is obviously a very basic model:
class Ticket {
public function notes(){}
public function events(){}
public function tags(){}
public function scopeWithAll($query)
{
$query->with('notes', 'events', 'tags');
}
}
// Loads Ticket with all 3 relationships
$ticket = Ticket::withAll();
This works great! The problem being, I need to chain this functionality down to 3-5 levels of dependent relationships. Each of the 3 loaded models is going to have n relationships of its own.
I know I can do this through eager loading if I specify all of the relationship names, as follows:
public function scopeWithAll($query)
{
$query->with('notes.attachments', 'notes.colors', 'events', 'tags', 'tags.colors.', 'tags.users.email');
}
This works great too. But I need my code to be smarter than that.
What I need to do
Statically defining the scope of each object load is not desirable at this point in my project. I need to be able to load a Ticket, and the Ticket load all of its relationships, and each of those relationships load all of their relationships.
The only way I can think to do this is find some way to eagerly load a query scope for each relationship on the class. Something like
public function scopeWithAll($query)
{
$query->with('notes.withAll()', 'events.withAll()', 'tags.withAll()');
}
Is there currently a way to do this within Eloquent?

Maybe you can try something like this:
User::withRelatives()->find(1);
Okay, that's an idea and how to implement that? For example, if you have some related methods for your User model such as 'posts', 'roles' etc then keep all the related methods (methods that make relationship) in a separate trait, for example:
trait UserRelatives {
public function posts()
{
// ...
}
public function roles()
{
// ...
}
}
Now, in the User model you may create a scopeMethod like withAll and inside there you may try something like this:
public function scopeWithAll($query)
{
// Get all the related methods defined in the trait
$relatives = get_class_methods(UserRelatives::class);
return $query->with($relatives);
}
So, if you do something like this:
$user = User::withAll()->find(1);
You'll be able to load all related models. Btw, get_class_methods(UserRelatives::class) will give you an array of all methods defined in that trait which may look something like this:
['posts', 'roles']
So, User::withAll() will load all the related models and then run the query.So, as a result the scope will do something like this:
$query->with(['posts', 'roles']);
Well, this is an abstract idea but hope you got it. Share your idea if you found something better.
Update:
According to your Model and related methods, this may look something like this:
class Ticket {
use TicketRelativesTrait;
public function scopeWithAll($query)
{
$relatives = get_class_methods(TicketRelativesTrait::class);
return $query->with($relatives);
}
}
Trait:
trait TicketRelativesTrait {
public function notes(){}
public function events(){}
public function tags(){}
}
// Loads Ticket with all relationships
$ticket = Ticket::withAll()->find(1);
This is more dynamic, no need to mention the related methods and whenever you add a new relationship method in the trait, that will also be loaded.

Related

Call a Model method using Laravel Relationship

I'm currently trying to use Laravel Relationships to access my achievements Model using User model, I use this relationship code:
public function achievements()
{
return $this->hasMany('App\Models\User\Achievement');
}
I can easily make some eloquent queries, however I can't access any method that I created there, I can't access this method:
class Achievement extends Model
{
public function achievementsAvailableToClaim(): int
{
// Not an eloquent query
}
}
Using the following code:
Auth::user()->achievements()->achievementsAvailableToClaim();
I believe that I am using this Laravel function in the wrong way, because of that I tried something else without using relationship:
public function achievements()
{
return new \App\Models\User\Achievement;
}
But that would have a performance problem, because would I be creating a new class instance every time I use the achievements function inside user model?
What would be the right way of what I'm trying to do?
it's not working because your eloquent relationship is a hasMany so it return a collection. you can not call the related model function from a collection.
you can var dump it on tinker to understand more what i mean.
You can use laravel scopes.Like local scopes allow you to define common sets of constraints that you may easily re-use throughout your application.
In your case you use this like, Define scope in model:
public function scopeAchievementsAvailableToClaim()
{
return $query->where('achivement_avilable', true);
}
And you can use this like :
Auth::user()->achievements()->achievementsAvailableToClaim();

What add to controller and what to models

I have a little problem. What data keep in controllers and what in models? I know in models keep whole logic of applications etc, but what's query and helpers functions? for example
Controller:
public function add(Request $request)
{
$item = new Item()
$item->name = $request->name;
$item->save();
$this->makeDirectory();
}
private function makeDirectory()
{
//make a directory with photo this product
}
Where should I keep "makeDirecory" method in controller or models?
This is another situation when i would delete product and reference from another table.
public function delete(Items $id)
{
$id->delete();
$this->deleteProperties($id->properties); // $id->properties is a method from Items model with references to table Properties
}
private function deleteProperties(Properties $id)
{
$id->delete();
}
Should I keep "deleteProperties" method in controller, Items model or Properties model? and invoke this method from this model?
You should keep methods like makeDirectory() in a service class and call it with:
$this->fileService->makeDirectory($directory);
You should keep data related logic in model classes or repository classes and use it in controller with:
$this->model->getSomeData();
You may also want to google "Fat models, skinny controllers".
Regarding helper functions, you should use these only when you really need one. For example, isAdmin() is a very handy global helper, but you should never create helpers like getAllUsers() or Helpers::getAllUsers()
I use controllers only to validate the incoming data and passing data to views.
I add another layer of classes that I call Departments. So, I have a department for profiles, artiles, info pages etc. Each department has its own namespace and a set of classes connected with the functionality.
Always think about SoC - separation of concerns. If you put a lot of logic into a controller, it will eventually get huge, hard to maintain and extend.
Example:
Controller:
public function addItem (Request $request, Item $item, ItemStorage
$itemStorage) {
if ($item->verifyInput($request->all())) {
$itemStorage->createItem ($item, $request->all());
}
else {
// ... handle input error
}
// ... view
}
App\Departments\Items:
class ItemStorage {
public function createItem ($newItem, $attributes) {
$newItem->create($attributes);
// ... prepare data for creating a directory
$this->makeDirectory($directoryName);
}
private function makeDirectory ($directoryName) {
//... create directory
}
}
You can/should separate the tasks even further. ItemStorage might not need to handle actual directory creation. You can call another department/service class name e.g. DiskManagement. This department would contain Classes like FileSystem. So, inside the makeDirectory() method, you would call a method from a class specialized in file system operations.

How to add where condition to eloquent model on relation?

I want to add where condition to my Model when with('category') is called. My relations are like this:
class Post extends Model
{
public function category()
{
return $this->belongsTo(Category::class);
}
}
Now I use this code to display post categories:
Post::where('slug', $slug)->with('category')->get();
I want to add where condition to Post Model when with('category') is called. I should display only posts.status== published if with('category') is called.
I think return $this->belongsTo(Category::class); is where i should add my where condition, but this doesn't work:
return $this->query()->where('posts.status', 'published')->getModel()->belongsTo(User::class)
How can I add where condition to all post queries if with('category') is called?
I know Laravel query scopes, but i think there is a simpler way we can use. (perhaps on $this->belongsTo(Category::class))
Relationships are implemented using additional queries. They are not part of the base query, and do not have access to modify the base query, so you cannot do this inside the relationship method.
The best way to do this is with a query scope:
Class:
class Post extends Model
{
public function scopeWithCategory($query)
{
return $query->with('category')->where('status', 'published');
}
}
Query:
$posts = Post::where('slug', $slug)->withCategory()->get();
Edit
Based on your comment, I think you've probably asked the wrong question. You may want to post another question explaining what you have setup, and what you need to do, and see if anyone has any suggestions from there.
However, to answer this specific question, I believe you should be able to do this using a global query scope. This is different than a local scope described in my original answer above.
Global query scopes are applied when get() is called on the Eloquent query builder. They have access to the query builder object, and can see the items that have been requested to be eager loaded. Due to this, you should be able to create a global query scope that checks if category is to be eager loaded, and if so, add in the status constraint.
class Post extends Model
{
/**
* The "booting" method of the model.
*
* #return void
*/
protected static function boot()
{
// make sure to call the parent method
parent::boot();
static::addGlobalScope('checkCategory', function(\Illuminate\Database\Eloquent\Builder $builder) {
// get the relationships that are to be eager loaded
$eagers = $builder->getEagerLoads();
// check if the "category" relationship is to be eager loaded.
// if so, add in the "status" constraint.
if (array_key_exists('category', $eagers)) {
$builder->where('status', 'published');
}
});
}
}
The code above shows adding in a global scope using an anonymous function. This was done for ease and clarity. I would suggest creating the actual scope class, as described in the documentation linked above.
This should work:
Post::where(['slug' => $slug, 'status' => 'published'])->with('category')->get();
you have to use withPivot() method .
class Post extends Model
{
public function category()
{
return $this->belongsTo(Category::class)->withPivot('status');
}
}
please refer to my question here

laravel many to many relationship in this case

I have already checked this official example http://laravel.com/docs/eloquent#many-to-many-polymorphic-relations
but I still confused because I may have a different case.
I have a DetailsAttribute model which deals with details_attribute table.
I have a Action model witch deals with action table.
The relationship between them is many to many.
So I created a new table details_attribute_action with model DetailsAttributeAction
My DetailsAttribute model should have:
public function actions(){}
My Actions model should have:
public function detailsAttributes(){}
and my DetailsAttributeAction model should have functions but I don't know what they are.
My question is what is the code inside the previous functions please? and should really the DetailsAttributeAction have functions of not?
What you're looking for is a Many-to-Many relation, not one that is polymorphic.
http://laravel.com/docs/eloquent#many-to-many
Your code should look something like this:
class DetailsAttribute extends Eloquent {
// ...
public function actions()
{
// Replace action_id and details_attribute_id with the proper
// column names in the details_attribute_action table
return $this->belongsToMany('Action', 'details_attribute_action', 'details_attribute_id', 'action_id');
}
}
class Action extends Eloquent {
// ...
public function detailsAttributes()
{
return $this->belongsToMany('DetailsAttribute', 'details_attribute_action', 'action_id', 'details_attribute_id');
}
}
You won't have to worry about how to create the DetailsAttributeAction model in Laravel. It's simply a table to map the Many-to-Many relationships you've created.

Multiple tables in one model - Laravel

My Index page uses 3 tables in the database:
index_slider
index_feature
footer_boxes
I use one controller (IndexController.php) and call the three models like so:
public function index() {
return View::make('index')
->with('index_slider', IndexSlider::all())
->with('index_feature', IndexFeature::all())
->with('footer_boxes', FooterBoxes::all());
}
The three models above need ::all() data, so they are all setup like this:
class IndexSlider extends Eloquent {
public $table ='index_slider';
}
note: class name changes for each model
Seeing as my index page requires these 3 tables and the fact I am repeating the syntax in each model then should I be using polymorphic relations or setting this up differently? ORM from what I have read should have 1 model for each table, but I can't help but feel this would be silly in my situation and many others. DRY (don't repeat yourself) looses meaning in a sense.
What would be the best approach to take here or am I on the right track?
Firstly I should say each model is written for a specific table, you can't squeeze three tables into one model unless they are related. See Here
There are two ways I would go about making your code more DRY.
Instead of passing your data in a chain of withs I would pass it as the second parameter in your make:
public function index() {
$data = array(
'index_slider' => IndexSlider::all(),
'index_feature' => IndexFeature::all(),
'footer_boxes' => FooterBoxes::all(),
);
return View::make('index', $data);
}
Passing data as the second parameter. See here
The other way I would go about it, and this is a better solution if your application is going to grow large, is to create a service (another model class but not hooked up to eloquent) that when you call will return the necessary data. I would definitely do it this way if you are returning the above data in multiple views.
An example of using a service would look something like this:
<?php
// app/models/services/indexService.php
namespace Services;
use IndexSlider;
use IndexFeature;
use FooterBoxes;
class IndexService
{
public function indexData()
{
$data = array(
'index_slider' => IndexSlider::all(),
'index_feature' => IndexFeature::all(),
'footer_boxes' => FooterBoxes::all(),
);
return $data;
}
}
and your controller:
<?php
// app/controllers/IndexController.php
use Services/IndexService;
class IndexController extends BaseController
{
public function index() {
return View::make('index', with(new IndexService())->indexData());
}
}
This service can be expanded with a lot less specific methods and you should definitely change the naming (from IndexService and indexData to more specific class/method names).
If you want more information on using Services I wrote a cool article about it here
Hope this helps!

Categories