Skip to content

Instantly share code, notes, and snippets.

@kbond
Last active January 21, 2023 16:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kbond/f7ec8dca21a234b0b4da251c85f865d5 to your computer and use it in GitHub Desktop.
Save kbond/f7ec8dca21a234b0b4da251c85f865d5 to your computer and use it in GitHub Desktop.
Proxy Builder
<?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
<?php
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @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';
}
}
<?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;
}
}
}
<?php
/**
* @author Kevin Bond <kevinbond@gmail.com>
*
* @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