目标
我在laravel项目中自定义的Model中添加了一个方法,我想像Model的其他方法如all
方法一样支持在实例上调用和静态方式调用:
$flights = (new App\Flight)->all(); //在实例上调用
$flights = App\Flight::all(); //静态方式调用
我们的Model名称为AvatarPic
,我编写的测试代码为:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AvatarPic extends Model
{
protected $table = 'avatar_pic';
/**
* @var string
*/
private $path;
public function test()
{
echo 'test';
}
}
问题
当我在实例上调用时,代码可以正常执行:
(new App\Models\AvatarPic)->test(); // 将会打印出test
当我使用静态方式调用时,代码将会抛出异常:
App\Models\AvatarPic::test(); //将会抛出ErrorException (E_DEPRECATED)
异常描述为:
Non-static method App\Models\AvatarPic::test() should not be called statically
我的环境信息:
- 操作系统:windows10
- PHP版本:PHP 7.2.1
- laravel版本:Laravel Framework 5.5.48
检测
在看过laravel中Illuminate\Database\Eloquent\Model
代码后,我对laravel模型像all
方法的调用方式有了初步了解。
根据Illuminate\Database\Eloquent\Model
中__callStatic
的使用,我编写了测试代码来探测问题发生在哪里。
我编写一个测试文件test.php
:
<?php
class TestService
{
public static function __callStatic($name, $arguments)
{
echo 't';
}
public function test()
{
echo 'test';
}
}
TestService::test();
当我运行这个测试文件时,程序成功打印出了’test’:
这个结果给我了实现目标的希望,几乎同样的代码,在laravel外可以正常执行。为了验证是否由于继承Illuminate\Database\Eloquent\Model
造成的异常,我在laravel项目中编写了一样的代码:
<?php
namespace App\Services;
class TestService
{
public static function __callStatic($name, $arguments)
{
echo '__callStatic';
}
public function test()
{
echo 'test';
}
}
当我运行代码时,程序还是抛出了异常:
为了排除环境因素,我在centos7、php 7.3.1、laravel 5上执行也是如此。
猜想
- 应该是因为laravel内部做了限制。
- 在laravel整个项目中搜索"Non-static method",并没有搜索结果,也就是说这个异常是php产生的,不是laravel定义的。
- 可能laravel内部对php的配置做了修改。
在打印的异常信息中有"ErrorException (E_DEPRECATED)",那么上面的写法在laravel中的异常级别是E_DEPRECATED,使用原生php执行没有抛异常,可能是默认的php异常级别与laravel中配置的异常级别不一致。
验证
- PHP中的异常级别有:PHP: 预定义常量 - Manual
- PHP中的设置错误异常:HP: error_reporting - Manual
- PHP中查看错误级别:
ini_get('error_reporting')
查看laravel中的错误级别:
查看PHP中错误级别:
laravel中设置为报告所有异常错误,PHP设置的报告异常有
我们修改我们在测试代码中添加error_reporting(-1)``,重新执行程序:
<?php
error_reporting(-1);
class TestService
{
public static function __callStatic($name, $arguments)
{
echo 't';
}
public function test()
{
echo 'test';
}
}
TestService::test();
(new TestService)->test();
这时,使用php执行也会抛出异常:
结尾
虽然知道了laravel和原生php执行结果不同的原因,但使用两种方式调用的目标依然没有解决。
要想静态访问可以设置自定义方法的访问限定为protected
;参考:PHP 调用静态方法不存在时首先检查方法是否是静态的而不是走__callStatic? | Laravel China 社区
public
可以在实例上调用;protected
可以静态调用;
对于像下面find
一样laravel提供的即可静态方式调用又可对象上调用的方法,又是怎么实现的呢?
- 首先,
App\User
中肯定没定义find
方法。 App\User
继承了\Illuminate\Database\Eloquent\Model
,Model
没定义find
方法,但定义了__callStatic
方法:
// \Illuminate\Database\Eloquent\Model::__callStatic
public static function __callStatic($method, $parameters)
{
return (new static)->$method(...$parameters);
}
__callStatic
内部new
了一个对象,在对象上调用find
方法。此时,User::find(1)
的调用被转化为类似于(new User)->find(1)
的调用。
App\User
类对象没有find
方法,但从Model
继承了_call
方法:
// \Illuminate\Database\Eloquent\Model::__call
/**
* 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);
}
//\Illuminate\Support\Traits\ForwardsCalls::forwardCallTo
/**
* Forward a method call to the given object.
*
* @param mixed $object
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
protected function forwardCallTo($object, $method, $parameters)
{
try {
return $object->{$method}(...$parameters);
} catch (Error | BadMethodCallException $e) {
$pattern = '~^Call to undefined method (?P<class>[^:]+)::(?P<method>[^\(]+)\(\)$~';
if (! preg_match($pattern, $e->getMessage(), $matches)) {
throw $e;
}
if ($matches['class'] != get_class($object) ||
$matches['method'] != $method) {
throw $e;
}
static::throwBadMethodCallException($method);
}
}
_call
中使用ForwardsCalls
的forwardCallTo
方法,在this->newQuery()
上调用find
方法。
this->newQuery()
返回\Illuminate\Database\Eloquent\Builder
对象,Builder
类中定义了find
方法:
// \Illuminate\Database\Eloquent\Builder::find
public function find($id, $columns = ['*'])
{
if (is_array($id) || $id instanceof Arrayable) {
return $this->findMany($id, $columns);
}
return $this->whereKey($id)->first($columns);
}
所以,find
方法不是App\User
类继承的方法。