Nette Documentation Preview

syntax
Δημιουργία μιας επέκτασης
*************************

.[perex]
Μια επέκταση είναι μια επαναχρησιμοποιήσιμη κλάση που μπορεί να ορίσει προσαρμοσμένες ετικέτες, φίλτρα, λειτουργίες, παρόχους κ.λπ.

Δημιουργούμε επεκτάσεις όταν θέλουμε να επαναχρησιμοποιήσουμε τις προσαρμογές μας στο Latte σε διαφορετικά έργα ή να τις μοιραστούμε με άλλους.
Είναι επίσης χρήσιμο να δημιουργήσετε μια επέκταση για κάθε έργο ιστού που θα περιέχει όλες τις συγκεκριμένες ετικέτες και φίλτρα που θέλετε να χρησιμοποιήσετε στα πρότυπα του έργου.


Κλάση επέκτασης .[#toc-extension-class]
=======================================

Η Extension είναι μια κλάση που κληρονομεί από την [api:Latte\Extension]. Καταχωρείται στο Latte χρησιμοποιώντας την `addExtension()` (ή μέσω του [αρχείου ρυθμίσεων |application:configuration#Latte]):

```php
$latte = new Latte\Engine;
$latte->addExtension(new MyLatteExtension);
```

Εάν καταχωρήσετε πολλαπλές επεκτάσεις και ορίζουν ετικέτες, φίλτρα ή λειτουργίες με το ίδιο όνομα, κερδίζει η επέκταση που προστέθηκε τελευταία. Αυτό σημαίνει επίσης ότι οι επεκτάσεις σας μπορούν να παρακάμπτουν τις εγγενείς ετικέτες/φίλτρα/λειτουργίες.

Κάθε φορά που κάνετε μια αλλαγή σε μια κλάση και η αυτόματη ανανέωση δεν είναι απενεργοποιημένη, το Latte θα μεταγλωττίζει αυτόματα ξανά τα πρότυπα σας.

Μια κλάση μπορεί να υλοποιεί οποιαδήποτε από τις ακόλουθες μεθόδους:

```php
abstract class Extension
{
	/**
	 * Initializes before template is compiler.
	 */
	public function beforeCompile(Engine $engine): void;

	/**
	 * Returns a list of parsers for Latte tags.
	 * @return array<string, callable>
	 */
	public function getTags(): array;

	/**
	 * Returns a list of compiler passes.
	 * @return array<string, callable>
	 */
	public function getPasses(): array;

	/**
	 * Returns a list of |filters.
	 * @return array<string, callable>
	 */
	public function getFilters(): array;

	/**
	 * Returns a list of functions used in templates.
	 * @return array<string, callable>
	 */
	public function getFunctions(): array;

	/**
	 * Returns a list of providers.
	 * @return array<mixed>
	 */
	public function getProviders(): array;

	/**
	 * Returns a value to distinguish multiple versions of the template.
	 */
	public function getCacheKey(Engine $engine): mixed;

	/**
	 * Initializes before template is rendered.
	 */
	public function beforeRender(Template $template): void;
}
```

Για μια ιδέα του πώς μοιάζει η επέκταση, ρίξτε μια ματιά στην ενσωματωμένη "CoreExtension:https://github.com/nette/latte/blob/master/src/Latte/Essential/CoreExtension.php".


beforeCompile(Latte\Engine $engine): void .[method]
---------------------------------------------------

Καλείται πριν από τη μεταγλώττιση του προτύπου. Η μέθοδος μπορεί να χρησιμοποιηθεί για αρχικοποιήσεις που σχετίζονται με τη μεταγλώττιση, για παράδειγμα.


getTags(): array .[method]
--------------------------

Καλείται κατά τη μεταγλώττιση του προτύπου. Επιστρέφει έναν συσχετιστικό πίνακα *όνομα ετικέτας => callable*, οι οποίες είναι [συναρτήσεις ανάλυσης ετικέτας |#Tag Parsing Function].

```php
public function getTags(): array
{
	return [
		'foo' => [FooNode::class, 'create'],
		'bar' => [BarNode::class, 'create'],
		'n:baz' => [NBazNode::class, 'create'],
		// ...
	];
}
```

Η ετικέτα `n:baz` αντιπροσωπεύει ένα καθαρό n:attribute, δηλαδή είναι μια ετικέτα που μπορεί να γραφτεί μόνο ως attribute.

Στην περίπτωση των ετικετών `foo` και `bar`, το Latte θα αναγνωρίσει αυτόματα αν πρόκειται για ζεύγη, και αν ναι, μπορούν να γραφτούν αυτόματα με τη χρήση n:attributes, συμπεριλαμβανομένων των παραλλαγών με τα προθέματα `n:inner-foo` και `n:tag-foo`.

Η σειρά εκτέλεσης αυτών των n:attributes καθορίζεται από τη σειρά τους στον πίνακα που επιστρέφει η `getTags()`. Έτσι, το `n:foo` εκτελείται πάντα πριν από το `n:bar`, ακόμη και αν τα χαρακτηριστικά παρατίθενται με αντίστροφη σειρά στην ετικέτα HTML ως `<div n:bar="..." n:foo="...">`.

Εάν πρέπει να καθορίσετε τη σειρά των n:attributes σε πολλαπλές επεκτάσεις, χρησιμοποιήστε τη βοηθητική μέθοδο `order()`, όπου η παράμετρος `before` xor `after` καθορίζει ποιες ετικέτες διατάσσονται πριν ή μετά την ετικέτα.

```php
public function getTags(): array
{
	return [
		'foo' => self::order([FooNode::class, 'create'], before: 'bar')]
		'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])]
	];
}
```


getPasses(): array .[method]
----------------------------

Καλείται κατά τη μεταγλώττιση του προτύπου. Επιστρέφει έναν συσχετιστικό πίνακα *name pass => callable*, οι οποίες είναι συναρτήσεις που αντιπροσωπεύουν τα λεγόμενα [περάσματα του μεταγλωττιστή |#compiler passes] που διασχίζουν και τροποποιούν το AST.

Και πάλι μπορεί να χρησιμοποιηθεί η βοηθητική μέθοδος `order()`. Η τιμή των παραμέτρων `before` ή `after` μπορεί να είναι `*` με την έννοια πριν/μετά από όλα.

```php
public function getPasses(): array
{
	return [
		'optimize' => [Passes::class, 'optimizePass'],
		'sandbox' => self::order([$this, 'sandboxPass'], before: '*'),
		// ...
	];
}
```


beforeRender(Latte\Engine $engine): void .[method]
--------------------------------------------------

Καλείται πριν από κάθε απόδοση προτύπου. Η μέθοδος μπορεί να χρησιμοποιηθεί, για παράδειγμα, για την αρχικοποίηση μεταβλητών που χρησιμοποιούνται κατά την απόδοση.


getFilters(): array .[method]
-----------------------------

Καλείται πριν από την απόδοση του προτύπου. Επιστρέφει [τα φίλτρα |extending-latte#filters] ως συσχετιστικό πίνακα *όνομα φίλτρου => callable*.

```php
public function getFilters(): array
{
	return [
		'batch' => [$this, 'batchFilter'],
		'trim' => [$this, 'trimFilter'],
		// ...
	];
}
```


getFunctions(): array .[method]
-------------------------------

Καλείται πριν από την απόδοση του προτύπου. Επιστρέφει τις [συναρτήσεις |extending-latte#functions] ως συσχετιστικό πίνακα *όνομα συνάρτησης => callable*.

```php
public function getFunctions(): array
{
	return [
		'clamp' => [$this, 'clampFunction'],
		'divisibleBy' => [$this, 'divisibleByFunction'],
		// ...
	];
}
```


getProviders(): array .[method]
-------------------------------

Καλείται πριν από την απόδοση του προτύπου. Επιστρέφει έναν πίνακα παρόχων, οι οποίοι είναι συνήθως αντικείμενα που χρησιμοποιούν ετικέτες κατά την εκτέλεση. Η πρόσβαση σε αυτούς γίνεται μέσω του `$this->global->...`.

```php
public function getProviders(): array
{
	return [
		'myFoo' => $this->foo,
		'myBar' => $this->bar,
		// ...
	];
}
```


getCacheKey(Latte\Engine $engine): mixed .[method]
--------------------------------------------------

Καλείται πριν από την απόδοση του προτύπου. Η τιμή επιστροφής γίνεται μέρος του κλειδιού του οποίου ο κατακερματισμός περιέχεται στο όνομα του μεταγλωττισμένου αρχείου προτύπου. Έτσι, για διαφορετικές τιμές επιστροφής, η Latte θα δημιουργήσει διαφορετικά αρχεία κρυφής μνήμης.


Πώς λειτουργεί η Latte; .[#toc-how-does-latte-work]
===================================================

Για να καταλάβετε πώς να ορίσετε προσαρμοσμένες ετικέτες ή περάσματα μεταγλωττιστή, είναι απαραίτητο να καταλάβετε πώς λειτουργεί το Latte κάτω από την κουκούλα.

Η μεταγλώττιση προτύπων στο Latte απλουστευτικά λειτουργεί ως εξής:

- Πρώτον, ο **lexer** μετατρέπει τον πηγαίο κώδικα του προτύπου σε μικρά κομμάτια (tokens) για ευκολότερη επεξεργασία.
- Στη συνέχεια, ο **διαχωριστής** μετατρέπει τη ροή των tokens σε ένα ουσιαστικό δέντρο κόμβων (το Αφηρημένο Συντακτικό Δέντρο, AST).
- Τέλος, ο μεταγλωττιστής **δημιουργεί** μια κλάση PHP από το AST που αποδίδει το πρότυπο και το αποθηκεύει.

Στην πραγματικότητα, η μεταγλώττιση είναι λίγο πιο περίπλοκη. Το Latte **έχει δύο** λεξικογράφους και αναλυτές: έναν για το πρότυπο HTML και έναν για τον κώδικα που μοιάζει με PHP μέσα στις ετικέτες. Επίσης, η ανάλυση δεν εκτελείται μετά τη tokenization, αλλά ο lexer και ο parser τρέχουν παράλληλα σε δύο "νήματα" και συντονίζονται. Είναι επιστήμη πυραύλων :-)

Επιπλέον, όλες οι ετικέτες έχουν τις δικές τους ρουτίνες ανάλυσης. Όταν ο αναλυτής συναντά μια ετικέτα, καλεί τη συνάρτηση ανάλυσης της (επιστρέφει την [Extension::getTags() |#getTags]).
Η δουλειά τους είναι να αναλύσουν τα ορίσματα της ετικέτας και, στην περίπτωση των ζευγαρωμένων ετικετών, το εσωτερικό περιεχόμενο. Επιστρέφει έναν *κόμβο* που γίνεται μέρος του AST. Ανατρέξτε στη [λειτουργία ανάλυσης ετικετών |#Tag parsing function] για λεπτομέρειες.

Όταν ο αναλυτής ολοκληρώσει τη δουλειά του, έχουμε ένα πλήρες AST που αναπαριστά το πρότυπο. Ο κόμβος ρίζα είναι το `Latte\Compiler\Nodes\TemplateNode`. Στη συνέχεια, οι επιμέρους κόμβοι μέσα στο δέντρο αντιπροσωπεύουν όχι μόνο τις ετικέτες, αλλά και τα στοιχεία HTML, τα χαρακτηριστικά τους, τυχόν εκφράσεις που χρησιμοποιούνται μέσα στις ετικέτες κ.λπ.

Στη συνέχεια, μπαίνουν στο παιχνίδι τα λεγόμενα [περάσματα του μεταγλωττιστή |#Compiler passes], τα οποία είναι συναρτήσεις (που επιστρέφονται από την [Extension::getPasses() |#getPasses]) που τροποποιούν το AST.

Όλη η διαδικασία, από τη φόρτωση του περιεχομένου του προτύπου, μέσω της ανάλυσης, μέχρι τη δημιουργία του αρχείου που προκύπτει, μπορεί να ακολουθηθεί με αυτόν τον κώδικα, με τον οποίο μπορείτε να πειραματιστείτε και να απορρίψετε τα ενδιάμεσα αποτελέσματα:

```php
$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);
```


Παράδειγμα AST .[#toc-example-of-ast]
-------------------------------------

Για να έχουμε μια καλύτερη ιδέα του AST, προσθέτουμε ένα δείγμα. Αυτό είναι το πρότυπο πηγής:

```latte
{foreach $category->getItems() as $item}
	<li>{$item->name|upper}</li>
	{else}
	no items found
{/foreach}
```

Και αυτή είναι η αναπαράστασή του με τη μορφή AST:

/--pre
Latte\Compiler\Nodes\<b>TemplateNode</b>(
   Latte\Compiler\Nodes\<b>FragmentNode</b>(
      - Latte\Essential\Nodes\<b>ForeachNode</b>(
           expression: Latte\Compiler\Nodes\Php\Expression\<b>MethodCallNode</b>(
              object: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$category')
              name: Latte\Compiler\Nodes\Php\<b>IdentifierNode</b>('getItems')
           )
           value: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$item')
           content: Latte\Compiler\Nodes\<b>FragmentNode</b>(
              - Latte\Compiler\Nodes\<b>TextNode</b>('  ')
              - Latte\Compiler\Nodes\<b>Html\ElementNode</b>('li')(
                   content: Latte\Essential\Nodes\<b>PrintNode</b>(
                      expression: Latte\Compiler\Nodes\Php\Expression\<b>PropertyFetchNode</b>(
                         object: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$item')
                         name: Latte\Compiler\Nodes\Php\<b>IdentifierNode</b>('name')
                      )
                      modifier: Latte\Compiler\Nodes\Php\<b>ModifierNode</b>(
                         filters:
                            - Latte\Compiler\Nodes\Php\<b>FilterNode</b>('upper')
                      )
                   )
                )
            )
            else: Latte\Compiler\Nodes\<b>FragmentNode</b>(
               - Latte\Compiler\Nodes\<b>TextNode</b>('no items found')
            )
        )
   )
)
\--


Προσαρμοσμένες ετικέτες .[#toc-custom-tags]
===========================================

Τρία βήματα απαιτούνται για τον ορισμό μιας νέας ετικέτας:

- ορισμός [συνάρτησης ανάλυσης ετικέτας |#tag parsing function] (υπεύθυνη για την ανάλυση της ετικέτας σε κόμβο)
- δημιουργία μιας κλάσης κόμβου (υπεύθυνη για [τη δημιουργία κώδικα PHP |#generating PHP code] και τη [διάσχιση AST |#AST traversing])
- καταχώριση της ετικέτας με τη χρήση της [Extension::getTags() |#getTags]


Λειτουργία ανάλυσης ετικέτας .[#toc-tag-parsing-function]
---------------------------------------------------------

Η ανάλυση των ετικετών γίνεται από τη συνάρτηση ανάλυσης (αυτή που επιστρέφεται από την [Extension::getTags() |#getTags]). Η δουλειά της είναι να αναλύει και να ελέγχει τυχόν ορίσματα μέσα στην ετικέτα (χρησιμοποιεί τον TagParser για να το κάνει αυτό).
Επιπλέον, αν η ετικέτα είναι ένα ζεύγος, θα ζητήσει από τον TemplateParser να αναλύσει και να επιστρέψει το εσωτερικό περιεχόμενο.
Η συνάρτηση δημιουργεί και επιστρέφει έναν κόμβο, ο οποίος είναι συνήθως παιδί του `Latte\Compiler\Nodes\StatementNode`, και αυτός γίνεται μέρος του AST.

Δημιουργούμε μια κλάση για κάθε κόμβο, κάτι που θα κάνουμε τώρα, και τοποθετούμε κομψά τη συνάρτηση ανάλυσης σε αυτήν ως στατικό εργοστάσιο. Ως παράδειγμα, ας δοκιμάσουμε να δημιουργήσουμε τη γνωστή ετικέτα `{foreach}`:

```php
use Latte\Compiler\Nodes\StatementNode;

class ForeachNode extends StatementNode
{
	// μια συνάρτηση ανάλυσης που απλά δημιουργεί έναν κόμβο προς το παρόν
	public static function create(Latte\Compiler\Tag $tag): self
	{
		$node = $tag->node = new self;
		return $node;
	}

	public function print(Latte\Compiler\PrintContext $context): string
	{
		// ο κώδικας θα προστεθεί αργότερα
	}

	public function &getIterator(): \Generator
	{
		// ο κώδικας θα προστεθεί αργότερα
	}
}
```

Στη συνάρτηση ανάλυσης `create()` περνάει ένα αντικείμενο [api:Latte\Compiler\Tag], το οποίο μεταφέρει βασικές πληροφορίες για την ετικέτα (αν είναι κλασική ετικέτα ή n:attribute, σε ποια γραμμή βρίσκεται, κ.λπ.) και κυρίως προσπελαύνει το [api:Latte\Compiler\TagParser] στο `$tag->parser`.

Εάν η ετικέτα πρέπει να έχει ορίσματα, ελέγξτε την ύπαρξή τους καλώντας το `$tag->expectArguments()`. Οι μέθοδοι του αντικειμένου `$tag->parser` είναι διαθέσιμες για την ανάλυσή τους:

- `parseExpression(): ExpressionNode` για μια έκφραση τύπου PHP (π.χ. `10 + 3`)
- `parseUnquotedStringOrExpression(): ExpressionNode` για μια έκφραση ή μια συμβολοσειρά χωρίς εισαγωγικά
- `parseArguments(): ArrayNode` περιεχόμενο του πίνακα (π.χ. `10, true, foo => bar`)
- `parseModifier(): ModifierNode` για έναν τροποποιητή (π.χ. `|upper|truncate:10`)
- `parseType(): expressionNode` για typehint (π.χ. `int|string` ή `Foo\Bar[]`)

και ένα χαμηλού επιπέδου [api:Latte\Compiler\TokenStream] που λειτουργεί απευθείας με tokens:

- `$tag->parser->stream->consume(...): Token`
- `$tag->parser->stream->tryConsume(...): ?Token`

Το Latte επεκτείνει τη σύνταξη της PHP με μικρούς τρόπους, για παράδειγμα προσθέτοντας τροποποιητές, συντομευμένους τριμερείς τελεστές ή επιτρέποντας την εγγραφή απλών αλφαριθμητικών συμβολοσειρών χωρίς εισαγωγικά. Αυτός είναι ο λόγος για τον οποίο χρησιμοποιούμε τον όρο *PHP-like* αντί για PHP. Έτσι, η μέθοδος `parseExpression()` αναλύει το `foo` ως `'foo'`, για παράδειγμα.
Επιπλέον, η *unquoted-string* είναι μια ειδική περίπτωση συμβολοσειράς που επίσης δεν χρειάζεται να είναι σε εισαγωγικά, αλλά ταυτόχρονα δεν χρειάζεται να είναι αλφαριθμητική. Για παράδειγμα, είναι η διαδρομή προς ένα αρχείο στην ετικέτα `{include ../file.latte}`. Η μέθοδος `parseUnquotedStringOrExpression()` χρησιμοποιείται για την ανάλυσή του.

.[note]
Η μελέτη των κλάσεων κόμβων που αποτελούν μέρος του Latte είναι ο καλύτερος τρόπος για να μάθετε όλες τις μικρές λεπτομέρειες της διαδικασίας ανάλυσης.

Ας επιστρέψουμε στην ετικέτα `{foreach}`. Σε αυτήν, περιμένουμε ορίσματα της μορφής `expression + 'as' + second expression`, τα οποία αναλύουμε ως εξής:

```php
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\AreaNode;

class ForeachNode extends StatementNode
{
	public ExpressionNode $expression;
	public ExpressionNode $value;

	public static function create(Latte\Compiler\Tag $tag): self
	{
		$tag->expectArguments();
		$node = $tag->node = new self;
		$node->expression = $tag->parser->parseExpression();
		$tag->parser->stream->consume('as');
		$node->value = $parser->parseExpression();
		return $node;
	}
}
```

Οι εκφράσεις που έχουμε γράψει στις μεταβλητές `$expression` και `$value` αντιπροσωπεύουν υποκόμβους.

.[tip]
Ορίστε τις μεταβλητές με υποκόμβους ως **δημόσιες**, ώστε να μπορούν να τροποποιηθούν σε [περαιτέρω βήματα επεξεργασίας |#Compiler Passes], αν χρειαστεί. Είναι επίσης απαραίτητο να **διαθέσουμε** τις μεταβλητές αυτές για [διάσχιση |#AST Traversing].

Για ζευγαρωτές ετικέτες, όπως η δική μας, η μέθοδος πρέπει επίσης να επιτρέπει στον TemplateParser να αναλύει τα εσωτερικά περιεχόμενα της ετικέτας. Αυτό αντιμετωπίζεται από το `yield`, το οποίο επιστρέφει ένα ζεύγος ''[εσωτερικό περιεχόμενο, τέλος ετικέτας]''. Αποθηκεύουμε το εσωτερικό περιεχόμενο στη μεταβλητή `$node->content`.

```php
public AreaNode $content;

public static function create(Latte\Compiler\Tag $tag): \Generator
{
	// ...
	[$node->content, $endTag] = yield;
	return $node;
}
```

Η λέξη-κλειδί `yield` προκαλεί τον τερματισμό της μεθόδου `create()`, επιστρέφοντας τον έλεγχο πίσω στον TemplateParser, ο οποίος συνεχίζει την ανάλυση του περιεχομένου μέχρι να συναντήσει την ετικέτα end. Στη συνέχεια περνάει τον έλεγχο πίσω στο `create()`, το οποίο συνεχίζει από εκεί που σταμάτησε. Η χρήση της μεθόδου `yield`, επιστρέφει αυτόματα τη μέθοδο `Generator`.

Μπορείτε επίσης να περάσετε έναν πίνακα ονομάτων ετικετών στο `yield` για τις οποίες θέλετε να σταματήσετε την ανάλυση, αν εμφανιστούν πριν από την ετικέτα τέλους. Αυτό μας βοηθάει να υλοποιήσουμε την `{foreach}...{else}...{/foreach}` construct. Εάν εμφανιστεί το `{else}`, αναλύουμε το περιεχόμενο μετά από αυτό στο `$node->elseContent`:

```php
public AreaNode $content;
public ?AreaNode $elseContent = null;

public static function create(Latte\Compiler\Tag $tag): \Generator
{
	// ...
	[$node->content, $nextTag] = yield ['else'];
	if ($nextTag?->name === 'else') {
		[$node->elseContent] = yield;
	}

	return $node;
}
```

Με την επιστροφή του κόμβου ολοκληρώνεται η ανάλυση της ετικέτας.


Δημιουργία κώδικα PHP .[#toc-generating-php-code]
-------------------------------------------------

Κάθε κόμβος πρέπει να υλοποιεί τη μέθοδο `print()`. Επιστρέφει κώδικα PHP που αποδίδει το συγκεκριμένο τμήμα του προτύπου (κώδικας εκτέλεσης). Παραδίδεται ένα αντικείμενο [api:Latte\Compiler\PrintContext] ως παράμετρος, το οποίο διαθέτει μια χρήσιμη μέθοδο `format()` που απλοποιεί τη συναρμολόγηση του κώδικα που προκύπτει.

Η μέθοδος `format(string $mask, ...$args)` δέχεται τα ακόλουθα placeholders στη μάσκα:
- `%node` εκτυπώνει τον κόμβο
- `%dump` εξάγει την τιμή στην PHP
- `%raw` εισάγει το κείμενο απευθείας χωρίς μετασχηματισμό
- `%args` εκτυπώνει ArrayNode ως ορίσματα στην κλήση της συνάρτησης
- `%line` εκτυπώνει ένα σχόλιο με αριθμό γραμμής
- `%escape(...)` διαφυγή του περιεχομένου
- `%modify(...)` εφαρμόζει τροποποιητή
- `%modifyContent(...)` εφαρμόζει τροποποιητή σε μπλοκ


Η συνάρτηση `print()` θα μπορούσε να μοιάζει ως εξής (παραλείπουμε τον κλάδο `else` για λόγους απλότητας):

```php
public function print(Latte\Compiler\PrintContext $context): string
{
	return $context->format(
		<<<'XX'
			foreach (%node as %node) %line {
				%node
			}

			XX,
		$this->expression,
		$this->value,
		$this->position,
		$this->content,
	);
}
```

Η μεταβλητή `$this->position` έχει ήδη οριστεί από την κλάση [api:Latte\Compiler\Node] και ορίζεται από τον αναλυτή. Περιέχει ένα αντικείμενο [api:Latte\Compiler\Position] με τη θέση της ετικέτας στον πηγαίο κώδικα με τη μορφή αριθμού γραμμής και στήλης.

Ο κώδικας εκτέλεσης μπορεί να χρησιμοποιεί βοηθητικές μεταβλητές. Για να αποφευχθεί η σύγκρουση με τις μεταβλητές που χρησιμοποιούνται από το ίδιο το πρότυπο, είναι σύμβαση να τους προτάσσεται με χαρακτήρες `$ʟ__`.

Μπορεί επίσης να χρησιμοποιεί αυθαίρετες τιμές κατά την εκτέλεση, οι οποίες περνούν στο πρότυπο με τη μορφή παρόχων χρησιμοποιώντας τη μέθοδο [Extension::getProviders() |#getProviders]. Η πρόσβαση σε αυτές γίνεται με τη χρήση του `$this->global->...`.


Διασχίζοντας το AST .[#toc-ast-traversing]
------------------------------------------

Για να διασχίσουμε το δέντρο AST σε βάθος, είναι απαραίτητο να υλοποιήσουμε τη μέθοδο `getIterator()`. Αυτό θα παρέχει πρόσβαση σε υποκόμβους:

```php
public function &getIterator(): \Generator
{
	yield $this->expression;
	yield $this->value;
	yield $this->content;
	if ($this->elseContent) {
		yield $this->elseContent;
	}
}
```

Σημειώστε ότι η `getIterator()` επιστρέφει μια αναφορά. Αυτό είναι που επιτρέπει στους επισκέπτες των κόμβων να αντικαταστήσουν μεμονωμένους κόμβους με άλλους κόμβους.

.[warning]
Εάν ένας κόμβος έχει υποκόμβους, είναι απαραίτητο να υλοποιήσετε αυτή τη μέθοδο και να καταστήσετε όλους τους υποκόμβους διαθέσιμους. Διαφορετικά, θα μπορούσε να δημιουργηθεί ένα κενό ασφαλείας. Για παράδειγμα, η λειτουργία sandbox δεν θα μπορούσε να ελέγξει τους υποκόμβους και να διασφαλίσει ότι δεν θα καλούνται σε αυτούς μη επιτρεπόμενες κατασκευές.

Δεδομένου ότι η λέξη-κλειδί `yield` πρέπει να υπάρχει στο σώμα της μεθόδου, ακόμη και αν δεν έχει κόμβους-παιδιά, γράψτε την ως εξής:

```php
public function &getIterator(): \Generator
{
	if (false) {
		yield;
	}
}
```


AuxiliaryNode
-------------

Εάν δημιουργείτε μια νέα ετικέτα για το Latte, είναι σκόπιμο να δημιουργήσετε μια ειδική κλάση κόμβου για αυτήν, η οποία θα την αντιπροσωπεύει στο δέντρο AST (βλέπε την κλάση `ForeachNode` στο παραπάνω παράδειγμα). Σε ορισμένες περιπτώσεις, μπορεί να βρείτε χρήσιμη την τετριμμένη βοηθητική κλάση κόμβου [AuxiliaryNode |api:Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode], η οποία σας επιτρέπει να περάσετε το σώμα της μεθόδου `print()` και τη λίστα των κόμβων που γίνονται προσβάσιμοι από τη μέθοδο `getIterator()` ως παραμέτρους του κατασκευαστή:

```php
// Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
// or Latte\Compiler\Nodes\AuxiliaryNode

$node = new AuxiliaryNode(
	// body of the print() method:
	fn(PrintContext $context, $argNode) => $context->format('myFunc(%node)', $argNode),
	// nodes accessed via getIterator() and also passed into the print() method:
	[$argNode],
);
```


Μεταγλωττιστής περνάει .[#toc-compiler-passes]
==============================================

Τα περάσματα μεταγλωττιστή είναι συναρτήσεις που τροποποιούν τα AST ή συλλέγουν πληροφορίες σε αυτά. Επιστρέφονται από τη μέθοδο [Extension::getPasses() |#getPasses].


Ανιχνευτής κόμβων .[#toc-node-traverser]
----------------------------------------

Ο πιο συνηθισμένος τρόπος για να εργαστούμε με το AST είναι με τη χρήση ενός [api:Latte\Compiler\NodeTraverser]:

```php
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: fn(Node $node) => ...,
	leave: fn(Node $node) => ...,
);
```

Η συνάρτηση *enter* (δηλαδή ο επισκέπτης) καλείται όταν συναντάται για πρώτη φορά ένας κόμβος, πριν από την επεξεργασία των υποκόμβων του. Η συνάρτηση *leave* καλείται μετά την επίσκεψη όλων των υποκόμβων.
Ένα κοινό μοτίβο είναι ότι η *enter* χρησιμοποιείται για να συλλέξει κάποιες πληροφορίες και στη συνέχεια η *leave* εκτελεί τροποποιήσεις με βάση αυτές. Τη στιγμή που καλείται η *leave*, όλος ο κώδικας μέσα στον κόμβο θα έχει ήδη επισκεφθεί και θα έχουν συλλεχθεί οι απαραίτητες πληροφορίες.

Πώς να τροποποιήσετε το AST; Ο ευκολότερος τρόπος είναι απλά να αλλάξετε τις ιδιότητες των κόμβων. Ο δεύτερος τρόπος είναι η πλήρης αντικατάσταση του κόμβου επιστρέφοντας έναν νέο κόμβο. Παράδειγμα: ο παρακάτω κώδικας θα αλλάξει όλους τους ακέραιους αριθμούς στο AST σε συμβολοσειρές (π.χ. το 42 θα αλλάξει σε `'42'`).

```php
use Latte\Compiler\Nodes\Php;

$ast = (new NodeTraverser)->traverse(
	$ast,
	leave: function (Node $node) {
		if ($node instanceof Php\Scalar\IntegerNode) {
            return new Php\Scalar\StringNode((string) $node->value);
        }
	},
);
```

Ένα AST μπορεί εύκολα να περιέχει χιλιάδες κόμβους και η διάσχιση όλων αυτών μπορεί να είναι αργή. Σε ορισμένες περιπτώσεις, είναι δυνατόν να αποφευχθεί η πλήρης διάσχιση.

Αν ψάχνετε για όλους τους κόμβους `Html\ElementNode` σε ένα δέντρο, ξέρετε ότι αφού έχετε δει το `Php\ExpressionNode`, δεν υπάρχει λόγος να ελέγξετε επίσης όλους τους κόμβους-παιδιά του, επειδή η HTML δεν μπορεί να είναι μέσα σε εκφράσεις. Σε αυτή την περίπτωση, μπορείτε να δώσετε εντολή στον περιηγητή να μην κάνει αναδρομή στον κόμβο κλάσης:

```php
$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: function (Node $node) {
		if ($node instanceof Php\ExpressionNode) {
			return NodeTraverser::DontTraverseChildren;
        }
        // ...
	},
);
```

Αν ψάχνετε μόνο για έναν συγκεκριμένο κόμβο, είναι επίσης δυνατό να διακόψετε εντελώς την αναστροφή αφού τον βρείτε.

```php
$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: function (Node $node) {
		if ($node instanceof Nodes\ParametersNode) {
			return NodeTraverser::StopTraversal;
        }
        // ...
	},
);
```


Βοηθοί κόμβων .[#toc-node-helpers]
----------------------------------

Η κλάση [api:Latte\Compiler\NodeHelpers] παρέχει ορισμένες μεθόδους που μπορούν να βρουν AST κόμβους που είτε ικανοποιούν ένα συγκεκριμένο callback κ.λπ. Παρουσιάζονται μερικά παραδείγματα:

```php
use Latte\Compiler\NodeHelpers;

// βρίσκει όλους τους κόμβους στοιχείων HTML
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);

// βρίσκει τον πρώτο κόμβο κειμένου
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);

// μετατρέπει τον κόμβο τιμών PHP σε πραγματική τιμή
$value = NodeHelpers::toValue($node);

// μετατρέπει στατικό κόμβο κειμένου σε συμβολοσειρά
$text = NodeHelpers::toText($node);
```

Δημιουργία μιας επέκτασης

Μια επέκταση είναι μια επαναχρησιμοποιήσιμη κλάση που μπορεί να ορίσει προσαρμοσμένες ετικέτες, φίλτρα, λειτουργίες, παρόχους κ.λπ.

Δημιουργούμε επεκτάσεις όταν θέλουμε να επαναχρησιμοποιήσουμε τις προσαρμογές μας στο Latte σε διαφορετικά έργα ή να τις μοιραστούμε με άλλους. Είναι επίσης χρήσιμο να δημιουργήσετε μια επέκταση για κάθε έργο ιστού που θα περιέχει όλες τις συγκεκριμένες ετικέτες και φίλτρα που θέλετε να χρησιμοποιήσετε στα πρότυπα του έργου.

Κλάση επέκτασης

Η Extension είναι μια κλάση που κληρονομεί από την Latte\Extension. Καταχωρείται στο Latte χρησιμοποιώντας την addExtension() (ή μέσω του αρχείου ρυθμίσεων):

$latte = new Latte\Engine;
$latte->addExtension(new MyLatteExtension);

Εάν καταχωρήσετε πολλαπλές επεκτάσεις και ορίζουν ετικέτες, φίλτρα ή λειτουργίες με το ίδιο όνομα, κερδίζει η επέκταση που προστέθηκε τελευταία. Αυτό σημαίνει επίσης ότι οι επεκτάσεις σας μπορούν να παρακάμπτουν τις εγγενείς ετικέτες/φίλτρα/λειτουργίες.

Κάθε φορά που κάνετε μια αλλαγή σε μια κλάση και η αυτόματη ανανέωση δεν είναι απενεργοποιημένη, το Latte θα μεταγλωττίζει αυτόματα ξανά τα πρότυπα σας.

Μια κλάση μπορεί να υλοποιεί οποιαδήποτε από τις ακόλουθες μεθόδους:

abstract class Extension
{
	/**
	 * Initializes before template is compiler.
	 */
	public function beforeCompile(Engine $engine): void;

	/**
	 * Returns a list of parsers for Latte tags.
	 * @return array<string, callable>
	 */
	public function getTags(): array;

	/**
	 * Returns a list of compiler passes.
	 * @return array<string, callable>
	 */
	public function getPasses(): array;

	/**
	 * Returns a list of |filters.
	 * @return array<string, callable>
	 */
	public function getFilters(): array;

	/**
	 * Returns a list of functions used in templates.
	 * @return array<string, callable>
	 */
	public function getFunctions(): array;

	/**
	 * Returns a list of providers.
	 * @return array<mixed>
	 */
	public function getProviders(): array;

	/**
	 * Returns a value to distinguish multiple versions of the template.
	 */
	public function getCacheKey(Engine $engine): mixed;

	/**
	 * Initializes before template is rendered.
	 */
	public function beforeRender(Template $template): void;
}

Για μια ιδέα του πώς μοιάζει η επέκταση, ρίξτε μια ματιά στην ενσωματωμένη „CoreExtension:https://github.com/…xtension.php“.

beforeCompile(Latte\Engine $engine)void

Καλείται πριν από τη μεταγλώττιση του προτύπου. Η μέθοδος μπορεί να χρησιμοποιηθεί για αρχικοποιήσεις που σχετίζονται με τη μεταγλώττιση, για παράδειγμα.

getTags(): array

Καλείται κατά τη μεταγλώττιση του προτύπου. Επιστρέφει έναν συσχετιστικό πίνακα όνομα ετικέτας ⇒ callable, οι οποίες είναι συναρτήσεις ανάλυσης ετικέτας.

public function getTags(): array
{
	return [
		'foo' => [FooNode::class, 'create'],
		'bar' => [BarNode::class, 'create'],
		'n:baz' => [NBazNode::class, 'create'],
		// ...
	];
}

Η ετικέτα n:baz αντιπροσωπεύει ένα καθαρό n:attribute, δηλαδή είναι μια ετικέτα που μπορεί να γραφτεί μόνο ως attribute.

Στην περίπτωση των ετικετών foo και bar, το Latte θα αναγνωρίσει αυτόματα αν πρόκειται για ζεύγη, και αν ναι, μπορούν να γραφτούν αυτόματα με τη χρήση n:attributes, συμπεριλαμβανομένων των παραλλαγών με τα προθέματα n:inner-foo και n:tag-foo.

Η σειρά εκτέλεσης αυτών των n:attributes καθορίζεται από τη σειρά τους στον πίνακα που επιστρέφει η getTags(). Έτσι, το n:foo εκτελείται πάντα πριν από το n:bar, ακόμη και αν τα χαρακτηριστικά παρατίθενται με αντίστροφη σειρά στην ετικέτα HTML ως <div n:bar="..." n:foo="...">.

Εάν πρέπει να καθορίσετε τη σειρά των n:attributes σε πολλαπλές επεκτάσεις, χρησιμοποιήστε τη βοηθητική μέθοδο order(), όπου η παράμετρος before xor after καθορίζει ποιες ετικέτες διατάσσονται πριν ή μετά την ετικέτα.

public function getTags(): array
{
	return [
		'foo' => self::order([FooNode::class, 'create'], before: 'bar')]
		'bar' => self::order([BarNode::class, 'create'], after: ['block', 'snippet'])]
	];
}

getPasses(): array

Καλείται κατά τη μεταγλώττιση του προτύπου. Επιστρέφει έναν συσχετιστικό πίνακα name pass ⇒ callable, οι οποίες είναι συναρτήσεις που αντιπροσωπεύουν τα λεγόμενα περάσματα του μεταγλωττιστή που διασχίζουν και τροποποιούν το AST.

Και πάλι μπορεί να χρησιμοποιηθεί η βοηθητική μέθοδος order(). Η τιμή των παραμέτρων before ή after μπορεί να είναι * με την έννοια πριν/μετά από όλα.

public function getPasses(): array
{
	return [
		'optimize' => [Passes::class, 'optimizePass'],
		'sandbox' => self::order([$this, 'sandboxPass'], before: '*'),
		// ...
	];
}

beforeRender(Latte\Engine $engine)void

Καλείται πριν από κάθε απόδοση προτύπου. Η μέθοδος μπορεί να χρησιμοποιηθεί, για παράδειγμα, για την αρχικοποίηση μεταβλητών που χρησιμοποιούνται κατά την απόδοση.

getFilters(): array

Καλείται πριν από την απόδοση του προτύπου. Επιστρέφει τα φίλτρα ως συσχετιστικό πίνακα όνομα φίλτρου ⇒ callable.

public function getFilters(): array
{
	return [
		'batch' => [$this, 'batchFilter'],
		'trim' => [$this, 'trimFilter'],
		// ...
	];
}

getFunctions(): array

Καλείται πριν από την απόδοση του προτύπου. Επιστρέφει τις συναρτήσεις ως συσχετιστικό πίνακα όνομα συνάρτησης ⇒ callable.

public function getFunctions(): array
{
	return [
		'clamp' => [$this, 'clampFunction'],
		'divisibleBy' => [$this, 'divisibleByFunction'],
		// ...
	];
}

getProviders(): array

Καλείται πριν από την απόδοση του προτύπου. Επιστρέφει έναν πίνακα παρόχων, οι οποίοι είναι συνήθως αντικείμενα που χρησιμοποιούν ετικέτες κατά την εκτέλεση. Η πρόσβαση σε αυτούς γίνεται μέσω του $this->global->....

public function getProviders(): array
{
	return [
		'myFoo' => $this->foo,
		'myBar' => $this->bar,
		// ...
	];
}

getCacheKey(Latte\Engine $engine)mixed

Καλείται πριν από την απόδοση του προτύπου. Η τιμή επιστροφής γίνεται μέρος του κλειδιού του οποίου ο κατακερματισμός περιέχεται στο όνομα του μεταγλωττισμένου αρχείου προτύπου. Έτσι, για διαφορετικές τιμές επιστροφής, η Latte θα δημιουργήσει διαφορετικά αρχεία κρυφής μνήμης.

Πώς λειτουργεί η Latte;

Για να καταλάβετε πώς να ορίσετε προσαρμοσμένες ετικέτες ή περάσματα μεταγλωττιστή, είναι απαραίτητο να καταλάβετε πώς λειτουργεί το Latte κάτω από την κουκούλα.

Η μεταγλώττιση προτύπων στο Latte απλουστευτικά λειτουργεί ως εξής:

  • Πρώτον, ο lexer μετατρέπει τον πηγαίο κώδικα του προτύπου σε μικρά κομμάτια (tokens) για ευκολότερη επεξεργασία.
  • Στη συνέχεια, ο διαχωριστής μετατρέπει τη ροή των tokens σε ένα ουσιαστικό δέντρο κόμβων (το Αφηρημένο Συντακτικό Δέντρο, AST).
  • Τέλος, ο μεταγλωττιστής δημιουργεί μια κλάση PHP από το AST που αποδίδει το πρότυπο και το αποθηκεύει.

Στην πραγματικότητα, η μεταγλώττιση είναι λίγο πιο περίπλοκη. Το Latte έχει δύο λεξικογράφους και αναλυτές: έναν για το πρότυπο HTML και έναν για τον κώδικα που μοιάζει με PHP μέσα στις ετικέτες. Επίσης, η ανάλυση δεν εκτελείται μετά τη tokenization, αλλά ο lexer και ο parser τρέχουν παράλληλα σε δύο „νήματα“ και συντονίζονται. Είναι επιστήμη πυραύλων :-)

Επιπλέον, όλες οι ετικέτες έχουν τις δικές τους ρουτίνες ανάλυσης. Όταν ο αναλυτής συναντά μια ετικέτα, καλεί τη συνάρτηση ανάλυσης της (επιστρέφει την Extension::getTags()). Η δουλειά τους είναι να αναλύσουν τα ορίσματα της ετικέτας και, στην περίπτωση των ζευγαρωμένων ετικετών, το εσωτερικό περιεχόμενο. Επιστρέφει έναν κόμβο που γίνεται μέρος του AST. Ανατρέξτε στη λειτουργία ανάλυσης ετικετών για λεπτομέρειες.

Όταν ο αναλυτής ολοκληρώσει τη δουλειά του, έχουμε ένα πλήρες AST που αναπαριστά το πρότυπο. Ο κόμβος ρίζα είναι το Latte\Compiler\Nodes\TemplateNode. Στη συνέχεια, οι επιμέρους κόμβοι μέσα στο δέντρο αντιπροσωπεύουν όχι μόνο τις ετικέτες, αλλά και τα στοιχεία HTML, τα χαρακτηριστικά τους, τυχόν εκφράσεις που χρησιμοποιούνται μέσα στις ετικέτες κ.λπ.

Στη συνέχεια, μπαίνουν στο παιχνίδι τα λεγόμενα περάσματα του μεταγλωττιστή, τα οποία είναι συναρτήσεις (που επιστρέφονται από την Extension::getPasses()) που τροποποιούν το AST.

Όλη η διαδικασία, από τη φόρτωση του περιεχομένου του προτύπου, μέσω της ανάλυσης, μέχρι τη δημιουργία του αρχείου που προκύπτει, μπορεί να ακολουθηθεί με αυτόν τον κώδικα, με τον οποίο μπορείτε να πειραματιστείτε και να απορρίψετε τα ενδιάμεσα αποτελέσματα:

$latte = new Latte\Engine;
$source = $latte->getLoader()->getContent($file);
$ast = $latte->parse($source);
$latte->applyPasses($ast);
$code = $latte->generate($ast, $file);

Παράδειγμα AST

Για να έχουμε μια καλύτερη ιδέα του AST, προσθέτουμε ένα δείγμα. Αυτό είναι το πρότυπο πηγής:

{foreach $category->getItems() as $item}
	<li>{$item->name|upper}</li>
	{else}
	no items found
{/foreach}

Και αυτή είναι η αναπαράστασή του με τη μορφή AST:

Latte\Compiler\Nodes\TemplateNode(
   Latte\Compiler\Nodes\FragmentNode(
      - Latte\Essential\Nodes\ForeachNode(
           expression: Latte\Compiler\Nodes\Php\Expression\MethodCallNode(
              object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$category')
              name: Latte\Compiler\Nodes\Php\IdentifierNode('getItems')
           )
           value: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item')
           content: Latte\Compiler\Nodes\FragmentNode(
              - Latte\Compiler\Nodes\TextNode('  ')
              - Latte\Compiler\Nodes\Html\ElementNode('li')(
                   content: Latte\Essential\Nodes\PrintNode(
                      expression: Latte\Compiler\Nodes\Php\Expression\PropertyFetchNode(
                         object: Latte\Compiler\Nodes\Php\Expression\VariableNode('$item')
                         name: Latte\Compiler\Nodes\Php\IdentifierNode('name')
                      )
                      modifier: Latte\Compiler\Nodes\Php\ModifierNode(
                         filters:
                            - Latte\Compiler\Nodes\Php\FilterNode('upper')
                      )
                   )
                )
            )
            else: Latte\Compiler\Nodes\FragmentNode(
               - Latte\Compiler\Nodes\TextNode('no items found')
            )
        )
   )
)

Προσαρμοσμένες ετικέτες

Τρία βήματα απαιτούνται για τον ορισμό μιας νέας ετικέτας:

Λειτουργία ανάλυσης ετικέτας

Η ανάλυση των ετικετών γίνεται από τη συνάρτηση ανάλυσης (αυτή που επιστρέφεται από την Extension::getTags()). Η δουλειά της είναι να αναλύει και να ελέγχει τυχόν ορίσματα μέσα στην ετικέτα (χρησιμοποιεί τον TagParser για να το κάνει αυτό). Επιπλέον, αν η ετικέτα είναι ένα ζεύγος, θα ζητήσει από τον TemplateParser να αναλύσει και να επιστρέψει το εσωτερικό περιεχόμενο. Η συνάρτηση δημιουργεί και επιστρέφει έναν κόμβο, ο οποίος είναι συνήθως παιδί του Latte\Compiler\Nodes\StatementNode, και αυτός γίνεται μέρος του AST.

Δημιουργούμε μια κλάση για κάθε κόμβο, κάτι που θα κάνουμε τώρα, και τοποθετούμε κομψά τη συνάρτηση ανάλυσης σε αυτήν ως στατικό εργοστάσιο. Ως παράδειγμα, ας δοκιμάσουμε να δημιουργήσουμε τη γνωστή ετικέτα {foreach}:

use Latte\Compiler\Nodes\StatementNode;

class ForeachNode extends StatementNode
{
	// μια συνάρτηση ανάλυσης που απλά δημιουργεί έναν κόμβο προς το παρόν
	public static function create(Latte\Compiler\Tag $tag): self
	{
		$node = $tag->node = new self;
		return $node;
	}

	public function print(Latte\Compiler\PrintContext $context): string
	{
		// ο κώδικας θα προστεθεί αργότερα
	}

	public function &getIterator(): \Generator
	{
		// ο κώδικας θα προστεθεί αργότερα
	}
}

Στη συνάρτηση ανάλυσης create() περνάει ένα αντικείμενο Latte\Compiler\Tag, το οποίο μεταφέρει βασικές πληροφορίες για την ετικέτα (αν είναι κλασική ετικέτα ή n:attribute, σε ποια γραμμή βρίσκεται, κ.λπ.) και κυρίως προσπελαύνει το Latte\Compiler\TagParser στο $tag->parser.

Εάν η ετικέτα πρέπει να έχει ορίσματα, ελέγξτε την ύπαρξή τους καλώντας το $tag->expectArguments(). Οι μέθοδοι του αντικειμένου $tag->parser είναι διαθέσιμες για την ανάλυσή τους:

  • parseExpression(): ExpressionNode για μια έκφραση τύπου PHP (π.χ. 10 + 3)
  • parseUnquotedStringOrExpression(): ExpressionNode για μια έκφραση ή μια συμβολοσειρά χωρίς εισαγωγικά
  • parseArguments(): ArrayNode περιεχόμενο του πίνακα (π.χ. 10, true, foo => bar)
  • parseModifier(): ModifierNode για έναν τροποποιητή (π.χ. |upper|truncate:10)
  • parseType(): expressionNode για typehint (π.χ. int|string ή Foo\Bar[])

και ένα χαμηλού επιπέδου Latte\Compiler\TokenStream που λειτουργεί απευθείας με tokens:

  • $tag->parser->stream->consume(...): Token
  • $tag->parser->stream->tryConsume(...): ?Token

Το Latte επεκτείνει τη σύνταξη της PHP με μικρούς τρόπους, για παράδειγμα προσθέτοντας τροποποιητές, συντομευμένους τριμερείς τελεστές ή επιτρέποντας την εγγραφή απλών αλφαριθμητικών συμβολοσειρών χωρίς εισαγωγικά. Αυτός είναι ο λόγος για τον οποίο χρησιμοποιούμε τον όρο PHP-like αντί για PHP. Έτσι, η μέθοδος parseExpression() αναλύει το foo ως 'foo', για παράδειγμα. Επιπλέον, η unquoted-string είναι μια ειδική περίπτωση συμβολοσειράς που επίσης δεν χρειάζεται να είναι σε εισαγωγικά, αλλά ταυτόχρονα δεν χρειάζεται να είναι αλφαριθμητική. Για παράδειγμα, είναι η διαδρομή προς ένα αρχείο στην ετικέτα {include ../file.latte}. Η μέθοδος parseUnquotedStringOrExpression() χρησιμοποιείται για την ανάλυσή του.

Η μελέτη των κλάσεων κόμβων που αποτελούν μέρος του Latte είναι ο καλύτερος τρόπος για να μάθετε όλες τις μικρές λεπτομέρειες της διαδικασίας ανάλυσης.

Ας επιστρέψουμε στην ετικέτα {foreach}. Σε αυτήν, περιμένουμε ορίσματα της μορφής expression + 'as' + second expression, τα οποία αναλύουμε ως εξής:

use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\AreaNode;

class ForeachNode extends StatementNode
{
	public ExpressionNode $expression;
	public ExpressionNode $value;

	public static function create(Latte\Compiler\Tag $tag): self
	{
		$tag->expectArguments();
		$node = $tag->node = new self;
		$node->expression = $tag->parser->parseExpression();
		$tag->parser->stream->consume('as');
		$node->value = $parser->parseExpression();
		return $node;
	}
}

Οι εκφράσεις που έχουμε γράψει στις μεταβλητές $expression και $value αντιπροσωπεύουν υποκόμβους.

Ορίστε τις μεταβλητές με υποκόμβους ως δημόσιες, ώστε να μπορούν να τροποποιηθούν σε περαιτέρω βήματα επεξεργασίας, αν χρειαστεί. Είναι επίσης απαραίτητο να διαθέσουμε τις μεταβλητές αυτές για διάσχιση.

Για ζευγαρωτές ετικέτες, όπως η δική μας, η μέθοδος πρέπει επίσης να επιτρέπει στον TemplateParser να αναλύει τα εσωτερικά περιεχόμενα της ετικέτας. Αυτό αντιμετωπίζεται από το yield, το οποίο επιστρέφει ένα ζεύγος [εσωτερικό περιεχόμενο, τέλος ετικέτας]. Αποθηκεύουμε το εσωτερικό περιεχόμενο στη μεταβλητή $node->content.

public AreaNode $content;

public static function create(Latte\Compiler\Tag $tag): \Generator
{
	// ...
	[$node->content, $endTag] = yield;
	return $node;
}

Η λέξη-κλειδί yield προκαλεί τον τερματισμό της μεθόδου create(), επιστρέφοντας τον έλεγχο πίσω στον TemplateParser, ο οποίος συνεχίζει την ανάλυση του περιεχομένου μέχρι να συναντήσει την ετικέτα end. Στη συνέχεια περνάει τον έλεγχο πίσω στο create(), το οποίο συνεχίζει από εκεί που σταμάτησε. Η χρήση της μεθόδου yield, επιστρέφει αυτόματα τη μέθοδο Generator.

Μπορείτε επίσης να περάσετε έναν πίνακα ονομάτων ετικετών στο yield για τις οποίες θέλετε να σταματήσετε την ανάλυση, αν εμφανιστούν πριν από την ετικέτα τέλους. Αυτό μας βοηθάει να υλοποιήσουμε την {foreach}...{else}...{/foreach} construct. Εάν εμφανιστεί το {else}, αναλύουμε το περιεχόμενο μετά από αυτό στο $node->elseContent:

public AreaNode $content;
public ?AreaNode $elseContent = null;

public static function create(Latte\Compiler\Tag $tag): \Generator
{
	// ...
	[$node->content, $nextTag] = yield ['else'];
	if ($nextTag?->name === 'else') {
		[$node->elseContent] = yield;
	}

	return $node;
}

Με την επιστροφή του κόμβου ολοκληρώνεται η ανάλυση της ετικέτας.

Δημιουργία κώδικα PHP

Κάθε κόμβος πρέπει να υλοποιεί τη μέθοδο print(). Επιστρέφει κώδικα PHP που αποδίδει το συγκεκριμένο τμήμα του προτύπου (κώδικας εκτέλεσης). Παραδίδεται ένα αντικείμενο Latte\Compiler\PrintContext ως παράμετρος, το οποίο διαθέτει μια χρήσιμη μέθοδο format() που απλοποιεί τη συναρμολόγηση του κώδικα που προκύπτει.

Η μέθοδος format(string $mask, ...$args) δέχεται τα ακόλουθα placeholders στη μάσκα:

  • %node εκτυπώνει τον κόμβο
  • %dump εξάγει την τιμή στην PHP
  • %raw εισάγει το κείμενο απευθείας χωρίς μετασχηματισμό
  • %args εκτυπώνει ArrayNode ως ορίσματα στην κλήση της συνάρτησης
  • %line εκτυπώνει ένα σχόλιο με αριθμό γραμμής
  • %escape(...) διαφυγή του περιεχομένου
  • %modify(...) εφαρμόζει τροποποιητή
  • %modifyContent(...) εφαρμόζει τροποποιητή σε μπλοκ

Η συνάρτηση print() θα μπορούσε να μοιάζει ως εξής (παραλείπουμε τον κλάδο else για λόγους απλότητας):

public function print(Latte\Compiler\PrintContext $context): string
{
	return $context->format(
		<<<'XX'
			foreach (%node as %node) %line {
				%node
			}

			XX,
		$this->expression,
		$this->value,
		$this->position,
		$this->content,
	);
}

Η μεταβλητή $this->position έχει ήδη οριστεί από την κλάση Latte\Compiler\Node και ορίζεται από τον αναλυτή. Περιέχει ένα αντικείμενο Latte\Compiler\Position με τη θέση της ετικέτας στον πηγαίο κώδικα με τη μορφή αριθμού γραμμής και στήλης.

Ο κώδικας εκτέλεσης μπορεί να χρησιμοποιεί βοηθητικές μεταβλητές. Για να αποφευχθεί η σύγκρουση με τις μεταβλητές που χρησιμοποιούνται από το ίδιο το πρότυπο, είναι σύμβαση να τους προτάσσεται με χαρακτήρες $ʟ__.

Μπορεί επίσης να χρησιμοποιεί αυθαίρετες τιμές κατά την εκτέλεση, οι οποίες περνούν στο πρότυπο με τη μορφή παρόχων χρησιμοποιώντας τη μέθοδο Extension::getProviders(). Η πρόσβαση σε αυτές γίνεται με τη χρήση του $this->global->....

Διασχίζοντας το AST

Για να διασχίσουμε το δέντρο AST σε βάθος, είναι απαραίτητο να υλοποιήσουμε τη μέθοδο getIterator(). Αυτό θα παρέχει πρόσβαση σε υποκόμβους:

public function &getIterator(): \Generator
{
	yield $this->expression;
	yield $this->value;
	yield $this->content;
	if ($this->elseContent) {
		yield $this->elseContent;
	}
}

Σημειώστε ότι η getIterator() επιστρέφει μια αναφορά. Αυτό είναι που επιτρέπει στους επισκέπτες των κόμβων να αντικαταστήσουν μεμονωμένους κόμβους με άλλους κόμβους.

Εάν ένας κόμβος έχει υποκόμβους, είναι απαραίτητο να υλοποιήσετε αυτή τη μέθοδο και να καταστήσετε όλους τους υποκόμβους διαθέσιμους. Διαφορετικά, θα μπορούσε να δημιουργηθεί ένα κενό ασφαλείας. Για παράδειγμα, η λειτουργία sandbox δεν θα μπορούσε να ελέγξει τους υποκόμβους και να διασφαλίσει ότι δεν θα καλούνται σε αυτούς μη επιτρεπόμενες κατασκευές.

Δεδομένου ότι η λέξη-κλειδί yield πρέπει να υπάρχει στο σώμα της μεθόδου, ακόμη και αν δεν έχει κόμβους-παιδιά, γράψτε την ως εξής:

public function &getIterator(): \Generator
{
	if (false) {
		yield;
	}
}

AuxiliaryNode

Εάν δημιουργείτε μια νέα ετικέτα για το Latte, είναι σκόπιμο να δημιουργήσετε μια ειδική κλάση κόμβου για αυτήν, η οποία θα την αντιπροσωπεύει στο δέντρο AST (βλέπε την κλάση ForeachNode στο παραπάνω παράδειγμα). Σε ορισμένες περιπτώσεις, μπορεί να βρείτε χρήσιμη την τετριμμένη βοηθητική κλάση κόμβου AuxiliaryNode, η οποία σας επιτρέπει να περάσετε το σώμα της μεθόδου print() και τη λίστα των κόμβων που γίνονται προσβάσιμοι από τη μέθοδο getIterator() ως παραμέτρους του κατασκευαστή:

// Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode
// or Latte\Compiler\Nodes\AuxiliaryNode

$node = new AuxiliaryNode(
	// body of the print() method:
	fn(PrintContext $context, $argNode) => $context->format('myFunc(%node)', $argNode),
	// nodes accessed via getIterator() and also passed into the print() method:
	[$argNode],
);

Μεταγλωττιστής περνάει

Τα περάσματα μεταγλωττιστή είναι συναρτήσεις που τροποποιούν τα AST ή συλλέγουν πληροφορίες σε αυτά. Επιστρέφονται από τη μέθοδο Extension::getPasses().

Ανιχνευτής κόμβων

Ο πιο συνηθισμένος τρόπος για να εργαστούμε με το AST είναι με τη χρήση ενός Latte\Compiler\NodeTraverser:

use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: fn(Node $node) => ...,
	leave: fn(Node $node) => ...,
);

Η συνάρτηση enter (δηλαδή ο επισκέπτης) καλείται όταν συναντάται για πρώτη φορά ένας κόμβος, πριν από την επεξεργασία των υποκόμβων του. Η συνάρτηση leave καλείται μετά την επίσκεψη όλων των υποκόμβων. Ένα κοινό μοτίβο είναι ότι η enter χρησιμοποιείται για να συλλέξει κάποιες πληροφορίες και στη συνέχεια η leave εκτελεί τροποποιήσεις με βάση αυτές. Τη στιγμή που καλείται η leave, όλος ο κώδικας μέσα στον κόμβο θα έχει ήδη επισκεφθεί και θα έχουν συλλεχθεί οι απαραίτητες πληροφορίες.

Πώς να τροποποιήσετε το AST; Ο ευκολότερος τρόπος είναι απλά να αλλάξετε τις ιδιότητες των κόμβων. Ο δεύτερος τρόπος είναι η πλήρης αντικατάσταση του κόμβου επιστρέφοντας έναν νέο κόμβο. Παράδειγμα: ο παρακάτω κώδικας θα αλλάξει όλους τους ακέραιους αριθμούς στο AST σε συμβολοσειρές (π.χ. το 42 θα αλλάξει σε '42').

use Latte\Compiler\Nodes\Php;

$ast = (new NodeTraverser)->traverse(
	$ast,
	leave: function (Node $node) {
		if ($node instanceof Php\Scalar\IntegerNode) {
            return new Php\Scalar\StringNode((string) $node->value);
        }
	},
);

Ένα AST μπορεί εύκολα να περιέχει χιλιάδες κόμβους και η διάσχιση όλων αυτών μπορεί να είναι αργή. Σε ορισμένες περιπτώσεις, είναι δυνατόν να αποφευχθεί η πλήρης διάσχιση.

Αν ψάχνετε για όλους τους κόμβους Html\ElementNode σε ένα δέντρο, ξέρετε ότι αφού έχετε δει το Php\ExpressionNode, δεν υπάρχει λόγος να ελέγξετε επίσης όλους τους κόμβους-παιδιά του, επειδή η HTML δεν μπορεί να είναι μέσα σε εκφράσεις. Σε αυτή την περίπτωση, μπορείτε να δώσετε εντολή στον περιηγητή να μην κάνει αναδρομή στον κόμβο κλάσης:

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: function (Node $node) {
		if ($node instanceof Php\ExpressionNode) {
			return NodeTraverser::DontTraverseChildren;
        }
        // ...
	},
);

Αν ψάχνετε μόνο για έναν συγκεκριμένο κόμβο, είναι επίσης δυνατό να διακόψετε εντελώς την αναστροφή αφού τον βρείτε.

$ast = (new NodeTraverser)->traverse(
	$ast,
	enter: function (Node $node) {
		if ($node instanceof Nodes\ParametersNode) {
			return NodeTraverser::StopTraversal;
        }
        // ...
	},
);

Βοηθοί κόμβων

Η κλάση Latte\Compiler\NodeHelpers παρέχει ορισμένες μεθόδους που μπορούν να βρουν AST κόμβους που είτε ικανοποιούν ένα συγκεκριμένο callback κ.λπ. Παρουσιάζονται μερικά παραδείγματα:

use Latte\Compiler\NodeHelpers;

// βρίσκει όλους τους κόμβους στοιχείων HTML
$elements = NodeHelpers::find($ast, fn(Node $node) => $node instanceof Nodes\Html\ElementNode);

// βρίσκει τον πρώτο κόμβο κειμένου
$text = NodeHelpers::findFirst($ast, fn(Node $node) => $node instanceof Nodes\TextNode);

// μετατρέπει τον κόμβο τιμών PHP σε πραγματική τιμή
$value = NodeHelpers::toValue($node);

// μετατρέπει στατικό κόμβο κειμένου σε συμβολοσειρά
$text = NodeHelpers::toText($node);