首页
Preview

重新审视 PHP 中的懒加载代理

在Symfony 6.2中,VarExporter组件将会推出两个新的traits,帮助实现延迟加载对象。

正如它们的名字所示,延迟加载对象只在实际需要时才初始化;通常是在访问它们的属性时。当一个对象实例化非常耗费资源,但不总是被使用时,就会使用它们。

延迟对象有两个主要用途:延迟服务和延迟实体。

  • 你可以在Symfony依赖注入容器中找到延迟服务。以下是文档的摘录:假设你有一个NewsletterManager,并将mailer服务注入其中。只有少数方法在NewsletterManager上实际使用mailer,但即使在不需要它时,mailer服务也总是被实例化以构造NewsletterManager。通过使它变为延迟加载,只有在NewsletterManager实际发送电子邮件时才会初始化mailer服务。
  • 你可以在Doctrine ORM中找到延迟实体。在其中,它们用于创建尚未填充的实体和集合。只有在首次访问任何属性时,延迟初始化才会通过执行SQL查询来检索它们的状态。

如果你已经了解这个概念,那么你肯定知道ocramius/proxy-manager库。虽然Doctrine ORM使用了自己的实现,但这个库是PHP中惰性加载代理的事实标准实现。自2013年以来,我们一直在Symfony容器中使用该软件包,引入了symfony/proxy-manager-bridge。对于它的作者,工作令人印象深刻,也是很有启发性的。

不幸的是,1.5年前,由于Symfony的维护政策和ProxyManager的维护政策之间的不兼容性,我们决定维护一个分支,你可能已经在使用:friendsofphp/proxy-manager-lts。这个分支与原始库保持同步,但被修补:

  • 支持广泛的PHP和Composer版本,
  • 修复了一些以前需要在proxy-manager-bridge上进行猴子补丁的行为(例如跳过未初始化实例的析构函数或与流畅API兼容性),
  • 并支持更新的PHP版本(截至目前,ProxyManager不支持PHP 8.1,但我们在Symfony 6.1中需要这个版本。) 不要误解我,我在这里描述的问题是由我们使用该代码的方式所创建的,而不是由源代码本身创建的。开源动态意味着作者对他们代码的用户完全没有责任。这也意味着,向后贡献可能是期望的。这就是为什么我们已经提交了所有有意义的更改。🤞

但是,这种情况不太理想,因为它会产生摩擦和挫败感。

解决这个问题是我写下前面提到的两个traits的首要原因。

原因#2是一个技术上的原因,我已经想了好几年:“我们能否用几个通用的traits替换ProxyManager生成的代码?”你已经想出来了,我为你想出了答案:“可以!”这是巨大的,因为它意味着我们可以将延迟加载实现的复杂性移到几个容易审计的文件中。

所以在这里,让我向你介绍LazyGhostTraitLazyProxyTrait

通过使用LazyGhostTrait,你可以为一个类添加延迟加载功能。这通过创建空实例(取消所有属性)并仅在直接或间接访问属性(通过调用方法)时计算它们的状态来实现。下面是一个例子:

class FooLazyGhost extends Foo
{
   use LazyGhostTrait;

   private int $lazyObjectId;
}

$foo = FooLazyGhost::createLazyGhost(initializer: function (Foo $instance): void {
   // [...] Use whatever heavy logic you need here
   // to compute the $dependencies of the $instance
   $instance->__construct(...$dependencies);
   // [...] Call setters, etc. if needed
});

// $foo is now a lazy-loading ghost object. The initializer will
// be called only when and if a *property* is accessed.

你还可以通过为初始化程序添加两个参数来逐个属性地部分初始化对象:

$initializer = function (Foo $instance, string $propertyName, ?string $propertyScope): mixed {
   if (Foo::class === $propertyScope && 'bar' === $propertyName) {
       return 123;
   }
   // [...] Add more logic for the other properties
};

另外,LazyProxyTrait可以用来创建虚拟代理:

$proxyCode = ProxyHelper::generateLazyProxy(new ReflectionClass(Foo::class));
// $proxyCode contains the reference to LazyProxyTrait
// and should be dumped into a file in production envs
eval('class FooLazyProxy'.$proxyCode);

$foo = FooLazyProxy::createLazyProxy(initializer: function (): Foo {
   // [...] Use whatever heavy logic you need here
   // to compute the $dependencies of the $instance
   $instance = new Foo(...$dependencies);
   // [...] Call setters, etc. if needed

   return $instance;
});
// $foo is now a lazy-loading virtual proxy object. The initializer will
// be called only when and if a *method* is called.

正如你可能已经注意到的那样,这个代码使用了一个ProxyHelper类来生成一些样板。这个代码生成是完全可选的,因为你可以决定直接使用trait。我在这个PR中这样做,为Cache组件中的延迟加载Redis类提供支持。

Ghost对象只能使用具体和非内部类。在通用情况下,它们与在初始化程序中使用工厂不兼容。

虚拟代理可以使用具体、抽象或内部类。它们提供了一个看起来像实际对象的API,并将调用转发给它们。它们可能会引起身份问题,因为代理可能不被视为与它们代理的实际对象等效。

关于这个身份问题,LazyProxyTrait只能代理实现的_属性_。因此,当一个方法return $this;(或clone $this)时,所讨论的$this是代理本身,而不是装饰实例。这意味着流畅和wither API可以正常工作!(出于性能原因和内部类的原因,装饰方法仍然可以生成。)

ProxyHelper类抛出的异常可以帮助决定哪个trait最适合特定的类。

Ghost对象和虚拟代理都提供了LazyObjectInterface的实现,它允许将它们重置为初始状态或在需要时强制初始化它们。请注意,重置Ghost对象会跳过它们的只读属性。你应该使用虚拟代理来重置只读属性。在Symfony 6.2中,DependencyInjection组件将开始使用这些traits。当然,请尝试并反馈任何问题!

有关本文的更多信息,请查看以下PR:

本文原载于 https://symfony.com

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
anko
宽以待人处事,严于律己修身。

评论(0)

添加评论