How where method works in Laravel Models? - laravel

We frequently use where method with our models but it's not defined in Base Model class so how laravel is doing this magic?
e.g MyModel::where('id, 2)->get();
The above where will definitely fetch the record having id equals 2 but where & how is this happening! I traced back to the Base Model in Laravel but didn't found where method!

Some of this magic resides behind a Facade Pattern https://www.tutorialspoint.com/design_pattern/facade_pattern.htm
But in Laravel case, they use Static methods in their Facades (\Illuminate\Support\Facades namespace), and create instance automatically to call those methods from instance, so you don't have to instantiate the Class yourself to start using the methods. For example when you use DB::, Cache::, Str:: to call static method.
However for Eloquent, Laravel uses this approach more internally without exposing the Model to \Illuminate\Support\Facades namespace, by configuring Eloquent Model to instantiate automatically the Model and creates\Illuminate\Database\Eloquent\Builder Eloquent Builder and forward those static methods call to the Eloquent Builder instance or to Query Builder one.
A model extends \Illuminate\Database\Eloquent\Model which has a special way to call any method statically at runtime.
/**
* Handle dynamic static method calls into the method.
*
* #param string $method
* #param array $parameters
* #return mixed
*/
public static function __callStatic($method, $parameters)
{
return (new static)->$method(...$parameters);
}
So at a certain point, the Builder classes an Eloquent use a Trait
use Illuminate\Support\Traits\ForwardsCalls;
to forward call to each others so that a Model can forward its static method to method from the created instance of Eloquent builder. So if model does not have where method, it will forward it to the where method of the newly created nstance of Eloquent Builder.
From a look of Facade Pattern, here is how Laravel calls a method from an instance but in a static way :
// \Illuminate\Support\Facades\Facade.php
/**
* Handle dynamic, static calls to the object.
*
* #param string $method
* #param array $args
* #return mixed
*
* #throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
So calling User::where() creates an instance of \Illuminate\Database\Eloquent\Builder and forward method call to that instance, here is the method :
// Illuminate\Database\Eloquent\Builder.php
/**
* Add a basic where clause to the query.
*
* #param string|array|\Closure $column
* #param mixed $operator
* #param mixed $value
* #param string $boolean
* #return $this
*/
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if ($column instanceof Closure) {
$column($query = $this->model->newModelQuery());
$this->query->addNestedWhereQuery($query->getQuery(), $boolean);
} else {
$this->query->where(...func_get_args());
}
return $this;
}

You can find the method here . framework/src/Illuminate/Database/Eloquent/Builder.php
public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if ($column instanceof Closure) {
$column($query = $this->model->newModelQuery());
$this->query->addNestedWhereQuery($query->getQuery(), $boolean);
} else {
$this->query->where(...func_get_args());
}
return $this;
}
Some more info about it

Related

Query to all contents of tabel in related model in laravel

Assume we have a model name "Article" in Laravel and want to make query for retrieving latest articles, so one way is to define a method in the "Article" model like this:
public function newArticle()
{
return static::where('created_at', '>', Carbon::subMonths(1));
}
The question is, why we should use
static::
in the above code?
Is it possible to use
$this or self::
instead of
"static::" ?
Thanks in advance,
You could but there is not interest because:
where method does not exist statically on Illuminate\Database\Eloquent\Model class, so it calls __callStatic magic method which delegates the call to an instance
/**
* Handle dynamic static method calls into the method.
*
* #param string $method
* #param array $parameters
* #return mixed
*/
public static function __callStatic($method, $parameters)
{
return (new static)->$method(...$parameters);
}
It calls where method on instance but it does not exist either, so it calls __call magic method which delegates it an Illuminate\Database\Eloquent\Builder instance.
/**
* Handle dynamic method calls into the model.
*
* #param string $method
* #param array $parameters
* #return mixed
*/
public function __call($method, $parameters)
{
if (in_array($method, ['increment', 'decrement'])) {
return $this->$method(...$parameters);
}
return $this->forwardCallTo($this->newQuery(), $method, $parameters);
}
/**
* Get a new query builder for the model's table.
*
* #return \Illuminate\Database\Eloquent\Builder
*/
public function newQuery()
{
return $this->registerGlobalScopes($this->newQueryWithoutScopes());
}

Laravel Lighthouse - Receive Eloquent Builder instead of Query Builder in handleBuilder method?

Is there a way to receive an instance of \Illuminate\Database\Eloquent\Builder instead of \Illuminate\Database\Query\Builder in the handleBuilder method when creating a custom ArgBuilderDirective?
See this example:
<?php
namespace Nuwave\Lighthouse\Schema\Directives;
use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
class EqDirective extends BaseDirective implements ArgBuilderDirective, DefinedDirective
{
/**
* Name of the directive.
*
* #return string
*/
public function name(): string
{
return 'eq';
}
/**
* Apply a "WHERE = $value" clause.
*
* #param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
* #param mixed $value
* #return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder
*/
public function handleBuilder($builder, $value)
{
// $builder is an instance of \Illuminate\Database\Query\Builder here.
// Is it possible to receive an instance of Illuminate\Database\Eloquent\Builder instead?
return $builder->where(
$this->directiveArgValue('key', $this->nodeName()),
$value
);
}
}

How to overwrite the laravel DatabaseManager method

There is a method in vendor/laravel/framework/src/Illuminate/Database/DatabaseManager.php
/**
* Parse the connection into an array of the name and read / write type.
*
* #param string $name
* #return array
*/
protected function parseConnectionName($name)
{
$name = $name ?: $this->getDefaultConnection();
return Str::endsWith($name, ['::read', '::write'])
? explode('::', $name, 2) : [$name, null];
}
If I want to overwrite this method how would I do it?
Since this method is protected, you have to create a class that extends Illuminate\Database\DatabaseManagerĀ in your appĀ folder and override that function with your own
Then inject your custom class into the container by binding a singleton
$this->app->bind('Illuminate\Database\DatabaseManager', function () {
return new App\CustomClass;
});
Hope this helps

