Skip to content

Commit 5e016f0

Browse files
committed
WIP [FEATURE] Add iterator for TypoScript files
1 parent 37a388d commit 5e016f0

7 files changed

+318
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
use Helmich\TypoScriptParser\Tokenizer\Tokenizer;
5+
use Helmich\TypoScriptParser\Parser\Parser;
6+
7+
use Symfony\Component\DependencyInjection\ContainerBuilder;
8+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
9+
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
10+
11+
return static function (ContainerConfigurator $containerConfigurator, ContainerBuilder $containerBuilder): void {
12+
$services = $containerConfigurator->services();
13+
$services->defaults()
14+
->autowire()
15+
->autoconfigure();
16+
17+
$services->load('a9f\\FractorTypoScript\\', __DIR__ . '/../src/');
18+
19+
$services->set(Tokenizer::class);
20+
$services->set(Parser::class)
21+
->arg('$tokenizer', service(Tokenizer::class));
22+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorTypoScript\Contract;
6+
7+
use a9f\Fractor\Application\ValueObject\File;
8+
use a9f\FractorTypoScript\TypoScriptStatementsIterator;
9+
use Helmich\TypoScriptParser\Parser\AST\Statement;
10+
11+
/**
12+
* Interface for node visitors. Will be called for each node in the tree.
13+
*/
14+
interface TypoScriptNodeVisitor
15+
{
16+
public function beforeTraversal(File $file, array $statements): void;
17+
18+
/**
19+
* @return Statement|list<Statement>|TypoScriptStatementsIterator::*
20+
*/
21+
public function enterNode(Statement $node): Statement|array|int;
22+
23+
public function leaveNode(Statement $node): void;
24+
25+
public function afterTraversal(array $statements): void;
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorTypoScript;
6+
7+
use a9f\Fractor\Application\ValueObject\File;
8+
use a9f\FractorTypoScript\Contract\TypoScriptNodeVisitor;
9+
use Helmich\TypoScriptParser\Parser\AST\ConditionalStatement;
10+
use Helmich\TypoScriptParser\Parser\AST\NestedAssignment;
11+
use Helmich\TypoScriptParser\Parser\AST\Statement;
12+
use Webmozart\Assert\Assert;
13+
14+
final readonly class TypoScriptStatementsIterator
15+
{
16+
/**
17+
* @var int
18+
*/
19+
public const REMOVE_NODE = 3;
20+
21+
/**
22+
* @var array<TypoScriptNodeVisitor>
23+
*/
24+
private iterable $visitors;
25+
26+
/**
27+
* @param list<TypoScriptNodeVisitor> $visitors
28+
*/
29+
public function __construct(
30+
iterable $visitors
31+
) {
32+
$visitors = iterator_to_array($visitors);
33+
Assert::allIsInstanceOf($visitors, TypoScriptNodeVisitor::class);
34+
$this->visitors = $visitors;
35+
}
36+
37+
/**
38+
* @param list<Statement> $statements
39+
* @return list<Statement>
40+
*/
41+
public function traverseDocument(File $file, array $statements): array
42+
{
43+
foreach ($this->visitors as $visitor) {
44+
$visitor->beforeTraversal($file, $statements);
45+
}
46+
47+
$resultingStatements = $this->processStatementList($statements);
48+
49+
foreach ($this->visitors as $visitor) {
50+
$visitor->afterTraversal($statements);
51+
}
52+
53+
return $resultingStatements;
54+
}
55+
56+
/**
57+
* @param list<Statement> $statements
58+
* @return list<Statement>
59+
*/
60+
private function processStatementList(array $statements): array
61+
{
62+
$resultingStatements = [];
63+
foreach ($statements as $statement) {
64+
$result = $this->traverseNode($statement);
65+
66+
if (is_array($result)) {
67+
$resultingStatements[] = $result;
68+
} else if ($result instanceof Statement) {
69+
$resultingStatements[] = [$result];
70+
} elseif ($result === null) {
71+
$resultingStatements[] = [$statement];
72+
}
73+
}
74+
return array_merge(...$resultingStatements);
75+
}
76+
77+
/**
78+
* @param Statement $node
79+
* @return self::*|Statement|list<Statement>
80+
*/
81+
private function traverseNode(Statement $node): int|Statement|array
82+
{
83+
$lastCalledVisitor = null;
84+
foreach ($this->visitors as $visitor) {
85+
$result = $visitor->enterNode($node);
86+
87+
if (is_int($result)) {
88+
$lastCalledVisitor = $visitor;
89+
break;
90+
}
91+
}
92+
93+
if ($node instanceof ConditionalStatement) {
94+
$node->ifStatements = $this->processStatementList($node->ifStatements);
95+
$node->elseStatements = $this->processStatementList($node->elseStatements);
96+
} else if ($node instanceof NestedAssignment) {
97+
$node->statements = $this->processStatementList($node->statements);
98+
}
99+
100+
foreach ($this->visitors as $visitor) {
101+
if ($lastCalledVisitor === $visitor) {
102+
break;
103+
}
104+
$visitor->leaveNode($node);
105+
}
106+
return $result;
107+
}
108+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorTypoScript\Tests\Fixture;
6+
7+
use a9f\Fractor\Application\ValueObject\File;
8+
use a9f\FractorTypoScript\Contract\TypoScriptNodeVisitor;
9+
use Helmich\TypoScriptParser\Parser\AST\Statement;
10+
11+
final class StatementCollectingVisitor implements TypoScriptNodeVisitor {
12+
/**
13+
* @param list<non-empty-string> $calls
14+
*/
15+
public function __construct(
16+
private readonly string $visitorName,
17+
public array &$calls // only public to please PHPStan
18+
) {
19+
}
20+
21+
/**
22+
* @param list<Statement> $statements
23+
*/
24+
public function beforeTraversal(File $file, array $statements): void
25+
{
26+
$this->calls[] = sprintf('%s:beforeTraversal:%s', $this->visitorName, count($statements));
27+
}
28+
29+
public function enterNode(Statement $node): Statement|array|int
30+
{
31+
$this->calls[] = sprintf('%s:enterNode:%s:l-%d', $this->visitorName, $node::class, $node->sourceLine);
32+
return $node;
33+
}
34+
35+
public function leaveNode(Statement $node): void
36+
{
37+
$this->calls[] = sprintf('%s:leaveNode:%s:l-%d', $this->visitorName, $node::class, $node->sourceLine);
38+
}
39+
40+
/**
41+
* @param list<Statement> $statements
42+
*/
43+
public function afterTraversal(array $statements): void
44+
{
45+
$this->calls[] = sprintf('%s:afterTraversal:%s', $this->visitorName, count($statements));
46+
}
47+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace a9f\FractorTypoScript;
6+
7+
use a9f\Fractor\Application\ValueObject\File;
8+
use a9f\Fractor\DependencyInjection\ContainerContainerBuilder;
9+
use a9f\Fractor\Exception\ShouldNotHappenException;
10+
use a9f\FractorTypoScript\Tests\Fixture\StatementCollectingVisitor;
11+
use Helmich\TypoScriptParser\Parser\Parser;
12+
use PHPUnit\Framework\Attributes\Test;
13+
use PHPUnit\Framework\TestCase;
14+
use Psr\Container\ContainerInterface;
15+
16+
final class TypoScriptStatementsIteratorTest extends TestCase
17+
{
18+
private ?ContainerInterface $currentContainer = null;
19+
20+
protected function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
$this->currentContainer = (new ContainerContainerBuilder())
25+
->createDependencyInjectionContainer(__DIR__ . '/config/fractor.php', [
26+
__DIR__ . '/config/config.php',
27+
]);
28+
}
29+
30+
#[Test]
31+
public function visitorsAreCalledForAllStatements(): void
32+
{
33+
$parser = $this->getService(Parser::class);
34+
$nodes = $parser->parseString(<<<TS
35+
page = PAGE
36+
page.10 = TEXT
37+
page.10.value = Hello World!
38+
TS);
39+
40+
$calls = [];
41+
$subject = new TypoScriptStatementsIterator([new StatementCollectingVisitor('statements', $calls)]);
42+
$subject->traverseDocument(new File('', ''), $nodes);
43+
44+
self::assertSame([
45+
'statements:beforeTraversal:3',
46+
'statements:enterNode:Helmich\TypoScriptParser\Parser\AST\Operator\Assignment:l-1',
47+
'statements:leaveNode:Helmich\TypoScriptParser\Parser\AST\Operator\Assignment:l-1',
48+
'statements:enterNode:Helmich\TypoScriptParser\Parser\AST\Operator\ObjectCreation:l-2',
49+
'statements:leaveNode:Helmich\TypoScriptParser\Parser\AST\Operator\ObjectCreation:l-2',
50+
'statements:enterNode:Helmich\TypoScriptParser\Parser\AST\Operator\Assignment:l-3',
51+
'statements:leaveNode:Helmich\TypoScriptParser\Parser\AST\Operator\Assignment:l-3',
52+
'statements:afterTraversal:3',
53+
], $calls);
54+
}
55+
56+
#[Test]
57+
public function visitorsAreCalledForAllStatementsWithNesting(): void
58+
{
59+
$parser = $this->getService(Parser::class);
60+
$nodes = $parser->parseString(<<<TS
61+
page = PAGE
62+
page.10 = TEXT
63+
page.10 {
64+
value = Hello World!
65+
}
66+
TS);
67+
68+
$calls = [];
69+
$subject = new TypoScriptStatementsIterator([new StatementCollectingVisitor('statements', $calls)]);
70+
$subject->traverseDocument(new File('', ''), $nodes);
71+
72+
self::assertSame([
73+
'statements:beforeTraversal:3',
74+
'statements:enterNode:Helmich\TypoScriptParser\Parser\AST\Operator\Assignment:l-1',
75+
'statements:leaveNode:Helmich\TypoScriptParser\Parser\AST\Operator\Assignment:l-1',
76+
'statements:enterNode:Helmich\TypoScriptParser\Parser\AST\Operator\ObjectCreation:l-2',
77+
'statements:leaveNode:Helmich\TypoScriptParser\Parser\AST\Operator\ObjectCreation:l-2',
78+
'statements:enterNode:Helmich\TypoScriptParser\Parser\AST\NestedAssignment:l-3',
79+
'statements:enterNode:Helmich\TypoScriptParser\Parser\AST\Operator\Assignment:l-4',
80+
'statements:leaveNode:Helmich\TypoScriptParser\Parser\AST\Operator\Assignment:l-4',
81+
'statements:leaveNode:Helmich\TypoScriptParser\Parser\AST\NestedAssignment:l-3',
82+
'statements:afterTraversal:3',
83+
], $calls);
84+
}
85+
86+
/**
87+
* @template T of object
88+
* @phpstan-param class-string<T> $type
89+
* @phpstan-return T
90+
*/
91+
protected function getService(string $type): object
92+
{
93+
if ($this->currentContainer === null) {
94+
throw new ShouldNotHappenException('Container is not initalized');
95+
}
96+
97+
return $this->currentContainer->get($type)
98+
?? throw new ShouldNotHappenException(sprintf('Service "%s" was not found', $type));
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
return static function (\Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator $containerConfigurator, \Symfony\Component\DependencyInjection\ContainerBuilder $containerBuilder): void {
4+
$containerConfigurator->import(__DIR__ . '/../../config/application.php');
5+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use a9f\Fractor\Configuration\FractorConfiguration;
6+
use a9f\Typo3Fractor\Set\Typo3LevelSetList;
7+
8+
return FractorConfiguration::configure()
9+
->withPaths([__DIR__ . '/output/'])
10+
->withSets([Typo3LevelSetList::UP_TO_TYPO3_13]);

0 commit comments

Comments
 (0)