Slide 1

Slide 1 text

Unleashing the power of lazy objects in PHP 🪄 @nicolasgrekas

Slide 2

Slide 2 text

@nicolasgrekas • Joined in 2013 at v2.5 • SensioLabs > Blackfire.io > Symfony Corp. • 3000+ PRs (10%) • 4000+ commits (6%) • 9780+ followers • 100+ sponsors (past+present)

Slide 3

Slide 3 text

Lazy Loading Don't do anything unless really needed

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Lazy Initialization The implementation is laziness-aware

Slide 9

Slide 9 text

Value Holders An object with a public getValue() method

Slide 10

Slide 10 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 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Value Holders The consumers are laziness-aware

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 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 19

Slide 19 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 20

Slide 20 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 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 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 25

Slide 25 text

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

Slide 26

Slide 26 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 27

Slide 27 text

Symfony 6.2 End of the month

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Thank you! @nicolasgrekas

Slide 33

Slide 33 text

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