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

    View Slide

  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)

    View Slide

  3. Lazy Loading
    Don't do anything
    unless really needed

    View Slide

  4. Lazy Loading
    Can save time and memory
    Perfect for short-lived requests
    • Lazyness = autoloader
    • Cache = opcache

    View Slide

  5. The 4 kinds of
    Lazy Loading
    • Lazy Initialization
    • Value holders
    • Virtual proxies
    • Ghost objects

    View Slide

  6. Lazy Initialization
    Check properties
    for a marker value
    (usually null) and
    load them on
    demand

    View Slide

  7. class LazyInitializedClass
    {
    public function getData()
    {
    return $this->data ??= $this->doGetData();
    }

    View Slide

  8. Lazy
    Initialization The implementation is laziness-aware

    View Slide

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

    View Slide

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

    View Slide

  11. class LocatorHolder
    {
    public function __construct(
    private ContainerInterface $workflows
    ) {
    }
    public function getWorkflow(string $name)
    {
    return $this->workflows->get($name);
    }

    View Slide

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

    View Slide

  13. class IterableHolder
    {
    public function __construct(
    private iterable $workflows
    ) {
    }
    public function getWorkflows(): Generator
    {
    foreach ($this->workflows as $workflow) {
    yield $workflow;
    }
    }

    View Slide

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

    View Slide

  15. Value Holders The consumers are laziness-aware

    View Slide

  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

    View Slide

  17. class EntityManager implements EntityManagerInterface
    {
    //...
    public function find(string $class, $id)
    {
    //...
    }

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  23. Ghost Objects
    The real object
    without any data
    The first time any
    methods are called,
    the ghost populates
    its properties

    View Slide

  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
    }
    // ...

    View Slide

  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

    View Slide

  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

    View Slide

  27. Symfony 6.2
    End of the month

    View Slide

  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 //...
    }

    View Slide

  29. class WithLazyProperty
    {
    public readonly string $slowToComputeProperty;
    use LazyGhostTrait;
    public function __construct()
    {
    $this->createLazyProperties([
    'slowToComputeProperty' => $this->doComputeSlowProperty(...),
    ]);
    }
    private function doComputeSlowProperty(): string
    {
    return //...
    }

    View Slide

  30. class Foo
    {
    public function __construct(
    #[Autowire(lazy: true)]
    private BarInterface $bar,
    ) {
    }

    View Slide

  31. TL;DR
    The heavy lifting is
    done for you
    Let me know your
    creative ideas

    View Slide

  32. Thank you!
    @nicolasgrekas

    View Slide

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

    View Slide