Pregunta

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.

¿Fue útil?

Solución

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();

Otros consejos

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 :)

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();
}

Laravel model custom methods -> best way is using traits

  • Step #1: Create a trait
  • Step #2: Add the trait to model
  • Step #3: Use the method
User::first()->confirmEmailNow()

app/Model/User.php

use App\Traits\EmailConfirmation;

class User extends Authenticatable
{
    use EmailConfirmation;
    //...
}

app/Traits/EmailConfirmation.php

<?php
namespace App\Traits;

trait EmailConfirmation
{
    /**
     * Set email_verified_at to now and save.
     *
     */
    public function confirmEmailNow()
    {
        $this->email_verified_at = now();
        $this->save();
        return $this;
    }
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top