$30 off During Our Annual Pro Sale. View Details »

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. Unleashing the power of lazy objects in PHP 🪄 @nicolasgrekas

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

    Blackfire.io > Symfony Corp. • 3000+ PRs (10%) • 4000+ commits (6%) • 9780+ followers • 100+ sponsors (past+present)
  3. Lazy Loading Don't do anything unless really needed

  4. Lazy Loading Can save time and memory Perfect for short-lived

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

    Value holders • Virtual proxies • Ghost objects
  6. Lazy Initialization Check properties for a marker value (usually null)

    and load them on demand
  7. class LazyInitializedClass { public function getData() { return $this->data ??=

    $this->doGetData(); }
  8. Lazy Initialization The implementation is laziness-aware

  9. Value Holders An object with a public getValue() method

  10. 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; }
  11. class LocatorHolder { public function __construct( private ContainerInterface $workflows )

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

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

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

    ) { } public function getWorkflows(): Generator { foreach ($this->workflows as $workflow) { yield $workflow; } }
  15. Value Holders The consumers are laziness-aware

  16. 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
  17. class EntityManager implements EntityManagerInterface { //... public function find(string $class,

    $id) { //... }
  18. 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); }
  19. 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); }
  20. 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
  21. #[Autoconfigure(lazy: true)] class EntityManager implements EntityManagerInterface { //...

  22. #[Autoconfigure(lazy: EntityManagerInterface::class)] class EntityManager implements EntityManagerInterface { //...

  23. Ghost Objects The real object without any data The first

    time any methods are called, the ghost populates its properties
  24. 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 } // ...
  25. 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
  26. 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
  27. Symfony 6.2 End of the month

  28. 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 //... }
  29. class WithLazyProperty { public readonly string $slowToComputeProperty; use LazyGhostTrait; public

    function __construct() { $this->createLazyProperties([ 'slowToComputeProperty' => $this->doComputeSlowProperty(...), ]); } private function doComputeSlowProperty(): string { return //... }
  30. class Foo { public function __construct( #[Autowire(lazy: true)] private BarInterface

    $bar, ) { }
  31. TL;DR The heavy lifting is done for you Let me

    know your creative ideas
  32. Thank you! @nicolasgrekas

  33. 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; }