Files
kupshop/bundles/KupShop/ComponentsBundle/Command/GenerateComponentsYamlCommand.php
2025-08-02 16:30:27 +02:00

196 lines
6.5 KiB
PHP

<?php
namespace KupShop\ComponentsBundle\Command;
use KupShop\ComponentsBundle\Utils\ComponentsLocator;
use KupShop\ComponentsBundle\View\ComponentsViewInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
use Symfony\Component\Yaml\Yaml;
use Symfony\UX\TwigComponent\Twig\ComponentNode;
use Twig\Environment;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\IncludeNode;
use Twig\Node\Node;
use Twig\NodeVisitor\NodeVisitorInterface;
#[AsCommand(name: 'kupshop:components:generate', description: 'Generate app/components.yaml file from current shop templates')]
class GenerateComponentsYamlCommand extends Command implements NodeVisitorInterface
{
protected array $templates = [];
protected array $components = [];
protected string $entrypoint;
public function __construct(
protected ComponentsLocator $componentsLocator,
protected Environment $twig,
#[AutowireIterator('kupshop.components.view')] protected \IteratorAggregate $container,
) {
parent::__construct();
}
protected function configure(): void
{
$this->addOption(
name: 'load',
shortcut: 'l',
mode: InputOption::VALUE_OPTIONAL,
description: 'Cesta k souboru se starým components.yaml',
default: 'config/components.yaml'
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->twig->addNodeVisitor($this);
/** @var ComponentsViewInterface $view */
foreach ($this->container as $view) {
$entrypoint = $view->getEntrypoint();
if (!$entrypoint) {
throw new \Exception(sprintf('%s does not have "protected string $entrypoint = \'neco\'" specified', $view::class));
}
foreach ($view->getTemplates() as $template) {
// Kopie z TemplateTrait
if (!str_ends_with($template, '.html.twig')) {
$template = 'view/'.$template;
}
$template = preg_replace('/\.tpl$/', '.html.twig', $template);
if (!$this->twig->getLoader()->exists($template)) {
continue;
}
$this->discoverComponents($template, $entrypoint);
}
}
// Load all components with #[Blocek] with entrypoint base
foreach ($this->componentsLocator->getComponents() as $component) {
$reflectionClass = new \ReflectionClass($component['class']);
if ($reflectionClass->getAttributes('KupShop\ComponentsBundle\Attributes\Blocek', \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) {
$this->entrypoint = 'base';
$this->addComponent($component['name']);
}
if ($attribute = $this->checkAttributeExists($reflectionClass, 'KupShop\ComponentsBundle\Attributes\Entrypoint')) {
$this->entrypoint = $attribute->newInstance()->getEntrypoint();
$this->addComponent($component['name']);
}
}
$components = $this->components;
$oldComponents = Yaml::parseFile($input->getOption('load'))['components']['components'] ?? [];
ksort($components);
foreach ($components as $name => &$component) {
$version = $oldComponents[$name]['version'] ?? $this->componentsLocator->findComponentByName($name)['latest_version'] ?? $this->componentsLocator->findUnusedComponentByName($name)['latest_version'];
$component['entrypoint'] = array_keys($component['entrypoint']);
$component['version'] = $version;
}
$output->write(Yaml::dump(['components' => ['components' => $components]], inline: 4));
return 0;
}
private function checkAttributeExists(\ReflectionClass $class, string $name): ?\ReflectionAttribute
{
if ($result = ($class->getAttributes($name, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null)) {
return $result;
}
if ($parent = $class->getParentClass()) {
return $this->checkAttributeExists($parent, $name);
}
return null;
}
protected function discoverComponents($template, $entrypoint = null): void
{
if ($entrypoint) {
$this->entrypoint = $entrypoint;
$this->templates = [];
}
if (array_key_exists($template, $this->templates)) {
return;
}
$this->templates[$template] = 1;
$source = $this->twig->getLoader()->getSourceContext($template);
$nodes = $this->twig->parse($this->twig->tokenize($source));
if ($nodes->hasNode('parent') && $parent = $nodes->getNode('parent')) {
$this->discoverComponents($parent->getAttribute('value'));
}
if ($entrypoint) {
unset($this->entrypoint);
}
}
protected function addComponent($name): void
{
$component = $this->componentsLocator->findComponentByName($name);
if (!$component) {
return;
}
$this->components[$name] ??= [
'version' => -1,
'entrypoint' => [],
];
$this->components[$name]['entrypoint'][$this->entrypoint] = 1;
$this->discoverComponents($component['template']);
}
public function enterNode(Node $node, Environment $env): Node
{
if ($node instanceof ComponentNode) {
$name = $node->getAttribute('component');
$this->addComponent($name);
}
if ($node instanceof FunctionExpression && $node->getAttribute('name') === 'component') {
$name = $node->getNode('arguments')->getNode(0)->getAttribute('value');
$this->addComponent($name);
}
if ($node instanceof IncludeNode) {
$this->discoverComponents($node->getNode('expr')->getAttribute('value'));
}
if ($node instanceof FunctionExpression && $node->getAttribute('name') === 'include') {
$this->discoverComponents($node->getNode('arguments')->getNode(0)->getAttribute('value'));
}
return $node;
}
public function leaveNode(Node $node, Environment $env): ?Node
{
return $node;
}
public function getPriority()
{
return 0;
}
}