Last active
January 21, 2023 16:49
-
-
Save kbond/f7ec8dca21a234b0b4da251c85f865d5 to your computer and use it in GitHub Desktop.
Proxy Builder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
$builder = (new GhostProxyBuilder(Object::class)) // or (new VirtualProxyBuilder(Object::class)) | |
->named('MyObjectProxy') // name for the proxy class (defaults to Proxy{hash of interfaces/traits} | |
->in('some/dir') // generated proxy class is put in this dir (defaults to sys_tmp_dir()) | |
->debugMode() // enable always regenerating the files | |
->implements(Interface1::class, Interface2::class) // generated proxy will implement these interfaces | |
->using(Trait1::class, Trait2::class) // generated proxy will use these traits | |
; | |
$builder->contents(); // string generated proxy class contents as string does not create/require the file | |
$builder->class(); // class-string<Object1&Interface1&Interface2&LazyObjectInterface> (creates/requires the file) | |
$builder->create($initializer); // Object1&Interface1&Interface2&LazyObjectInterface |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @author Kevin Bond <[email protected]> | |
* | |
* @template T of object | |
* @extends ProxyBuilder<T> | |
*/ | |
final class GhostProxyBuilder extends ProxyBuilder | |
{ | |
/** | |
* @see LazyGhostTrait::createLazyGhost() | |
* | |
* @param array<string,callable(T,string=,?string=):mixed>|callable(T,string=,?string=):mixed $initializer | |
* | |
* @return T&LazyObjectInterface | |
*/ | |
public function create(callable|array $initializer): LazyObjectInterface | |
{ | |
return $this->class()::createLazyGhost(\is_callable($initializer) ? MirrorCallable::closureFrom($initializer) : $initializer); // @phpstan-ignore-line | |
} | |
protected static function proxyTrait(): string | |
{ | |
return LazyGhostTrait::class; | |
} | |
protected static function proxyMethod(): string | |
{ | |
return 'generateLazyGhost'; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @template T of object | |
*/ | |
abstract class ProxyBuilder | |
{ | |
/** @var class-string<T> */ | |
private string $class; | |
private string $name; | |
private string $directory; | |
private bool $debug = false; | |
/** @var class-string[] */ | |
private array $interfaces = []; | |
/** @var class-string[] */ | |
private array $traits = []; | |
/** @var array<string,string> */ | |
private array $replacements = []; | |
/** | |
* @param class-string<T>|T $class | |
*/ | |
final public function __construct(string|object $class) | |
{ | |
if (!\class_exists(ProxyHelper::class)) { | |
throw new \LogicException('symfony/var-exporter 6.2+ required to generate proxies. Install with "composer require symfony/var-exporter".'); | |
} | |
$this->class = \is_string($class) ? $class : $class::class; | |
} | |
/** | |
* @return $this | |
*/ | |
final public function named(string $name): self | |
{ | |
$this->name = $name; | |
return $this; | |
} | |
/** | |
* @return $this | |
*/ | |
final public function in(string $directory): self | |
{ | |
$this->directory = $directory; | |
return $this; | |
} | |
/** | |
* @return $this | |
*/ | |
final public function debugMode(): self | |
{ | |
$this->debug = true; | |
return $this; | |
} | |
/** | |
* @param class-string ...$interface | |
* | |
* @return $this | |
*/ | |
final public function implementing(string ...$interface): self | |
{ | |
$this->interfaces = \array_merge($this->interfaces, $interface); | |
return $this; | |
} | |
/** | |
* @param class-string ...$trait | |
* | |
* @return $this | |
*/ | |
final public function using(string ...$trait): self | |
{ | |
$this->traits = \array_merge($this->traits, $trait); | |
return $this; | |
} | |
/** | |
* @return $this | |
*/ | |
final public function replace(string $search, string $replace): self | |
{ | |
$this->replacements[$search] = $replace; | |
return $this; | |
} | |
final public function contents(): string | |
{ | |
$contents = \sprintf('class %s%s', $this->name(), ProxyHelper::{static::proxyMethod()}(new \ReflectionClass($this->class))); | |
foreach ($this->replacements() as $search => $replace) { | |
$contents = \str_replace($search, $replace, $contents); | |
} | |
return $contents; | |
} | |
/** | |
* @return class-string<T&LazyObjectInterface> | |
*/ | |
final public function class(): string | |
{ | |
if (\class_exists($name = $this->name())) { | |
return $name; // @phpstan-ignore-line | |
} | |
$filename = $this->filenameFor($name); | |
if (!$this->debug && \file_exists($filename)) { | |
require_once $filename; | |
return $name; // @phpstan-ignore-line | |
} | |
if (!\is_dir($dir = \dirname($filename)) && !@\mkdir($dir, recursive: true) && !\is_dir($dir)) { | |
throw new \RuntimeException(\sprintf('Directory "%s" was not created', $dir)); | |
} | |
\file_put_contents($filename, "<?php\n\n".$this->contents()); | |
require_once $filename; | |
return $name; // @phpstan-ignore-line | |
} | |
/** | |
* @return LazyObjectInterface&T | |
*/ | |
abstract public function create(callable $initializer): LazyObjectInterface; | |
abstract protected static function proxyTrait(): string; | |
abstract protected static function proxyMethod(): string; | |
/** | |
* @return class-string<T> | |
*/ | |
final protected function className(): string | |
{ | |
return $this->class; | |
} | |
private function name(): string | |
{ | |
return $this->name ??= 'Proxy'.\sha1(\implode('', \array_merge( | |
[static::class], | |
$this->interfaces, | |
$this->traits | |
))); | |
} | |
private function filenameFor(string $name): string | |
{ | |
return \sprintf('%s/%s.php', $this->directory ?? \sys_get_temp_dir(), $name); | |
} | |
/** | |
* @return \Traversable<string,string> | |
*/ | |
private function replacements(): \Traversable | |
{ | |
foreach ($this->interfaces as $interface) { | |
if (!\interface_exists($interface)) { | |
throw new \LogicException(\sprintf('"%s" is not an interface.', $interface)); | |
} | |
yield LazyObjectInterface::class => \sprintf('%s, \%s', LazyObjectInterface::class, $interface); | |
} | |
foreach ($this->traits as $trait) { | |
if (!\trait_exists($trait)) { | |
throw new \LogicException(\sprintf('"%s" is not a trait.', $trait)); | |
} | |
yield static::proxyTrait() => \sprintf('%s, \%s', static::proxyTrait(), $trait); | |
} | |
foreach ($this->replacements as $search => $replace) { | |
yield $search => $replace; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @author Kevin Bond <[email protected]> | |
* | |
* @template T of object | |
* @extends ProxyBuilder<T> | |
*/ | |
final class VirtualProxyBuilder extends ProxyBuilder | |
{ | |
/** | |
* @see LazyProxyTrait::createLazyProxy() | |
* | |
* @param T|callable():T $initializer | |
* | |
* @return T&LazyObjectInterface | |
*/ | |
public function create(callable|object $initializer): LazyObjectInterface | |
{ | |
$initializer = \is_a($initializer, $this->className(), true) ? static fn() => $initializer : $initializer; // @phpstan-ignore-line | |
return $this->class()::createLazyProxy(MirrorCallable::closureFrom($initializer)); // @phpstan-ignore-line | |
} | |
protected static function proxyTrait(): string | |
{ | |
return LazyProxyTrait::class; | |
} | |
protected static function proxyMethod(): string | |
{ | |
return 'generateLazyProxy'; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment