6

I have been trying to create my own chainable method in laravel' eloquent but I'm missing something and not sure what. This may sound a little bit nuts but have a look at my function below to get a better idea of what I'm trying to say.

class Post extends Eloquent{
    public static function custom_wh($data){
        return static::where_in('categories_id', $data, 'AND');
    }
}

//this works fine
$posts = Post::custom_wh(array(1, 2, 3))->get();

//but this says custom_wh is not defined in the query class
$posts = Post::where_in('tags', array(2, 3, 4), 'AND')->custom_wh(array(1, 2, 3))->get();

if I understand correctly then my method is not eligible to chain after another method? So I guess my question is how can I create a chainable method in my model?

P.S I have looked into the laravel's query builder class where I have seen that the chainable methods returns the instance of that object but I couldn't find a way to return the object other than the way I've done in the code above. Any kind of suggestion or advice is highly appreciated. Thanks in advance.

Foysal Ahamed
  • 282
  • 1
  • 7
  • 17

5 Answers5

13

You can do that in Laravel with the "query scopes". You can find the doc here.

You just have to write a function with the prefix scope and you will be able to chain this method like the other query builder ones :

class Post extends Eloquent {

    public function scopeWhereCategories($query, $categories)
    {
        return $query->whereIn('categories_id', $categories, 'AND');
    }

}

$posts = Post::whereCategories([1, 2, 3])->get();
$posts = Post::orderBy('date')->whereCategories([1, 2, 3])->take(5)->get();
Alexandre Butynski
  • 6,625
  • 2
  • 31
  • 44
  • 1
    Love this feature and been using it already :D I know this didn't exist back when I posted my question but I'm selecting this as the correct answer for other googlers like me. thanks. – Foysal Ahamed Jan 14 '14 at 13:50
  • @Alexandre Butynski for what 3rd param is used for ? `('categories_id', $categories, 'AND');` – Daniyal Nasir Oct 02 '19 at 07:02
  • @DaniyalNasir, I don't know ! It's just a copy past from initial question. Anyway, it seems pretty useless because it is the default value for this param... – Alexandre Butynski Oct 03 '19 at 09:03
  • @AlexandreButynski Oh alright but it does take where operator as 3rd parameter right ? *I'm new to Laravel* – Daniyal Nasir Oct 04 '19 at 05:39
12

OK... This may warp your brain a bit but stick with me. The actual method defined is _where(). So how does both Post::where and $post->where end up calling the _where() method? The answer is 'magic'. :D

PHP has these things called 'magic methods' which enable some very dynamic behaviour. In this case Laravel is using __callStatic() and __call() to handle calls to undefined methods. This enabled the same method to be called statically and non-statically.

public static function __callStatic($method, $parameters)
{
    // Create a new instance of the called class, in this case it is Post
    $model = get_called_class();

    // Call the requested method on the newly created object
    return call_user_func_array(array(new $model, $method), $parameters);
}

So basically Post::where() is just shorthand for $post = new Post; $post->where()

From here the request goes to __call() in which there is an array of underscored method names. If the requested method is in the list of underscored names, then $this->_method() is called and returned.

But this is still not the whole story. Drew is correct in that 'where_in' returns a Query object. Most of the ORM chaining methods you are familiar with are actually part of the ORM Query object. Here is the whole process.

  • Post::where( ... )
  • Post::__callStatic( 'where', ... )
  • $post->__call( 'where', ... )
  • $query->_call( 'where', ... )
  • $query->_where( ... )

The class you want to extend is the Query that is used by Model. There is no way add another method name to the list of underscored methods defined in __call(). You will have to copy __call() in it's entirety into your Query definition in order to enable your new method to have that behaviour.

I have accomplished this in my projects be pointing Eloquent ( the alias for the Laravel model ) to my extended version, enabling all my models to use the extended Query methods.

// application/libraries/mylib/query.php
namespace MyLib;

class Model extends \Laravel\Model {

    // Tell \MyLib\Model to use \MyLib\Query instead of Laravels Query
    protected function query()
    { 
        return new \MyLib\Query($this);
    } 

}

// application/libraries/mylib/query.php
namespace MyLib;

class Query extends \Laravel\Database\Eloquent\Query {

    function _my_method() { ... }

    function __call() { ... }
}

// application/config/application.php
'aliases' => array(
    ...
    'Eloquent' => 'MyLib\\Model',
    ...
)

http://php.net/manual/en/language.oop5.magic.php https://github.com/laravel/laravel/blob/master/laravel/database/eloquent/model.php#L734 https://github.com/laravel/laravel/blob/master/laravel/database/eloquent/query.php#L274

Collin James
  • 9,062
  • 2
  • 28
  • 36
  • that explains a lot. to be honest I haven't studied much about OO in php and I started using frameworks cause I thought I'd learn OO as I go along so yeah this is quite much to have a grip on but Lemme a shot at it using your technic. I'll keep you posted if I have any further questions along the way. Thanks a lot for helping. – Foysal Ahamed Mar 03 '13 at 15:41
0

not sure if i am accurate but this is what i came up with quick...

Post::where_in() is not returning an Eloquent model it is returning object of type Query.

For how simply you have written your custom function I would just avoid the function

$posts = Post::where_in('tags', array(2, 3, 4))->where_in('categories_ids', array(1,2,3))->get();

from the top of my head

you could try something like

class Post extends Eloquent {
    public static function custom_wh($data=array()) {
        return static::where_in('categories_id', $data);
        // return type of query not eloquent
    }
}

$posts = Post::custom_wh(array(1,2,3))->where_in('tags', array(2, 3, 4))->get();

unless you wanted to alter the query class

lagbox
  • 48,571
  • 8
  • 72
  • 83
  • well, that's what I did and it works but the problem is the custom_wh function is not chainable so suppose I have a huge query that needs to do several join, where clauses so I want to wrap them in seperate functions and chain them when needed, then the problem appears. One more reason that I'm trying to put them in a function is cause the ORM shouldn't be my model right? and I'm also trying to keep my code "DRY". – Foysal Ahamed Feb 26 '13 at 08:05
0

Have you tried using 'self' instead of 'static'? As far as I know, if you use self and the attribute/method and you subclass and the method that you are referring to is does not override the main class attribute/method, it will return, in this case the, the method 'where_in' of the Query class. And then you will be able to chain you custom methods.

0

Actually you can extend eloquent builder easily and just run :

$posts = Post::custom_wh(array(1, 2, 3))->get();

You have instructions at my answer here : Laravel Custom Model Methods

also, don't use scopes for that except you now exactly what are you doing, this is also clarified at my answer.

fico7489
  • 7,931
  • 7
  • 55
  • 89