Laravel router namespace method

In Laravel documentation routing there is a namespace method.
Route::namespace
I tried to explore what does it really do but couldn't find it's definition in Laravel source codes. Where is it?
It's not related to code, just to group the routes. Like this:
The source is here: https://github.com/laravel/framework/blob/b73691ac7b309cd2c4fb29b32d3eed76fecca58b/src/Illuminate/Routing/RouteGroup.php#L40, it just adds the namespace at end of the current namespace.
You have a controller group like 'Products' for example,
App/
Http/
Controllers/
Products/
Stocks.php
Prices.php
Sizes.php
And you need to modify their namespaces like this to meet the PSR-4 requirements to enable autoloading of controllers:
namespace App\Http\Controllers\Products;
class Stocks {
function index(){
}
}
Then if you want to access the methods of these controllers, you might want to group them with Route::namespace():
Route::namespace("Products")->group(function(){
Route::get("stocks", "Stocks#index");
});
This will search for the Stocks class in the App\Http\Controllers\Products namespace instead of App\Http\Controllers namespace. and call the index method.
Note that you might run composer dumpautoload to let the framework rebuild the autoload.php with the PSR-4 namespaces to make these things effective.
Later Edit:
framework/src/Illuminate/Routing/Router.php defines the Route class, which redirects the Route::namespace method to RouterRegistrar class at this line:
/**
* Dynamically handle calls into the router instance.
*
* #param string $method
* #param array $parameters
* #return mixed
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
if ($method == 'middleware') {
return (new RouteRegistrar($this))->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters);
}
return (new RouteRegistrar($this))->attribute($method, $parameters[0]);
}
in the last line. And in that method,
/**
* The attributes that can be set through this class.
*
* #var array
*/
protected $allowedAttributes = [
'as', 'domain', 'middleware', 'name', 'namespace', 'prefix',
];
/**
* Set the value for a given attribute.
*
* #param string $key
* #param mixed $value
* #return $this
*
* #throws \InvalidArgumentException
*/
public function attribute($key, $value)
{
if (! in_array($key, $this->allowedAttributes)) {
throw new InvalidArgumentException("Attribute [{$key}] does not exist.");
}
$this->attributes[Arr::get($this->aliases, $key, $key)] = $value;
return $this;
}
namespace attribute is being set, to use in ->group() method.

Unable to override automatic model find method calls since upgrading to Laravel 5.1

I have a simple trait which I use to always include soft-deleted items for a few things:
trait OverrideTrashedTrait {
public static function find($id, $columns = ['*'])
{
return parent::withTrashed()->find($id, $columns);
}
}
However, since upgrading to Laravel 5.1, this no longer works. Soft-deleted items do not turn up in get() lists, and if I try to access a page where I've used route model bindings, I get the NotFoundHttpException.
Laravel's upgrade documentation states that:
If you are overriding the find method in your own models and calling parent::find() within your custom method, you should now change it to call the find method on the Eloquent query builder:
So I changed the trait accordingly:
trait OverrideTrashedTrait {
public static function find($id, $columns = ['*'])
{
return static::query()->withTrashed()->find($id, $columns);
}
}
But it appears that no matter what I write in there, it doesn't affect the results. I have also tried to put the overriding find() method directly in the model, but that doesn't appear to be working either. The only way anything changes is if I write invalid syntax. Even if I change the $id to a hardcoded id of an item that is not soft-deleted, I get the same result, and absolutely nothing happens if I e.g. try to dd('sdfg'), so I doubt the method is even called.
Edit: If I do trigger the method manually, it works just like intended.
How can I fix this?
Ok here it goes:
short version: Model binding does not use find.
longer version:
/**
* Register a model binder for a wildcard.
*
* #param string $key
* #param string $class
* #param \Closure|null $callback
* #return void
*
* #throws NotFoundHttpException
*/
public function model($key, $class, Closure $callback = null)
{
$this->bind($key, function ($value) use ($class, $callback) {
if (is_null($value)) {
return;
}
// For model binders, we will attempt to retrieve the models using the first
// method on the model instance. If we cannot retrieve the models we'll
// throw a not found exception otherwise we will return the instance.
$instance = new $class;
if ($model = $instance->where($instance->getRouteKeyName(), $value)->first()) {
return $model;
}
// If a callback was supplied to the method we will call that to determine
// what we should do when the model is not found. This just gives these
// developer a little greater flexibility to decide what will happen.
if ($callback instanceof Closure) {
return call_user_func($callback, $value);
}
throw new NotFoundHttpException;
});
}
Line 931 of Illuminate\Routing\Router says it does:
$instance->where($instance->getRouteKeyName(), $value)->first()
It uses the key from the model used in a where and loads the first result.
In Laravel 5.1 find() method can be found in Illuminate\Database\Eloquent\Builder
From your Model class you can override it like following:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
/**
* Overrides find() method in Illuminate\Database\Eloquent\Builder.
* Finds only active products.
*
* #param mixed $id
* #param array $columns
* #return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|null
*/
public static function find($id, $columns = ['*']) {
// create builder from the Model
$builder = (new self)->newQuery();
// method customization
if (is_array($id)) {
// findMany() also should be customized
return self::findMany($id, $columns);
}
$builder->getQuery()->where("isActive", '=', 1)->where($builder->getModel()->getQualifiedKeyName(), '=', $id);
return $builder->first($columns);
}
}

Resources