Skip to content

Commit cee7a2c

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

7 files changed

+300
-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,92 @@
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\Statement;
10+
use Webmozart\Assert\Assert;
11+
12+
final readonly class TypoScriptStatementsIterator
13+
{
14+
/**
15+
* @var int
16+
*/
17+
public const REMOVE_NODE = 3;
18+
19+
/**
20+
* @var array<TypoScriptNodeVisitor>
21+
*/
22+
private iterable $visitors;
23+
24+
/**
25+
* @param list<TypoScriptNodeVisitor> $visitors
26+
*/
27+
public function __construct(
28+
iterable $visitors
29+
) {
30+
$visitors = iterator_to_array($visitors);
31+
Assert::allIsInstanceOf($visitors, TypoScriptNodeVisitor::class);
32+
$this->visitors = $visitors;
33+
}
34+
35+
/**
36+
* @param list<Statement> $statements
37+
* @return list<Statement>
38+
*/
39+
public function traverseDocument(File $file, array $statements): array
40+
{
41+
foreach ($this->visitors as $visitor) {
42+
$visitor->beforeTraversal($file, $statements);
43+
}
44+
45+
$resultingStatements = [];
46+
foreach ($statements as $statement) {
47+
$result = $this->traverseNode($statement);
48+
49+
if (is_array($result)) {
50+
$resultingStatements[] = $result;
51+
} else if ($result instanceof Statement) {
52+
$resultingStatements[] = [$result];
53+
} elseif ($result === null) {
54+
$resultingStatements[] = [$statement];
55+
}
56+
}
57+
58+
foreach ($this->visitors as $visitor) {
59+
$visitor->afterTraversal($statements);
60+
}
61+
62+
return array_merge(...$resultingStatements);
63+
}
64+
65+
/**
66+
* @param Statement $node
67+
* @return self::*|Statement|list<Statement>
68+
*/
69+
private function traverseNode(Statement $node): int|Statement|array
70+
{
71+
$lastCalledVisitor = null;
72+
foreach ($this->visitors as $visitor) {
73+
$result = $visitor->enterNode($node);
74+
75+
if (is_int($result)) {
76+
$lastCalledVisitor = $visitor;
77+
break;
78+
}
79+
}
80+
81+
// TODO currently we don't have real tree, just a list of statements
82+
// => once we get a real AST, we should descend to children here
83+
84+
foreach ($this->visitors as $visitor) {
85+
if ($lastCalledVisitor === $visitor) {
86+
break;
87+
}
88+
$visitor->leaveNode($node);
89+
}
90+
return $result;
91+
}
92+
}
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,98 @@
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:leaveNode:Helmich\TypoScriptParser\Parser\AST\NestedAssignment:l-3',
80+
'statements:afterTraversal:3',
81+
], $calls);
82+
}
83+
84+
/**
85+
* @template T of object
86+
* @phpstan-param class-string<T> $type
87+
* @phpstan-return T
88+
*/
89+
protected function getService(string $type): object
90+
{
91+
if ($this->currentContainer === null) {
92+
throw new ShouldNotHappenException('Container is not initalized');
93+
}
94+
95+
return $this->currentContainer->get($type)
96+
?? throw new ShouldNotHappenException(sprintf('Service "%s" was not found', $type));
97+
}
98+
}
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)