Laravel Eloquent 魔法的幕后功能
你是否曾经好奇,为什么User::find(1)* 返回的是一个* User* 实例,而不是某个通用模型对象?这就是原因。
一个你可能从未注意到的难题
当你在 Laravel 中编写这样的代码时:
$user = User::find(1);$post = Post::find(5);
你会分别得到一个 User 实例和一个 Post 实例。看起来很明显,对吧?
但仔细想想——find() 方法只在基类 Model 中定义了一次。它是如何知道从 User 调用时返回 User,从 Post 调用时返回 Post 的?
答案就是晚期静态绑定——PHP OOP 中最重要却最少被讨论的功能之一。
问题:self:: 在编译时就被冻结
让我们从晚期静态绑定旨在修复的 bug 开始。
classModel{publicstaticfunctioncreate(): static{returnnewself(); // 看起来没问题,但实际上有问题 }}classUserextendsModel{}$user = User::create();echo get_class($user); // "Model"
我们调用了 User::create(),却得到一个 Model 实例。这不对。
原因:self:: 总是引用方法实际编写所在的类——在本例中是 Model。不管你通过 User 调用它,PHP 都在编译时解析 self::,那时它还不知道运行时代码的实际调用方式。
修复:static::
PHP 5.3 引入了 static:: 关键字,专门用来解决这个问题。与 self:: 不同,它在运行时根据实际调用的类来解析。
classModel{publicstaticfunctioncreate(): static{returnnewstatic(); // 在运行时解析 }}classUserextendsModel{}classPostextendsModel{}$user = User::create();echo get_class($user); // "User"$post = Post::create();echo get_class($post); // "Post"
相同的方法,相同的代码——但现在它正确地为调用它的类创建实例。
self:: 与 static:: ——并排对比
classParentClass{publicstaticfunctionselfMethod(): string{returnself::class; // 总是 "ParentClass" } publicstaticfunctionstaticMethod(): string{returnstatic::class; // 由调用者决定 }}classChildClassextendsParentClass{}echo ChildClass::selfMethod(); // "ParentClass"echo ChildClass::staticMethod(); // "ChildClass"
一句话概括区别:
真实示例:Laravel 的 Eloquent
这正是 Eloquent 的静态方法在底层的工作方式。这里是一个简化版本:
abstractclassModel{publicstaticfunctionfind(int $id): static{// new static() 创建 User、Post 等——不是 Model $instance = newstatic();return $instance->newQuery()->find($id); }publicstaticfunctionwhere(string $column, mixed $value): QueryBuilder{return (newstatic())->newQuery()->where($column, $value); }publicstaticfunctionall(): Collection{return (newstatic())->newQuery()->get(); }}classUserextendsModel{}classPostextendsModel{}
每次调用静态 Eloquent 方法时,Laravel 内部都会执行 new static()——它会根据你调用的类创建正确的模型实例。
User::find(1); // new static() = new User()Post::where('active', true); // new static() = new Post()User::all(); // new static() = new User()
没有晚期静态绑定,每个 Eloquent 查询都会创建一个基类 Model 实例——整个 ORM 都会崩溃。
LSB 也适用于属性和常量
晚期静态绑定不仅限于方法调用。它在任何使用 static:: 的地方都有效:
classModel{protectedstatic string $table = 'models'; publicstaticfunctiongetTable(): string{returnstatic::$table; // 从调用类读取 }}classUserextendsModel{protectedstatic string $table = 'users';}classPostextendsModel{protectedstatic string $table = 'posts';}echo User::getTable();echo Post::getTable();
如果使用 self::$table,两者都会返回 "models"——来自 Model 的值。如果使用 static::$table,每个类都会读取自己覆盖的值。
这类似于 Laravel 中 Eloquent 模型的 $table、$primaryKey 和 $connection 属性的工作方式。
实用模式:静态工厂方法
晚期静态绑定启用了一种在基类上通过静态工厂方法创建对象的干净模式:
abstractclassBaseRepository{protectedstatic string $model; publicstaticfunctionfind(int $id): mixed{ $modelClass = static::$model;return $modelClass::find($id); } publicstaticfunctionall(): array{ $modelClass = static::$model;return $modelClass::all()->toArray(); }}classUserRepositoryextendsBaseRepository{protectedstatic string $model = User::class;}classPostRepositoryextendsBaseRepository{protectedstatic string $model = Post::class;}UserRepository::find(1);PostRepository::all();
一个基类,多个仓库——每个仓库都在运行时解析到正确的模型。
思维模型
记住区别的最简单方式:
self:: 是一个硬编码的地址——不管谁问,它总是去同一个地方。static::
当你编写将被继承的代码,并且子类需要自定义行为时,总是问自己:“这应该解析到我编写它的地方,还是被调用的地方?”
如果答案是“被调用的地方”——就使用 static::。
快速测试
在继续之前,预测一下这个代码的输出:
classA{publicstaticfunctioncreate(): static{returnnewstatic(); } publicstaticfunctionclassName(): string{returnstatic::class; }}classBextendsA{}classCextendsB{}echo C::className(); // ?echo get_class(B::create()); // ?
答案:"C" 和 "B"。static:: 解析到调用栈顶部的类——C 调用了 className(),B 调用了 create()。
自己运行确认一下。
总结
延迟静态绑定是 PHP 中实现多态静态方法的方法。它是 Laravel 的 Eloquent 能够在基类中定义一次 find()、where() 和 all(),并让它们在应用中的每个模型上正确工作的原因。
规则很简单:当你想引用代码所在类时,使用 self::;当你想引用运行时实际使用的类时,使用 static::。
一旦你理解了这一点,Laravel 的很多“魔法”就不再是魔法了——而是优雅的工程。