42

Whenever I add additional logic to Eloquent models, I end up having to make it a static method (i.e. less than ideal) in order to call it from the model's facade. I've tried searching a lot on how to do this the proper way and pretty much all results talk about creating methods that return portions of a Query Builder interface. I'm trying to figure out how to add methods that can return anything and be called using the model's facade.

For example, lets say I have a model called Car and want to get them all:

$cars = Car::all();

Great, except for now, let's say I want to sort the result into a multidimensional array by make so my result may look like this:

$cars = array(
  'Ford' => array(
     'F-150' => '...',
     'Escape' => '...',
  ),
  'Honda' => array(
     'Accord' => '...',
     'Civic' => '...',
  ),
);

Taking that theoretical example, I am tempted to create a method that can be called like:

$cars = Car::getAllSortedByMake();

For a moment, lets forget the terrible method name and the fact that it is tightly coupled to the data structure. If I make a method like this in the model:

public function getAllSortedByMake()
{
   // Process and return resulting array
   return array('...');
}

And finally call it in my controller, I will get this Exception thrown:

Non-static method Car::getAllSortedByMake() should not be called statically, assuming $this from incompatible context

TL;DR: How can I add custom functionality that makes sense to be in the model without making it a static method and call it using the model's facade?


Edit:

This is a theoretical example. Perhaps a rephrase of the question would make more sense. Why are certain non-static methods such as all() or which() available on the facade of an Eloquent model, but not additional methods added into the model? This means that the __call magic method is being used, but how can I make it recognize my own functions in the model?

Probably a better example over the "sorting" is if I needed to run an calculation or algorithm on a piece of data:

$validSPG = Chemical::isValidSpecificGravity(-1.43);

To me, it makes sense for something like that to be in the model as it is domain specific.

Jeremy Harris
  • 23,007
  • 13
  • 73
  • 124
  • 1
    Start by having two datatables: `manufacturers` and `models`, so `manufacturers` contains "Ford", "Honda", etc and `models` with a `manufacturer_id` linking the `model` to the `manufacturer` and containing "F-150", "Escape", "Accord", "Civic", etc – Mark Baker May 14 '14 at 15:09
  • 1
    @MarkBaker This was a theoretical example. My question is at more of a fundamental level such as why is `all()` accessible via the facade? It is not a static method, which means that the `__call` magic method is being used. Due to that, why is `arbitraryMethodICreate()` not accessible? – Jeremy Harris May 14 '14 at 15:30
  • see `Illuminate\Database\Eloquent\Model`, `all` is a static method – Jeff Lambert May 14 '14 at 15:34
  • 1
    cillosis: the reason for that is simple: `all()` is in fact static method on the `Model`, and `__call` is not called in this situation. There are more static methods on the `Model` class, and others that you can use the way `Model::method()` are processed by `__callStatic` then `__call` magic methods and passed to the `Eloquent Builder` class. – Jarek Tkaczyk May 14 '14 at 15:34
  • This might helo you https://codezen.io/most-useful-laravel-collection-methods/ – Sapnesh Naik May 08 '20 at 06:07

3 Answers3

63

My question is at more of a fundamental level such as why is all() accessible via the facade?

If you look at the Laravel Core - all() is actually a static function

public static function all($columns = array('*'))

You have two options:

public static function getAllSortedByMake()
{
    return Car::where('....')->get();
}

or

public function scopeGetAllSortedByMake($query)
{
    return $query->where('...')->get();
}

Both will allow you to do

Car::getAllSortedByMake();
Laurence
  • 55,427
  • 18
  • 158
  • 197
  • 1
    Ahh, I had heard about scopes in Eloquent, but haven't used them yet. Thanks! I see that `all()` is static now (thats what I get for not verifying). I had always heard that these methods were not and just used design patterns to *appear* that way for usability while maintaining testability. – Jeremy Harris May 14 '14 at 15:40
  • Yeah - most functions are not static - just some of the model ones for some reason - which I'm sure Taylor has good reason for - but its above my knowledge. – Laurence May 14 '14 at 15:43
  • 8
    Note that ["scopes should always return a query builder instance"](https://laravel.com/docs/5.1/eloquent#query-scopes). So if you use `->get()` you should use a static method and not the scope variant. Also note that scopes can be chained: `Car::GetAllSortedByMake()->GetAllSortedByMake()->otherScopeYouHaveDefined()->where(...)->get()`. – Sawny Jan 03 '16 at 12:36
  • don't use scopes because query is not the same because scopes are isolated – fico7489 Aug 07 '18 at 06:00
7

Actually you can extend Eloquent Builder and put custom methods there.

Steps to extend builder :

1.Create custom builder

<?php

namespace App;

class CustomBuilder extends \Illuminate\Database\Eloquent\Builder
{
    public function test()
    {
        $this->where(['id' => 1]);

        return $this;
    }
}

2.Add this method to your base model :

public function newEloquentBuilder($query)
{
    return new CustomBuilder($query);
}

3.Run query with methods inside your custom builder :

User::where('first_name', 'like', 'a')
    ->test()
    ->get();

for above code generated mysql query will be :

select * from `users` where `first_name` like ? and (`id` = ?) and `users`.`deleted_at` is null

PS:

First Laurence example is code more suitable for you repository not for model, but also you can't pipe more methods with this approach :

public static function getAllSortedByMake()
{
    return Car::where('....')->get();
}

Second Laurence example is event worst.

public function scopeGetAllSortedByMake($query)
{
    return $query->where('...')->get();
}

Many people suggest using scopes for extend laravel builder but that is actually bad solution because scopes are isolated by eloquent builder and you won't get the same query with same commands inside vs outside scope. I proposed PR for change whether scopes should be isolated but Taylor ignored me.

More explanation : For example if you have scopes like this one :

public function scopeWhereTest($builder, $column, $operator = null, $value = null, $boolean = 'and')
{
    $builder->where($column, $operator, $value, $boolean);
}

and two eloquent queries :

User::where(function($query){
    $query->where('first_name', 'like', 'a');
    $query->where('first_name', 'like', 'b');
})->get();

vs

User::where(function($query){
    $query->where('first_name', 'like', 'a');
    $query->whereTest('first_name', 'like', 'b');
})->get();

Generated queries would be :

select * from `users` where (`first_name` like ? and `first_name` like ?) and `users`.`deleted_at` is null

vs

select * from `users` where (`first_name` like ? and (`id` = ?)) and `users`.`deleted_at` is null

on first sight queries look the same but there are not. For this simple query maybe it does not matter but for complicated queries it does, so please don't use scopes for extending builder :)

fico7489
  • 6,416
  • 5
  • 44
  • 77
1

for better dynamic code, rather than using Model class name "Car",

just use "static" or "self"

public static function getAllSortedByMake()
{
    //to return "Illuminate\Database\Query\Builder" class object you can add another where as you want
    return static::where('...');

    //or return already as collection object
    return static::where('...')->get();
}
jalmatari
  • 171
  • 3
  • 7