Decisions, decisions! We have to make numerous decisions when planning projects. When penciling out Forte, one of the things I wanted to improve upon compared to earlier projects was the handling of more complex directive structures, such as the switch
statement.
If we look at a simple paired directive:
1@production
2 // Production specific content...
3@endproduction
It's not hard to figure out how to pair those up in a naive way. A little more fiddling to do it in a generalized way, but what about something like this:
1@switch($i)
2 @case(1)
3 First case...
4 @break
5
6 @case(2)
7 Second case...
8 @break
9
10 @default
11 Default case...
12@endswitch
In previous Blade parser projects, I just created specialized parser mechanics and node types to represent stuff like this. However, since I want Forte to be more flexible than all previous projects, that wasn't going to cut it.
For something like the switch directive, a built-in language feature, it isn't necessarily the worst thing to have built-in parsing features for this. However, what if someone wanted to parse something similar? And I really don't want to have an excessive amount of node types.
#Getting a Sense of What We're Dealing With
The following is the AST produced by the parser when we're not doing anything special with the switch statement:
Just a bunch of directives entwined within text nodes. To be fair and realistic, that could work for many use cases. But we want to do better.
#Parser Extensions
As I mentioned earlier, I want to minimize the special handling of Blade features within the Forte as much as possible: the more generic it is, the better it will be for everyone. To help achieve this goal, the Forte parser now supports parser extensions.
A parser extension implements the following interface:
1<?php
2
3namespace Forte\Contracts\Parser;
4
5use Forte\Lexer\Token;
6use Forte\Parser\Parser;
7
8interface Extension
9{
10 public function canParse(Token $token, Parser $parser): bool;
11
12 public function parse(Parser $parser);
13}
The canParse
method is called on all extensions as the first step in the main parser loop. This was strategic, as it will allow for custom extensions to override default parser logic.
When the canParse
method returns true
, the parse
method is invoked, and the Forte parser steps back to let the extension take over. Once the extension is finished, the Forte parser continues to parse the rest of the template, if needed.
#The Switch Extension
To dog-food extensions, more complex features like the switch statement are implemented as core extensions. This is the current implementation for the switch extension:
1<?php
2
3namespace Forte\Parser\Extensions\Core;
4
5use Forte\Contracts\Parser\Extension;
6use Forte\Lexer\Token;
7use Forte\Nodes\BlockDirectiveNode;
8use Forte\Parser\Parser;
9
10class SwitchDirectiveExtension implements Extension
11{
12 private static array $keywords = ['case', 'default', 'endswitch'];
13
14 public function canParse(Token $token, Parser $parser): bool
15 {
16 return $token->isDirective('switch');
17 }
18
19 public function parse(Parser $parser)
20 {
21 $switchDirective = $parser->parseDirective();
22 $switch = new BlockDirectiveNode;
23
24 $switch
25 ->setStartDirective($switchDirective)
26 ->addChildren($parser->collectNodesUntilDirectives(static::$keywords));
27
28 while ($parser->currentTokenIsDirective('case')) {
29 $switch->addChild(
30 $parser->parseDirective()
31 ->addChildren($parser->collectNodesUntilDirectives(static::$keywords))
32 );
33 }
34
35 if ($parser->currentTokenIsDirective('default')) {
36 $switch->addChild(
37 $parser->parseDirective()
38 ->addChildren($parser->collectNodesUntilDirective('endswitch'))
39 );
40 }
41
42 $endSwitch = $parser->parseDirective();
43 $switch->setEndDirective($endSwitch);
44
45 $switch->startOffset = $switch->getStartDirective()->startOffset;
46 $switch->endOffset = $switch->getEndDirective()->endOffset;
47 $switch->documentContent = $parser->getSourceContent($switch->startOffset, $switch->endOffset); 48
49 return $switch;
50 }
51}
Compared to previous projects, that's a minimal amount of code for what it accomplishes! Additionally, it doesn't introduce any new specialized node types, and any other extension can use the same methods it does: nothing hiding away in the parser's private API.
With the switch extension enabled, the parser now produces the following output:
Much better! And because Forte is a recursive parser and takes care of a bunch of complexity behind the scenes, our extension will work just fine for input like this:
1@switch($i)
2 @case(1)
3
4 @switch($i)
5 @case(1)
6 First case...
7 @break
8
9 @case(2)
10 Second case...
11 @break
12
13 @default
14 Default case...
15 @endswitch
16
17 @break
18
19 @case(2)
20 Second case...
21 @break
22
23 @default
24 Default case...
25@endswitch
∎