Slide 1

Slide 1 text

Revisiting lazy objects in PHP and Symfony @nicolasgrekas

Slide 2

Slide 2 text

Lazy Loading Don't do anything unless really needed

Slide 3

Slide 3 text

Lazy Loading Can save time and memory Perfect for short-lived requests Allows creating circular object graphs (Provides resetting for free)

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Lazy Initialization The implementation is laziness-aware

Slide 8

Slide 8 text

Value Holders An object with a public getValue() method

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Value Holders The consumers are laziness-aware

Slide 13

Slide 13 text

Virtual Proxies An object with the same interface as the real object The real object is created just-in-time

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Ghost Objects Neither the consumers nor the real object are laziness-aware Don't work with final classes Work with final methods Don't cause identity issues aka work with fluent/wither APIs

Slide 24

Slide 24 text

Virtual State Proxies Proxy the properties not the methods

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Virtual State Proxies Neither the consumers nor the real object are laziness-aware Don't work with final classes Work with final methods Don't cause identity issues aka work with fluent/wither APIs

Slide 27

Slide 27 text

Native Lazy Objects RFC pending

Slide 28

Slide 28 text

class ReflectionClass { [...] public int const SKIP_INITIALIZATION_ON_SERIALIZE = 1; public int const SKIP_DESTRUCTOR = 2; public function newLazyGhost(callable $initializer, int $options = 0): object; public function newLazyProxy(callable $factory, int $options = 0): object; public function resetAsLazyGhost(object $object, callable $initializer, int $options = 0): void; public function resetAsLazyProxy(object $object, callable $factory, int $options = 0): void; public function isUninitializedLazyObject(object $instance): bool; [...]

Slide 29

Slide 29 text

class ReflectionClass { [...] /** * Initializes a lazy object (no-op if the object is already initialized.) * * The backing object is returned, which can be another instance than the lazy object when the virtual strategy is used. */ public function initializeLazyObject(object $object): object; public function isUninitializedLazyObject(object $object): object; public function markLazyObjectAsInitialized(object $object): void; public function getLazyInitializer(object $object): ?callable; }

Slide 30

Slide 30 text

class ReflectionProperty { [...] /** * Marks a property as *not* triggering initialization when being accessed. */ public function skipLazyInitialization(object $object): void; /** * Sets a property *without* triggering initialization while skipping hooks if any. */ public function setRawValueWithoutLazyInitialization(object $object, mixed $value): void; }

Slide 31

Slide 31 text

Native Lazy Objects Neither the consumers nor the real object are laziness-aware Do work with final classes Does works with fluent/wither APIs Stellar performance

Slide 32

Slide 32 text

TL;DR github.com/sponsors/nicolas-grekas symfony.com/sponsor opencollective.com/phpfoundation The heavy lifting is done for you Be creative

Slide 33

Slide 33 text

Thank you! @nicolasgrekas See you in Vienna! live.symfony.com