Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unleashing the power of lazy objects in PHP 🪄

Unleashing the power of lazy objects in PHP 🪄

Lazy-objects are a bit magical. They are created empty and populate themselves on-demand. They are useful when an object is heavy to instantiate but is not always used, like for example Doctrine entities or Symfony lazy-services.

But do you know how they work internally? In this talk, I'll tell you about the mechanisms provided by PHP to enable such use cases 🧙. Because doing this sort of wizardry is not common practice, I'll also introduce you to two new traits that package those lazy-loading behaviors introduced in Symfony 6.2: one for virtual inheritance proxies, and one for ghost objects 👻.

While lazy objects used to require complex code generation, these new traits make it way easier to leverage them, opening up possible innovations; lazy arguments, lazy properties, or by-design lazy classes to name a few ones. What will you come up with? Let me know after you've seen this talk!

Nicolas Grekas

November 17, 2022
Tweet

More Decks by Nicolas Grekas

Other Decks in Programming

Transcript

  1. @nicolasgrekas • Joined in 2013 at v2.5 • SensioLabs >

    Blackfire.io > Symfony Corp. • 3000+ PRs (10%) • 4000+ commits (6%) • 9780+ followers • 100+ sponsors (past+present)
  2. Lazy Loading Can save time and memory Perfect for short-lived

    requests • Lazyness = autoloader • Cache = opcache
  3. The 4 kinds of Lazy Loading • Lazy Initialization •

    Value holders • Virtual proxies • Ghost objects
  4. class ClosureHolder { public function __construct( private Closure|string $value )

    { } public function getValue(): string { if ($this->value instanceof Closure) { $this->value = ($this->value)(); } return $this->value; }
  5. class LocatorHolder { public function __construct( private ContainerInterface $workflows )

    { } public function getWorkflow(string $name) { return $this->workflows->get($name); }
  6. class LocatorHolder { public function __construct( #[TaggedLocator('workflow', 'name')] private ContainerInterface

    $workflows ) { } public function getWorkflow(string $name) { return $this->workflows->get($name); }
  7. class IterableHolder { public function __construct( private iterable $workflows )

    { } public function getWorkflows(): Generator { foreach ($this->workflows as $workflow) { yield $workflow; } }
  8. class IterableHolder { public function __construct( #[TaggedIterator('workflow')] private iterable $workflows

    ) { } public function getWorkflows(): Generator { foreach ($this->workflows as $workflow) { yield $workflow; } }
  9. Virtual Proxies An object with the same interface as the

    real object The first time any methods are called, the real object is created and called
  10. class VirtualChildEntityManager extends EntityManager { private parent $em; private bool

    $isInitialized = false; public function __construct( private Closure $initializer ) { } public function find(string $class, $id) { if (!$this->isInitialized) { ($this->initializer)($this); } return $this->em->find($class, $id); }
  11. class VirtualProxyEntityManager implements EntityManagerInterface { private EntityManagerInterface $em; private bool

    $isInitialized = false; public function __construct( private Closure $initializer ) { } public function find(string $class, $id) { if (!$this->isInitialized) { ($this->initializer)($this); } return $this->em->find($class, $id); }
  12. Virtual Proxies Neither the consumers nor the real object are

    laziness-aware Do work with final classes Can cause identity issues aka break fluent/wither APIs
  13. Ghost Objects The real object without any data The first

    time any methods are called, the ghost populates its properties
  14. class GhostEntityManager extends EntityManager { public function __construct( private Closure

    $initializer ) { unset(/* all properties defined by the parent */); } public function __get($name) { // initialize all parent properties } // ...
  15. Ghost Objects Neither the consumers nor the real object are

    laziness-aware Don't work with final classes Don't cause identity issues aka work with fluent/wither APIs
  16. namespace Proxies\__CG__\App\Entity; use Doctrine\Persistence\Proxy; /** * DO NOT EDIT THIS

    FILE - IT WAS CREATED BY DOCTRINE'S PROXY GENERATOR */ class Conference extends \App\Entity\Conference implements Proxy { use \Symfony\Component\VarExporter\LazyGhostTrait
  17. class WithLazyProperty { public readonly string $slowToComputeProperty; use LazyGhostTrait; private

    int $lazyObjectId; public function __construct() { self::createLazyGhost(instance: $this, initializer: [ 'slowToComputeProperty' => $this->doComputeSlowProperty(...), ]); } private function doComputeSlowProperty(): string { return //... }
  18. class WithLazyProperty { public readonly string $slowToComputeProperty; use LazyGhostTrait; public

    function __construct() { $this->createLazyProperties([ 'slowToComputeProperty' => $this->doComputeSlowProperty(...), ]); } private function doComputeSlowProperty(): string { return //... }
  19. class VirtualChildEntityManager extends EntityManager { private parent $em; public function

    __construct( private Closure $initializer ) { unset(/* all properties defined by the parent */); } public function __get($name) { $this->em ??= ($this->initializer)($this); return $this->em->$name; }