webprofiler-10.0.x-dev/src/Entity/ConfigEntityStorageDecoratorGenerator.php
src/Entity/ConfigEntityStorageDecoratorGenerator.php
<?php
declare(strict_types=1);
namespace Drupal\webprofiler\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\PhpStorage\PhpStorageFactory;
use Drupal\webprofiler\DecoratorGeneratorInterface;
use PhpParser\Builder\Method;
use PhpParser\Builder\Param;
use PhpParser\BuilderFactory;
use PhpParser\Error;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\FindingVisitor;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter;
/**
* Generate decorators for config entity storage classes.
*/
class ConfigEntityStorageDecoratorGenerator implements DecoratorGeneratorInterface {
/**
* DecoratorGenerator constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The Entity type manager service.
*/
public function __construct(
protected readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* {@inheritdoc}
*/
public function generate(): void {
$classes = $this->getClasses();
foreach ($classes as $class) {
try {
$methods = $this->getMethods($class);
$body = $this->createDecorator($class, $methods);
$this->writeDecorator($class['id'], $body);
}
catch (\Exception $e) {
throw new \Exception('Unable to generate decorator for class ' . $class['class'] . '. ' . $e->getMessage());
}
}
}
/**
* {@inheritdoc}
*/
public function getDecorators(): array {
$decorators = &drupal_static(__FUNCTION__);
if (!isset($decorators)) {
$classes = $this->getClasses();
$decorators = \array_map(static function ($class) {
return $class['decoratorClass'];
}, $classes);
}
return $decorators;
}
/**
* Return information about every config entity storage classes.
*
* @return array
* Information about every config entity storage classes.
*/
private function getClasses(): array {
// @phpstan-ignore-next-line
$cache_backend = \Drupal::cache('default');
$cid = 'webprofiler:config_entity_storage_classes';
$cache = $cache_backend->get($cid);
if ($cache) {
return $cache->data;
}
$definitions = $this->entityTypeManager->getDefinitions();
$classes = [];
foreach ($definitions as $definition) {
try {
$classPath = $this->getClassPath($definition->getStorageClass());
$uses = $this->getUses($classPath);
$class = $this->getClass($classPath);
if ($class != NULL) {
$namespace = $class->namespacedName->slice(0, -1)->toString();
$classes[$definition->id()] = [
'id' => $definition->id(),
'namespace' => $namespace,
'class' => $class->name->name,
'interface' => '\\' . \implode('\\', $class->implements[0]->getParts()),
'decoratorClass' => $namespace . '\\' . $class->name->name . 'Decorator',
'uses' => $uses,
];
}
}
catch (Error $error) {
echo "Parse error: {$error->getMessage()}\n";
return [];
}
catch (\ReflectionException $error) {
echo "Reflection error: {$error->getMessage()}\n";
return [];
}
}
$cache_backend->set($cid, $classes, Cache::PERMANENT, ['webprofiler', 'config:core.extension']);
return $classes;
}
/**
* Get the filename of the file in which the class has been defined.
*
* @param string $class
* A class name.
*
* @return string
* The filename of the file in which the class has been defined.
*
* @throws \ReflectionException
*/
private function getClassPath(string $class): string {
$reflector = new \ReflectionClass($class);
return $reflector->getFileName();
}
/**
* Parses PHP code into a node tree.
*
* @param string $classPath
* The filename of the file in which a class has been defined.
*
* @return \PhpParser\Node\Stmt[]|null
* Array of statements.
*/
private function getAst(string $classPath): ?array {
$code = \file_get_contents($classPath);
$parser = (new ParserFactory())->createForHostVersion();
return $parser->parse($code);
}
/**
* Return TRUE if this Node represents a config entity storage class.
*
* @param \PhpParser\Node $node
* The Node to check.
*
* @return bool
* TRUE if this Node represents a config entity storage class.
*/
private function isConfigEntityStorage(Node $node): bool {
if (!$node instanceof Class_) {
return FALSE;
}
if ($node->extends !== NULL &&
$node->extends->getParts()[0] == 'ConfigEntityStorage' &&
isset($node->implements[0]) &&
$node->implements[0]->getParts()[0] != ''
) {
return TRUE;
}
return FALSE;
}
/**
* Create the decorator from class information.
*
* @param array $class
* The class information.
*
* @return \PhpParser\Node\Stmt\ClassMethod[]
* The methods of the class.
*
* @throws \Exception
*/
private function getMethods(array $class): array {
$classPath = $this->getClassPath($class['interface']);
$ast = $this->getAst($classPath);
$nodeFinder = new NodeFinder();
/** @var \PhpParser\Node\Stmt\ClassMethod[] $nodes */
$nodes = $nodeFinder->find($ast, static function (Node $node) {
return $node instanceof ClassMethod;
});
return $nodes;
}
/**
* Create the decorator from class information and methods.
*
* @param array $class
* The class information.
* @param \PhpParser\Node\Stmt\ClassMethod[] $methods
* The methods of the class.
*
* @return string
* The decorator class body.
*
* phpcs:disable Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing
*/
private function createDecorator(array $class, array $methods): string {
$decorator = $class['class'] . 'Decorator';
$factory = new BuilderFactory();
$file = $factory
->namespace($class['namespace'])
->addStmt($factory->use('Drupal\webprofiler\Entity\ConfigEntityStorageDecorator'));
foreach ($class['uses'] as $use) {
$file->addStmt($factory->use($use));
}
$generated_class = $factory
->class($decorator)
->extend('ConfigEntityStorageDecorator')
->implement($class['interface'])
->setDocComment('
/**
* This file is auto-generated by the Webprofiler module.
*/',
);
foreach ($methods as $method) {
$generated_class->addStmt($this->createMethod($method));
}
$file->addStmt($generated_class);
$stmts = [$file->getNode()];
$prettyPrinter = new PrettyPrinter\Standard();
// Add a newline at the end of the file.
return $prettyPrinter->prettyPrintFile($stmts) . "\n";
}
/**
* Create a decorator method.
*
* @param \PhpParser\Node\Stmt\ClassMethod $method
* The method.
*
* @return \PhpParser\Builder\Method
* The generated method.
*/
private function createMethod(ClassMethod $method): Method {
$factory = new BuilderFactory();
$generated_method = $factory->method($method->name->name)->makePublic();
foreach ($method->getParams() as $param) {
$generated_method->addParam($this->createParameter($param));
}
$generated_body = $factory->methodCall(
new Node\Expr\PropertyFetch(new Node\Expr\Variable('this'), 'getOriginalObject()'),
$method->name->name,
\array_map(static function ($param) {
return new Node\Expr\Variable($param->var->name);
}, $method->getParams()),
);
// If return type is different from void, add a return statement.
if (!$method->getReturnType() instanceof Node\Identifier || $method->getReturnType()->name != 'void') {
$generated_body = new Node\Stmt\Return_($generated_body);
}
$generated_method->addStmt($generated_body);
if ($method->getReturnType() != NULL) {
$generated_method->setReturnType($method->getReturnType());
}
return $generated_method;
}
/**
* Create a decorator method parameter.
*
* @param \PhpParser\Node\Param $param
* The method parameter.
*
* @return \PhpParser\Builder\Param
* The generated parameter.
*/
private function createParameter(Node\Param $param): Param {
$factory = new BuilderFactory();
$generated_param = $factory
->param($param->var->name);
if ($param->type != NULL) {
$generated_param->setType($param->type);
}
if ($param->default != NULL) {
$generated_param->setDefault($param->default);
}
if ($param->byRef) {
$generated_param->makeByRef();
}
if ($param->variadic) {
$generated_param->makeVariadic();
}
return $generated_param;
}
/**
* Write a decorator class body to file.
*
* @param string $name
* The class name.
* @param string $body
* The class body.
*/
private function writeDecorator(string $name, string $body): void {
$storage = PhpStorageFactory::get('webprofiler');
if (!$storage->exists($name)) {
$storage->save($name, $body);
}
}
/**
* Get the list of classes in a file.
*
* @param string $classPath
* The filename of the file in which a class has been defined.
*
* @return \PhpParser\Node\Stmt\Class_|null
* The list of classes in a file.
*/
private function getClass(string $classPath): ?Class_ {
$ast = $this->getAst($classPath);
$visitor = new FindingVisitor(function (Node $node) {
return $this->isConfigEntityStorage($node);
});
$traverser = new NodeTraverser();
$traverser->addVisitor($visitor);
$traverser->addVisitor(new NameResolver());
$traverser->traverse($ast);
/** @var \PhpParser\Node\Stmt\Class_[] $nodes */
$nodes = $visitor->getFoundNodes();
if (\count($nodes) == 0) {
return NULL;
}
return \reset($nodes);
}
/**
* Get the list of uses in a class.
*
* @param string $classPath
* The filename of the file in which a class has been defined.
*
* @return array
* The list of uses in a class.
*/
private function getUses(string $classPath): array {
$ast = $this->getAst($classPath);
$visitor = new FindingVisitor(static function (Node $node) {
return $node instanceof Node\Stmt\Use_;
});
$traverser = new NodeTraverser();
$traverser->addVisitor($visitor);
$traverser->addVisitor(new NameResolver());
$traverser->traverse($ast);
/** @var \PhpParser\Node\Stmt\Use_[] $nodes */
$nodes = $visitor->getFoundNodes();
return \array_map(static function (Node\Stmt\Use_ $node) {
return $node->uses[0]->name->toString();
}, $nodes);
}
}
