Δημιουργία προσαρμοσμένων tags

Αυτή η σελίδα παρέχει έναν ολοκληρωμένο οδηγό για τη δημιουργία προσαρμοσμένων tags στο Latte. Θα καλύψουμε τα πάντα, από απλά tags έως πιο σύνθετα σενάρια με ένθετο περιεχόμενο και συγκεκριμένες ανάγκες ανάλυσης, βασιζόμενοι στην κατανόησή σας για το πώς το Latte μεταγλωττίζει τα templates.

Τα προσαρμοσμένα tags παρέχουν το υψηλότερο επίπεδο ελέγχου στη σύνταξη του template και τη λογική απόδοσης, αλλά είναι επίσης το πιο πολύπλοκο σημείο επέκτασης. Πριν αποφασίσετε να δημιουργήσετε ένα προσαρμοσμένο tag, εξετάστε πάντα αν δεν υπάρχει απλούστερη λύση ή αν ένα κατάλληλο tag υπάρχει ήδη στο τυπικό σύνολο. Χρησιμοποιήστε προσαρμοσμένα tags μόνο όταν οι απλούστερες εναλλακτικές δεν επαρκούν για τις ανάγκες σας.

Κατανόηση της διαδικασίας μεταγλώττισης

Για την αποτελεσματική δημιουργία προσαρμοσμένων tags, είναι χρήσιμο να εξηγήσουμε πώς το Latte επεξεργάζεται τα templates. Η κατανόηση αυτής της διαδικασίας διευκρινίζει γιατί τα tags είναι δομημένα με αυτόν τον τρόπο και πώς εντάσσονται στο ευρύτερο πλαίσιο.

Η μεταγλώττιση ενός template στο Latte, απλουστευμένα, περιλαμβάνει αυτά τα βασικά βήματα:

  1. Λεξικογραφική ανάλυση: Ο lexer διαβάζει τον πηγαίο κώδικα του template (αρχείο .latte) και τον χωρίζει σε μια ακολουθία μικρών, διακριτών τμημάτων που ονομάζονται tokens (π.χ. {, foreach, $variable, }, κείμενο HTML, κ.λπ.).
  2. Ανάλυση: Ο parser παίρνει αυτή τη ροή tokens και κατασκευάζει από αυτή μια ουσιαστική δενδρική δομή που αντιπροσωπεύει τη λογική και το περιεχόμενο του template. Αυτό το δέντρο ονομάζεται Abstract Syntax Tree (AST).
  3. Compilation passes: Πριν από τη δημιουργία του κώδικα PHP, το Latte εκτελεί compilation passes. Αυτές είναι συναρτήσεις που διασχίζουν ολόκληρο το AST και μπορούν να το τροποποιήσουν ή να συλλέξουν πληροφορίες. Αυτό το βήμα είναι κρίσιμο για λειτουργίες όπως η ασφάλεια (Sandbox) ή η βελτιστοποίηση.
  4. Code generation: Τέλος, ο compiler διασχίζει το (πιθανώς τροποποιημένο) AST και δημιουργεί τον αντίστοιχο κώδικα της κλάσης PHP. Αυτός ο κώδικας PHP είναι αυτός που στην πραγματικότητα αποδίδει το template κατά την εκτέλεση.
  5. Caching: Ο παραγόμενος κώδικας PHP αποθηκεύεται στο δίσκο, καθιστώντας τις επακόλουθες αποδόσεις πολύ γρήγορες, καθώς τα βήματα 1–4 παραλείπονται.

Στην πραγματικότητα, η μεταγλώττιση είναι λίγο πιο περίπλοκη. Το Latte έχει δύο lexers και parsers: έναν για το HTML template και έναν άλλο για τον PHP-like κώδικα εντός των tags. Επίσης, η ανάλυση δεν πραγματοποιείται μετά την tokenization, αλλά ο lexer και ο parser εκτελούνται παράλληλα σε δύο “νήματα” και συντονίζονται. Πιστέψτε με, ο προγραμματισμός του ήταν πυραυλική επιστήμη :-)

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

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

Η ανατομία ενός tag

Η δημιουργία ενός πλήρως λειτουργικού προσαρμοσμένου tag στο Latte περιλαμβάνει πολλά συνδεδεμένα μέρη. Πριν ξεκινήσουμε την υλοποίηση, ας κατανοήσουμε τις βασικές έννοιες και την ορολογία, χρησιμοποιώντας μια αναλογία με το HTML και το Document Object Model (DOM).

Tags vs. Κόμβοι (Αναλογία με HTML)

Στο HTML, γράφουμε tags όπως <p> ή <div>...</div>. Αυτά τα tags είναι η σύνταξη στον πηγαίο κώδικα. Όταν ο browser αναλύει αυτό το HTML, δημιουργεί μια αναπαράσταση στη μνήμη που ονομάζεται Document Object Model (DOM). Στο DOM, τα HTML tags αντιπροσωπεύονται από κόμβους (συγκεκριμένα κόμβους Element στην ορολογία του JavaScript DOM). Με αυτούς τους κόμβους εργαζόμαστε προγραμματιστικά (π.χ., το document.getElementById(...) του JavaScript επιστρέφει έναν κόμβο Element). Το tag είναι απλώς μια κειμενική αναπαράσταση στο πηγαίο αρχείο. Ο κόμβος είναι μια αντικειμενοστρεφής αναπαράσταση στο λογικό δέντρο.

Το Latte λειτουργεί παρόμοια:

  • Στο αρχείο .latte του template, γράφετε Latte tags, όπως {foreach ...} και {/foreach}. Αυτή είναι η σύνταξη με την οποία εργάζεστε εσείς ως συγγραφέας του template.
  • Όταν το Latte αναλύει το template, χτίζει ένα Abstract Syntax Tree (AST). Αυτό το δέντρο αποτελείται από κόμβους. Κάθε Latte tag, HTML element, κομμάτι κειμένου ή έκφραση στο template γίνεται ένας ή περισσότεροι κόμβοι σε αυτό το δέντρο.
  • Η βασική κλάση για όλους τους κόμβους στο AST είναι η Latte\Compiler\Node. Όπως το DOM έχει διαφορετικούς τύπους κόμβων (Element, Text, Comment), το AST του Latte έχει διαφορετικούς τύπους κόμβων. Θα συναντήσετε Latte\Compiler\Nodes\TextNode για στατικό κείμενο, Latte\Compiler\Nodes\Html\ElementNode για HTML elements, Latte\Compiler\Nodes\Php\ExpressionNode για εκφράσεις εντός των tags και, κρίσιμα για τα προσαρμοσμένα tags, κόμβους που κληρονομούν από το Latte\Compiler\Nodes\StatementNode.

Γιατί StatementNode;

Τα HTML elements (Html\ElementNode) αντιπροσωπεύουν κυρίως δομή και περιεχόμενο. Οι PHP εκφράσεις (Php\ExpressionNode) αντιπροσωπεύουν τιμές ή υπολογισμούς. Αλλά τι γίνεται με τα Latte tags όπως {if}, {foreach} ή το δικό μας {datetime}; Αυτά τα tags εκτελούν ενέργειες, ελέγχουν τη ροή του προγράμματος ή παράγουν έξοδο βάσει λογικής. Είναι λειτουργικές μονάδες που καθιστούν το Latte μια ισχυρή μηχανή templating, όχι απλώς μια γλώσσα σήμανσης.

Στον προγραμματισμό, τέτοιες μονάδες που εκτελούν ενέργειες συχνά ονομάζονται “statements” (εντολές). Γι' αυτό οι κόμβοι που αντιπροσωπεύουν αυτά τα λειτουργικά Latte tags τυπικά κληρονομούν από το Latte\Compiler\Nodes\StatementNode. Αυτό τους διακρίνει από τους καθαρά δομικούς κόμβους (όπως τα HTML elements) ή τους κόμβους που αντιπροσωπεύουν τιμές (όπως οι εκφράσεις).

Βασικά συστατικά

Ας δούμε τα κύρια συστατικά που απαιτούνται για τη δημιουργία ενός προσαρμοσμένου tag:

Συνάρτηση ανάλυσης tag

  • Αυτή η PHP callable συνάρτηση αναλύει τη σύνταξη του Latte tag ({...}) στο πηγαίο template.
  • Λαμβάνει πληροφορίες για το tag (όπως το όνομά του, τη θέση του και αν είναι n:attribute) μέσω του αντικειμένου Latte\Compiler\Tag.
  • Το κύριο εργαλείο της για την ανάλυση των arguments και των εκφράσεων εντός των οριοθετών του tag είναι το αντικείμενο Latte\Compiler\TagParser, προσβάσιμο μέσω του $tag->parser (αυτός είναι διαφορετικός parser από αυτόν που αναλύει ολόκληρο το template).
  • Για τα paired tags, χρησιμοποιεί το yield για να σηματοδοτήσει στο Latte να αναλύσει το εσωτερικό περιεχόμενο μεταξύ του αρχικού και του τελικού tag.
  • Ο τελικός στόχος της συνάρτησης ανάλυσης είναι να δημιουργήσει και να επιστρέψει μια παρουσία της κλάσης κόμβου, η οποία προστίθεται στο AST.
  • Είναι σύνηθες (αν και δεν απαιτείται) να υλοποιείται η συνάρτηση ανάλυσης ως στατική μέθοδος (συχνά ονομάζεται create) απευθείας στην αντίστοιχη κλάση κόμβου. Αυτό διατηρεί τη λογική ανάλυσης και την αναπαράσταση του κόμβου τακτοποιημένα σε ένα πακέτο, επιτρέπει την πρόσβαση σε ιδιωτικά/προστατευμένα μέλη της κλάσης, εάν χρειάζεται, και βελτιώνει την οργάνωση.

Κλάση κόμβου

  • Αντιπροσωπεύει τη λογική λειτουργία του tag σας στο Abstract Syntax Tree (AST).
  • Περιέχει τις αναλυμένες πληροφορίες (όπως arguments ή περιεχόμενο) ως δημόσιες ιδιότητες. Αυτές οι ιδιότητες συχνά περιέχουν άλλες παρουσίες Node (π.χ. ExpressionNode για αναλυμένα arguments, AreaNode για αναλυμένο περιεχόμενο).
  • Η μέθοδος print(PrintContext $context): string δημιουργεί τον κώδικα PHP (μια εντολή ή μια σειρά εντολών) που εκτελεί την ενέργεια του tag κατά την απόδοση του template.
  • Η μέθοδος getIterator(): \Generator καθιστά προσβάσιμους τους παιδικούς κόμβους (arguments, περιεχόμενο) για τη διέλευση από τα compilation passes. Πρέπει να παρέχει αναφορές (&) για να επιτρέπει στα passes να τροποποιούν ή να αντικαθιστούν πιθανώς τους υποκόμβους.
  • Αφού ολόκληρο το template αναλυθεί σε AST, το Latte εκτελεί μια σειρά από compilation passes. Αυτά τα passes διασχίζουν ολόκληρο το AST χρησιμοποιώντας τη μέθοδο getIterator() που παρέχεται από κάθε κόμβο. Μπορούν να επιθεωρήσουν κόμβους, να συλλέξουν πληροφορίες και ακόμη και να τροποποιήσουν το δέντρο (π.χ. αλλάζοντας τις δημόσιες ιδιότητες των κόμβων ή αντικαθιστώντας πλήρως κόμβους). Αυτός ο σχεδιασμός, που απαιτεί ένα ολοκληρωμένο getIterator(), είναι θεμελιώδης. Επιτρέπει σε ισχυρές λειτουργίες όπως το Sandbox να αναλύουν και πιθανώς να αλλάζουν τη συμπεριφορά οποιουδήποτε μέρους του template, συμπεριλαμβανομένων των δικών σας προσαρμοσμένων tags, διασφαλίζοντας την ασφάλεια και τη συνέπεια.

Εγγραφή μέσω επέκτασης

  • Πρέπει να ενημερώσετε το Latte για το νέο σας tag και ποια συνάρτηση ανάλυσης πρέπει να χρησιμοποιηθεί για αυτό. Αυτό γίνεται στο πλαίσιο μιας επέκτασης Latte.
  • Μέσα στην κλάση επέκτασής σας, υλοποιείτε τη μέθοδο getTags(): array. Αυτή η μέθοδος επιστρέφει έναν συσχετιστικό πίνακα όπου τα κλειδιά είναι τα ονόματα των tags (π.χ. 'mytag', 'n:myattribute') και οι τιμές είναι PHP callable συναρτήσεις που αντιπροσωπεύουν τις αντίστοιχες συναρτήσεις ανάλυσής τους (π.χ. MyNamespace\DatetimeNode::create(...)).

Σύνοψη: Η συνάρτηση ανάλυσης tag μετατρέπει τον πηγαίο κώδικα του template του tag σας σε έναν κόμβο AST. Η κλάση κόμβου μπορεί στη συνέχεια να μετατρέψει τον εαυτό της σε εκτελέσιμο κώδικα PHP για το μεταγλωττισμένο template και καθιστά τους υποκόμβους της προσβάσιμους για τα compilation passes μέσω του getIterator(). Η εγγραφή μέσω επέκτασης συνδέει το όνομα του tag με τη συνάρτηση ανάλυσης και το γνωστοποιεί στο Latte.

Τώρα θα εξερευνήσουμε πώς να υλοποιήσουμε αυτά τα συστατικά βήμα προς βήμα.

Δημιουργία ενός απλού tag

Ας ξεκινήσουμε με τη δημιουργία του πρώτου σας προσαρμοσμένου Latte tag. Θα ξεκινήσουμε με ένα πολύ απλό παράδειγμα: ένα tag με το όνομα {datetime}, το οποίο εκτυπώνει την τρέχουσα ημερομηνία και ώρα. Αρχικά, αυτό το tag δεν θα δέχεται κανένα argument, αλλά θα το βελτιώσουμε αργότερα στην ενότητα Ανάλυση των arguments του tag. Επίσης, δεν έχει εσωτερικό περιεχόμενο.

Αυτό το παράδειγμα θα σας καθοδηγήσει στα βασικά βήματα: ορισμός της κλάσης κόμβου, υλοποίηση των μεθόδων της print() και getIterator(), δημιουργία της συνάρτησης ανάλυσης και, τέλος, εγγραφή του tag.

Στόχος: Υλοποίηση του {datetime} για την έξοδο της τρέχουσας ημερομηνίας και ώρας χρησιμοποιώντας τη συνάρτηση PHP date().

Δημιουργία της κλάσης κόμβου

Πρώτα, χρειαζόμαστε μια κλάση που θα αντιπροσωπεύει το tag μας στο Abstract Syntax Tree (AST). Όπως συζητήθηκε παραπάνω, κληρονομούμε από το Latte\Compiler\Nodes\StatementNode.

Δημιουργήστε ένα αρχείο (π.χ. DatetimeNode.php) και ορίστε την κλάση:

<?php

namespace App\Latte;

use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class DatetimeNode extends StatementNode
{
	/**
	 * Συνάρτηση ανάλυσης tag, καλείται όταν βρίσκεται το {datetime}.
	 */
	public static function create(Tag $tag): self
	{
		// Το απλό μας tag προς το παρόν δεν δέχεται arguments, οπότε δεν χρειάζεται να αναλύσουμε τίποτα
		$node = $tag->node = new self;
		return $node;
	}

	/**
	 * Δημιουργεί τον κώδικα PHP που θα εκτελεστεί κατά την απόδοση του template.
	 */
	public function print(PrintContext $context): string
	{
		return $context->format(
			'echo date(\'Y-m-d H:i:s\') %line;',
			$this->position,
		);
	}

	/**
	 * Παρέχει πρόσβαση στους παιδικούς κόμβους για τα compilation passes του Latte.
	 */
	public function &getIterator(): \Generator
	{
		false && yield;
	}
}

Όταν το Latte συναντήσει το {datetime} στο template, καλεί τη συνάρτηση ανάλυσης create(). Ο ρόλος της είναι να επιστρέψει μια παρουσία του DatetimeNode.

Η μέθοδος print() δημιουργεί τον κώδικα PHP που θα εκτελεστεί κατά την απόδοση του template. Καλούμε τη μέθοδο $context->format(), η οποία συνθέτει την τελική συμβολοσειρά κώδικα PHP για το μεταγλωττισμένο template. Το πρώτο argument, 'echo date('Y-m-d H:i:s') %line;', είναι μια μάσκα στην οποία συμπληρώνονται οι ακόλουθες παράμετροι. Ο placeholder %line λέει στη μέθοδο format() να χρησιμοποιήσει το δεύτερο argument, το οποίο είναι το $this->position, και να εισάγει ένα σχόλιο όπως /* line 15 */, το οποίο συνδέει τον παραγόμενο κώδικα PHP πίσω στην αρχική γραμμή του template, κάτι που είναι κρίσιμο για το debugging.

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

Η μέθοδος getIterator() είναι θεμελιώδης για τα compilation passes. Πρέπει να παρέχει όλους τους παιδικούς κόμβους, αλλά το απλό μας DatetimeNode προς το παρόν δεν έχει arguments ή περιεχόμενο, άρα ούτε παιδικούς κόμβους. Ωστόσο, η μέθοδος πρέπει ακόμα να υπάρχει και να είναι generator, δηλαδή η λέξη-κλειδί yield πρέπει να είναι παρούσα με κάποιον τρόπο στο σώμα της μεθόδου.

Εγγραφή μέσω επέκτασης

Τέλος, ας ενημερώσουμε το Latte για το νέο tag. Δημιουργήστε μια κλάση επέκτασης (π.χ. MyLatteExtension.php) και καταχωρήστε το tag στη μέθοδό της getTags().

<?php

namespace App\Latte;

use Latte\Extension;

class MyLatteExtension extends Extension
{
	/**
	 * Επιστρέφει τη λίστα των tags που παρέχονται από αυτή την επέκταση.
	 * @return array<string, callable> Χάρτης: 'tag-name' => parsing-function
	 */
	public function getTags(): array
	{
		return [
			'datetime' => DatetimeNode::create(...),
			// Αργότερα καταχωρήστε περισσότερα tags εδώ
		];
	}
}

Στη συνέχεια, καταχωρήστε αυτή την επέκταση στο Latte Engine:

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

Δημιουργήστε ένα template:

<p>Η σελίδα δημιουργήθηκε: {datetime}</p>

Αναμενόμενη έξοδος: <p>Η σελίδα δημιουργήθηκε: 2023-10-27 11:00:00</p>

Σύνοψη αυτής της φάσης

Δημιουργήσαμε με επιτυχία ένα βασικό προσαρμοσμένο tag {datetime}. Ορίσαμε την αναπαράστασή του στο AST (DatetimeNode), χειριστήκαμε την ανάλυσή του (create()), καθορίσαμε πώς θα πρέπει να δημιουργεί κώδικα PHP (print()), διασφαλίσαμε ότι τα παιδιά του είναι προσβάσιμα για διέλευση (getIterator()) και το καταχωρήσαμε στο Latte.

Στην επόμενη ενότητα, θα βελτιώσουμε αυτό το tag ώστε να δέχεται arguments και θα δείξουμε πώς να αναλύουμε εκφράσεις και να διαχειριζόμαστε παιδικούς κόμβους.

Ανάλυση των arguments του tag

Το απλό μας tag {datetime} λειτουργεί, αλλά δεν είναι πολύ ευέλικτο. Ας το βελτιώσουμε ώστε να δέχεται ένα προαιρετικό argument: μια συμβολοσειρά μορφοποίησης για τη συνάρτηση date(). Η απαιτούμενη σύνταξη θα είναι {datetime $format}.

Στόχος: Τροποποίηση του {datetime} ώστε να δέχεται μια προαιρετική έκφραση PHP ως argument, η οποία θα χρησιμοποιηθεί ως συμβολοσειρά μορφοποίησης για το date().

Εισαγωγή του TagParser

Πριν τροποποιήσουμε τον κώδικα, είναι σημαντικό να κατανοήσουμε το εργαλείο που θα χρησιμοποιήσουμε: Latte\Compiler\TagParser. Όταν ο κύριος parser του Latte (TemplateParser) συναντήσει ένα Latte tag όπως {datetime ...} ή ένα n:attribute, αναθέτει την ανάλυση του περιεχομένου εντός του tag (το τμήμα μεταξύ { και } ή η τιμή του attribute) σε έναν εξειδικευμένο TagParser.

Αυτός ο TagParser λειτουργεί αποκλειστικά με τα arguments του tag. Ο ρόλος του είναι να επεξεργάζεται τα tokens που αντιπροσωπεύουν αυτά τα arguments. Κλειδί είναι ότι πρέπει να επεξεργαστεί ολόκληρο το περιεχόμενο που του παρέχεται. Εάν η συνάρτηση ανάλυσής σας τελειώσει, αλλά ο TagParser δεν έχει φτάσει στο τέλος των arguments (ελέγχεται μέσω του $tag->parser->isEnd()), το Latte θα πετάξει μια εξαίρεση, καθώς αυτό υποδεικνύει ότι απέμειναν μη αναμενόμενα tokens εντός του tag. Αντίθετα, εάν το tag απαιτεί arguments, θα πρέπει να καλέσετε το $tag->expectArguments() στην αρχή της συνάρτησης ανάλυσής σας. Αυτή η μέθοδος ελέγχει εάν υπάρχουν arguments και πετάει μια χρήσιμη εξαίρεση εάν το tag χρησιμοποιήθηκε χωρίς κανένα argument.

Ο TagParser προσφέρει χρήσιμες μεθόδους για την ανάλυση διαφόρων ειδών arguments:

  • parseExpression(): ExpressionNode: Αναλύει μια PHP-like έκφραση (μεταβλητές, literals, τελεστές, κλήσεις συναρτήσεων/μεθόδων, κ.λπ.). Χειρίζεται το συντακτικό ζάχαρο του Latte, όπως η μεταχείριση απλών αλφαριθμητικών συμβολοσειρών ως συμβολοσειρές σε εισαγωγικά (π.χ. το foo αναλύεται σαν να ήταν 'foo').
  • parseUnquotedStringOrExpression(): ExpressionNode: Αναλύει είτε μια τυπική έκφραση είτε μια συμβολοσειρά χωρίς εισαγωγικά. Οι συμβολοσειρές χωρίς εισαγωγικά είναι ακολουθίες που επιτρέπονται από το Latte χωρίς εισαγωγικά, συχνά χρησιμοποιούμενες για πράγματα όπως διαδρομές αρχείων (π.χ. {include ../file.latte}). Εάν αναλύσει μια συμβολοσειρά χωρίς εισαγωγικά, επιστρέφει StringNode.
  • parseArguments(): ArrayNode: Αναλύει arguments διαχωρισμένα με κόμμα, πιθανώς με κλειδιά, όπως 10, name: 'John', true.
  • parseModifier(): ModifierNode: Αναλύει φίλτρα όπως |upper|truncate:10.
  • parseType(): ?SuperiorTypeNode: Αναλύει υποδείξεις τύπου PHP όπως int, ?string, array|Foo.

Για πιο σύνθετες ή χαμηλού επιπέδου ανάγκες ανάλυσης, μπορείτε να αλληλεπιδράσετε απευθείας με τη ροή token μέσω του $tag->parser->stream. Αυτό το αντικείμενο παρέχει μεθόδους για τον έλεγχο και την επεξεργασία μεμονωμένων tokens:

  • $tag->parser->stream->is(...): bool: Ελέγχει εάν το τρέχον token αντιστοιχεί σε κάποιον από τους καθορισμένους τύπους (π.χ. Token::Php_Variable) ή literal τιμές (π.χ. 'as') χωρίς να το καταναλώσει. Χρήσιμο για να κοιτάξετε μπροστά.
  • $tag->parser->stream->consume(...): Token: Καταναλώνει το τρέχον token και προωθεί τη θέση της ροής. Εάν παρέχονται αναμενόμενοι τύποι/τιμές token ως arguments και το τρέχον token δεν ταιριάζει, πετάει CompileException. Χρησιμοποιήστε το όταν αναμένετε ένα συγκεκριμένο token.
  • $tag->parser->stream->tryConsume(...): ?Token: Προσπαθεί να καταναλώσει το τρέχον token μόνο εάν ταιριάζει με έναν από τους καθορισμένους τύπους/τιμές. Εάν ταιριάζει, καταναλώνει το token και το επιστρέφει. Εάν δεν ταιριάζει, αφήνει τη θέση της ροής αμετάβλητη και επιστρέφει null. Χρησιμοποιήστε το για προαιρετικά tokens ή όταν επιλέγετε μεταξύ διαφορετικών συντακτικών διαδρομών.

Ενημέρωση της συνάρτησης ανάλυσης create()

Με αυτή την κατανόηση, ας τροποποιήσουμε τη μέθοδο create() στο DatetimeNode ώστε να αναλύει το προαιρετικό argument μορφοποίησης χρησιμοποιώντας το $tag->parser.

<?php

namespace App\Latte;

use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class DatetimeNode extends StatementNode
{
	// Προσθέτουμε μια δημόσια ιδιότητα για να κρατήσουμε τον αναλυμένο κόμβο της έκφρασης μορφοποίησης
	public ?ExpressionNode $format = null;

	public static function create(Tag $tag): self
	{
		$node = $tag->node = new self;

		// Ελέγχουμε αν υπάρχουν κάποια tokens
		if (!$tag->parser->isEnd()) {
			// Αναλύουμε το argument ως μια PHP-like έκφραση χρησιμοποιώντας το TagParser.
			$node->format = $tag->parser->parseExpression();
		}

		return $node;
	}

	// ... οι μέθοδοι print() και getIterator() θα ενημερωθούν παρακάτω ...
}

Προσθέσαμε τη δημόσια ιδιότητα $format. Στο create(), χρησιμοποιούμε τώρα το $tag->parser->isEnd() για να ελέγξουμε αν υπάρχουν arguments. Αν ναι, το $tag->parser->parseExpression() επεξεργάζεται τα tokens για την έκφραση. Επειδή ο TagParser πρέπει να επεξεργαστεί όλα τα tokens εισόδου, το Latte θα πετάξει αυτόματα ένα σφάλμα εάν ο χρήστης γράψει κάτι μη αναμενόμενο μετά την έκφραση μορφοποίησης (π.χ. {datetime 'Y-m-d', unexpected}).

Ενημέρωση της μεθόδου print()

Τώρα, ας τροποποιήσουμε τη μέθοδο print() ώστε να χρησιμοποιεί την αναλυμένη έκφραση μορφοποίησης που είναι αποθηκευμένη στο $this->format. Εάν δεν παρασχέθηκε μορφοποίηση ($this->format είναι null), θα πρέπει να χρησιμοποιήσουμε μια προεπιλεγμένη συμβολοσειρά μορφοποίησης, για παράδειγμα 'Y-m-d H:i:s'.

	public function print(PrintContext $context): string
	{
		$formatNode = $this->format ?? new StringNode('Y-m-d H:i:s');

		// Το %node εκτυπώνει την αναπαράσταση κώδικα PHP του $formatNode.
		return $context->format(
			'echo date(%node) %line;',
			$formatNode,
			$this->position
		);
	}

Στη μεταβλητή $formatNode αποθηκεύουμε τον κόμβο AST που αντιπροσωπεύει τη συμβολοσειρά μορφοποίησης για τη συνάρτηση PHP date(). Χρησιμοποιούμε εδώ τον τελεστή null coalescing (??). Εάν ο χρήστης παρείχε ένα argument στο template (π.χ. {datetime 'd.m.Y'}), τότε η ιδιότητα $this->format περιέχει τον αντίστοιχο κόμβο (σε αυτή την περίπτωση ένα StringNode με τιμή 'd.m.Y'), και αυτός ο κόμβος χρησιμοποιείται. Εάν ο χρήστης δεν παρείχε argument (έγραψε απλώς {datetime}), η ιδιότητα $this->format είναι null, και αντ' αυτού δημιουργούμε ένα νέο StringNode με την προεπιλεγμένη μορφοποίηση 'Y-m-d H:i:s'. Αυτό διασφαλίζει ότι το $formatNode περιέχει πάντα έναν έγκυρο κόμβο AST για τη μορφοποίηση.

Στη μάσκα 'echo date(%node) %line;' χρησιμοποιείται ο νέος placeholder %node, ο οποίος λέει στη μέθοδο format() να πάρει το πρώτο επόμενο argument (που είναι το $formatNode μας), να καλέσει τη μέθοδό του print() (η οποία θα επιστρέψει την αναπαράσταση κώδικα PHP του) και να εισάγει το αποτέλεσμα στη θέση του placeholder.

Υλοποίηση του getIterator() για υποκόμβους

Το DatetimeNode μας έχει τώρα έναν παιδικό κόμβο: την έκφραση $format. Πρέπει να κάνουμε αυτόν τον παιδικό κόμβο προσβάσιμο στα compilation passes παρέχοντάς τον στη μέθοδο getIterator(). Μην ξεχάσετε να παρέχετε μια αναφορά (&) για να επιτρέψετε στα passes να αντικαταστήσουν πιθανώς τον κόμβο.

	public function &getIterator(): \Generator
	{
		if ($this->format) {
			yield $this->format;
		}
	}

Γιατί είναι αυτό θεμελιώδες; Φανταστείτε ένα Sandbox pass που χρειάζεται να ελέγξει αν το argument $format περιέχει μια απαγορευμένη κλήση συνάρτησης (π.χ. {datetime dangerousFunction()}). Εάν το getIterator() δεν παρέχει το $this->format, το Sandbox pass δεν θα έβλεπε ποτέ την κλήση dangerousFunction() μέσα στο argument του tag μας, δημιουργώντας ένα πιθανό κενό ασφαλείας. Παρέχοντάς το, επιτρέπουμε στο Sandbox (και σε άλλα passes) να ελέγχουν και πιθανώς να τροποποιούν τον κόμβο έκφρασης $format.

Χρήση του βελτιωμένου tag

Το tag τώρα χειρίζεται σωστά το προαιρετικό argument:

Προεπιλεγμένη μορφοποίηση: {datetime}
Προσαρμοσμένη μορφοποίηση: {datetime 'd.m.Y'}
Χρήση μεταβλητής: {datetime $userDateFormatPreference}

{* Αυτό θα προκαλούσε σφάλμα μετά την ανάλυση του 'd.m.Y', επειδή το ", foo" είναι μη αναμενόμενο *}
{* {datetime 'd.m.Y', foo} *}

Στη συνέχεια, θα δούμε τη δημιουργία paired tags που επεξεργάζονται το περιεχόμενο μεταξύ τους.

Χειρισμός paired tags

Μέχρι τώρα, το tag μας {datetime} ήταν self-closing (εννοιολογικά). Δεν είχε περιεχόμενο μεταξύ του αρχικού και του τελικού tag. Ωστόσο, πολλά χρήσιμα tags λειτουργούν με ένα μπλοκ περιεχομένου template. Αυτά ονομάζονται paired tags. Παραδείγματα περιλαμβάνουν {if}...{/if}, {block}...{/block} ή ένα προσαρμοσμένο tag που θα δημιουργήσουμε τώρα: {debug}...{/debug}.

Αυτό το tag θα μας επιτρέψει να συμπεριλάβουμε πληροφορίες debugging στα templates μας, οι οποίες θα πρέπει να είναι ορατές μόνο κατά τη διάρκεια της ανάπτυξης.

Στόχος: Δημιουργία ενός paired tag {debug}, του οποίου το περιεχόμενο αποδίδεται μόνο όταν είναι ενεργό ένα συγκεκριμένο flag “development mode”.

Εισαγωγή των providers

Μερικές φορές τα tags σας χρειάζονται πρόσβαση σε δεδομένα ή υπηρεσίες που δεν περνούν απευθείας ως παράμετροι του template. Για παράδειγμα, ο προσδιορισμός εάν η εφαρμογή βρίσκεται σε development mode, η πρόσβαση στο αντικείμενο χρήστη ή η λήψη τιμών διαμόρφωσης. Το Latte παρέχει έναν μηχανισμό που ονομάζεται Providers για αυτόν τον σκοπό.

Οι Providers καταχωρούνται στην επέκτασή σας χρησιμοποιώντας τη μέθοδο getProviders(). Αυτή η μέθοδος επιστρέφει έναν συσχετιστικό πίνακα, όπου τα κλειδιά είναι τα ονόματα με τα οποία οι providers θα είναι προσβάσιμοι στον runtime κώδικα του template, και οι τιμές είναι τα πραγματικά δεδομένα ή αντικείμενα.

Μέσα στον κώδικα PHP που δημιουργείται από τη μέθοδο print() του tag σας, μπορείτε να αποκτήσετε πρόσβαση σε αυτούς τους providers μέσω της ειδικής ιδιότητας του αντικειμένου $this->global. Επειδή αυτή η ιδιότητα μοιράζεται σε όλες τις επεκτάσεις, είναι καλή πρακτική να προσθέτετε πρόθεμα στα ονόματα των providers σας για να αποφύγετε πιθανές συγκρούσεις ονομάτων με βασικούς providers του Latte ή providers από άλλες επεκτάσεις τρίτων. Μια κοινή σύμβαση είναι η χρήση ενός σύντομου, μοναδικού προθέματος που σχετίζεται με τον κατασκευαστή σας ή το όνομα της επέκτασης. Για το παράδειγμά μας, θα χρησιμοποιήσουμε το πρόθεμα app και το flag development mode θα είναι διαθέσιμο ως $this->global->appDevMode.

Η λέξη-κλειδί yield για την ανάλυση περιεχομένου

Πώς λέμε στον parser του Latte να επεξεργαστεί το περιεχόμενο μεταξύ του {debug} και του {/debug}; Εδώ μπαίνει στο παιχνίδι η λέξη-κλειδί yield.

Όταν το yield χρησιμοποιείται στη συνάρτηση create(), η συνάρτηση γίνεται PHP generator. Η εκτέλεσή της αναστέλλεται και ο έλεγχος επιστρέφει στον κύριο TemplateParser. Ο TemplateParser στη συνέχεια συνεχίζει να αναλύει το περιεχόμενο του template μέχρι να συναντήσει το αντίστοιχο tag κλεισίματος ({/debug} στην περίπτωσή μας).

Μόλις βρεθεί το tag κλεισίματος, ο TemplateParser συνεχίζει την εκτέλεση της συνάρτησης create() μας ακριβώς μετά την εντολή yield. Η τιμή που επιστρέφεται από την εντολή yield είναι ένας πίνακας που περιέχει δύο στοιχεία:

  1. Ένα AreaNode που αντιπροσωπεύει το αναλυμένο περιεχόμενο μεταξύ του αρχικού και του τελικού tag.
  2. Ένα αντικείμενο Tag που αντιπροσωπεύει το tag κλεισίματος (π.χ. {/debug}).

Ας δημιουργήσουμε την κλάση DebugNode και τη μέθοδό της create που χρησιμοποιεί το yield.

<?php

namespace App\Latte;

use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class DebugNode extends StatementNode
{
	// Δημόσια ιδιότητα για να κρατήσει το αναλυμένο εσωτερικό περιεχόμενο
	public AreaNode $content;

	/**
	 * Συνάρτηση ανάλυσης για το paired tag {debug} ... {/debug}.
	 */
	public static function create(Tag $tag): \Generator // παρατηρήστε τον τύπο επιστροφής
	{
		$node = $tag->node = new self;

		// Αναστολή ανάλυσης, λήψη εσωτερικού περιεχομένου και τελικού tag όταν βρεθεί το {/debug}
		[$node->content, $endTag] = yield;

		return $node;
	}

	// ... οι print() και getIterator() θα υλοποιηθούν παρακάτω ...
}

Σημείωση: Το $endTag είναι null εάν το tag χρησιμοποιείται ως n:attribute, δηλαδή <div n:debug>...</div>.

Υλοποίηση του print() για υπό συνθήκη απόδοση

Η μέθοδος print() χρειάζεται τώρα να δημιουργήσει κώδικα PHP ο οποίος κατά το runtime θα ελέγξει τον provider appDevMode και θα εκτελέσει τον κώδικα για το εσωτερικό περιεχόμενο μόνο εάν το flag είναι true.

	public function print(PrintContext $context): string
	{
		// Δημιουργεί μια εντολή PHP 'if' που ελέγχει τον provider κατά το runtime
		return $context->format(
			<<<'XX'
				if ($this->global->appDevMode) %line {
					// Εάν είναι σε development mode, εκτυπώνει το εσωτερικό περιεχόμενο
					%node
				}

				XX,
			$this->position, // Για το σχόλιο %line
			$this->content,  // Ο κόμβος που περιέχει το AST του εσωτερικού περιεχομένου
		);
	}

Αυτό είναι απλό. Χρησιμοποιούμε το PrintContext::format() για να δημιουργήσουμε μια τυπική εντολή PHP if. Μέσα στο if τοποθετούμε τον placeholder %node για το $this->content. Το Latte θα καλέσει αναδρομικά το $this->content->print($context) για να δημιουργήσει τον κώδικα PHP για το εσωτερικό τμήμα του tag, αλλά μόνο εάν το $this->global->appDevMode αξιολογηθεί ως true κατά το runtime.

Υλοποίηση του getIterator() για το περιεχόμενο

Όπως και με τον κόμβο argument στο προηγούμενο παράδειγμα, το DebugNode μας έχει τώρα έναν παιδικό κόμβο: AreaNode $content. Πρέπει να τον κάνουμε προσβάσιμο παρέχοντάς τον στο getIterator():

	public function &getIterator(): \Generator
	{
		// Παρέχει αναφορά στον κόμβο περιεχομένου
		yield $this->content;
	}

Αυτό επιτρέπει στα compilation passes να κατέβουν στο περιεχόμενο του tag μας {debug}, το οποίο είναι σημαντικό ακόμα και αν το περιεχόμενο αποδίδεται υπό συνθήκη. Για παράδειγμα, το Sandbox χρειάζεται να αναλύσει το περιεχόμενο ανεξάρτητα από το αν το appDevMode είναι true ή false.

Εγγραφή και χρήση

Καταχωρήστε το tag και τον provider στην επέκτασή σας:

class MyLatteExtension extends Extension
{
	// Υποθέτουμε ότι το $isDevelopmentMode καθορίζεται κάπου (π.χ. από τη διαμόρφωση)
	public function __construct(
		private bool $isDevelopmentMode,
	) {
	}

	public function getTags(): array
	{
		return [
			'datetime' => DatetimeNode::create(...),
			'debug' => DebugNode::create(...), // Εγγραφή του νέου tag
		];
	}

	public function getProviders(): array
	{
		return [
			'appDevMode' => $this->isDevelopmentMode, // Εγγραφή του provider
		];
	}
}

// Κατά την εγγραφή της επέκτασης:
$isDev = true; // Καθορίστε αυτό βάσει του περιβάλλοντος της εφαρμογής σας
$latte->addExtension(new App\Latte\MyLatteExtension($isDev));

Και η χρήση του στο template:

<p>Κανονικό περιεχόμενο ορατό πάντα.</p>

{debug}
	<div class="debug-panel">
		ID τρέχοντος χρήστη: {$user->id}
		Χρόνος αιτήματος: {=time()}
	</div>
{/debug}

<p>Άλλο κανονικό περιεχόμενο.</p>

Ενσωμάτωση n:attributes

Το Latte προσφέρει μια βολική συντομογραφία για πολλά paired tags: n:attributes. Εάν έχετε ένα paired tag όπως {tag}...{/tag} και θέλετε το αποτέλεσμά του να εφαρμοστεί απευθείας σε ένα μόνο HTML element, μπορείτε συχνά να το γράψετε πιο συνοπτικά ως attribute n:tag σε αυτό το element.

Για τα περισσότερα τυπικά paired tags που ορίζετε (όπως το δικό μας {debug}), το Latte θα ενεργοποιήσει αυτόματα την αντίστοιχη έκδοση n: attribute. Δεν χρειάζεται να κάνετε τίποτα επιπλέον κατά την εγγραφή:

{* Τυπική χρήση του paired tag *}
{debug}<div>Πληροφορίες για debugging</div>{/debug}

{* Ισοδύναμη χρήση με n:attribute *}
<div n:debug>Πληροφορίες για debugging</div>

Και οι δύο εκδόσεις θα αποδώσουν το <div> μόνο εάν το $this->global->appDevMode είναι true. Τα προθέματα inner- και tag- λειτουργούν επίσης όπως αναμένεται.

Μερικές φορές, η λογική του tag σας μπορεί να χρειαστεί να συμπεριφερθεί ελαφρώς διαφορετικά ανάλογα με το αν χρησιμοποιείται ως τυπικό paired tag ή ως n:attribute, ή αν χρησιμοποιείται ένα πρόθεμα όπως n:inner-tag ή n:tag-tag. Το αντικείμενο Latte\Compiler\Tag, που περνά στη συνάρτηση ανάλυσής σας create(), παρέχει αυτές τις πληροφορίες:

  • $tag->isNAttribute(): bool: Επιστρέφει true εάν το tag αναλύεται ως n:attribute
  • $tag->prefix: ?string: Επιστρέφει το πρόθεμα που χρησιμοποιείται με το n:attribute, το οποίο μπορεί να είναι null (όχι n:attribute), Tag::PrefixNone, Tag::PrefixInner ή Tag::PrefixTag

Τώρα που κατανοούμε τα απλά tags, την ανάλυση arguments, τα paired tags, τους providers και τα n:attributes, ας ασχοληθούμε με ένα πιο περίπλοκο σενάριο που περιλαμβάνει tags ένθετα σε άλλα tags, χρησιμοποιώντας το tag μας {debug} ως σημείο εκκίνησης.

Intermediate tags

Ορισμένα paired tags επιτρέπουν ή ακόμη και απαιτούν την εμφάνιση άλλων tags εντός τους πριν από το τελικό tag κλεισίματος. Αυτά ονομάζονται intermediate tags. Κλασικά παραδείγματα περιλαμβάνουν {if}...{elseif}...{else}...{/if} ή {switch}...{case}...{default}...{/switch}.

Ας επεκτείνουμε το tag μας {debug} για να υποστηρίξουμε μια προαιρετική ρήτρα {else}, η οποία θα αποδίδεται όταν η εφαρμογή δεν βρίσκεται σε development mode.

Στόχος: Τροποποίηση του {debug} ώστε να υποστηρίζει ένα προαιρετικό intermediate tag {else}. Η τελική σύνταξη θα πρέπει να είναι {debug} ... {else} ... {/debug}.

Ανάλυση intermediate tags με το yield

Γνωρίζουμε ήδη ότι το yield αναστέλλει τη συνάρτηση ανάλυσης create() και επιστρέφει το αναλυμένο περιεχόμενο μαζί με το tag κλεισίματος. Ωστόσο, το yield προσφέρει περισσότερο έλεγχο: μπορείτε να του παρέχετε έναν πίνακα με ονόματα intermediate tags. Όταν ο parser συναντήσει οποιοδήποτε από αυτά τα καθορισμένα tags στο ίδιο επίπεδο ένθεσης (δηλαδή, ως άμεσα παιδιά του γονικού tag, όχι μέσα σε άλλα μπλοκ ή tags εντός του), σταματά επίσης την ανάλυση.

Όταν η ανάλυση σταματήσει λόγω ενός intermediate tag, σταματά την ανάλυση του περιεχομένου, συνεχίζει τον generator create() και επιστρέφει το μερικώς αναλυμένο περιεχόμενο και το ίδιο το intermediate tag (αντί για το τελικό tag κλεισίματος). Η συνάρτηση create() μας μπορεί στη συνέχεια να επεξεργαστεί αυτό το intermediate tag (π.χ. να αναλύσει τα arguments του, αν είχε) και να χρησιμοποιήσει ξανά το yield για να αναλύσει το επόμενο τμήμα περιεχομένου μέχρι το τελικό tag κλεισίματος ή ένα άλλο αναμενόμενο intermediate tag.

Ας τροποποιήσουμε το DebugNode::create() ώστε να αναμένει το {else}:

<?php

namespace App\Latte;

use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\NopNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class DebugNode extends StatementNode
{
	// Περιεχόμενο για το τμήμα {debug}
	public AreaNode $thenContent;
	// Προαιρετικό περιεχόμενο για το τμήμα {else}
	public ?AreaNode $elseContent = null;

	public static function create(Tag $tag): \Generator
	{
		$node = $tag->node = new self;

		// yield και αναμονή είτε για {/debug} είτε για {else}
		[$node->thenContent, $nextTag] = yield ['else'];

		// Έλεγχος αν το tag στο οποίο σταματήσαμε ήταν το {else}
		if ($nextTag?->name === 'else') {
			// Yield ξανά για ανάλυση του περιεχομένου μεταξύ {else} και {/debug}
			[$node->elseContent, $endTag] = yield;
		}

		return $node;
	}

	// ... οι print() και getIterator() θα ενημερωθούν παρακάτω ...
}

Τώρα το yield ['else'] λέει στο Latte να σταματήσει την ανάλυση όχι μόνο για το {/debug}, αλλά και για το {else}. Εάν βρεθεί το {else}, το $nextTag θα περιέχει το αντικείμενο Tag για το {else}. Στη συνέχεια, χρησιμοποιούμε ξανά το yield χωρίς arguments, πράγμα που σημαίνει ότι τώρα αναμένουμε μόνο το τελικό tag {/debug}, και αποθηκεύουμε το αποτέλεσμα στο $node->elseContent. Εάν το {else} δεν βρέθηκε, το $nextTag θα ήταν το Tag για το {/debug}null εάν χρησιμοποιείται ως n:attribute) και το $node->elseContent θα παρέμενε null.

Υλοποίηση του print() με {else}

Η μέθοδος print() χρειάζεται να αντικατοπτρίζει τη νέα δομή. Θα πρέπει να δημιουργήσει μια εντολή PHP if/else βασισμένη στον provider devMode.

	public function print(PrintContext $context): string
	{
		return $context->format(
			<<<'XX'
				if ($this->global->appDevMode) %line {
					%node // Κώδικας για τον κλάδο 'then' (περιεχόμενο {debug})
				} else {
					%node // Κώδικας για τον κλάδο 'else' (περιεχόμενο {else})
				}

				XX,
			$this->position,    // Αριθμός γραμμής για τη συνθήκη 'if'
			$this->thenContent, // Πρώτος placeholder %node
			$this->elseContent ?? new NopNode, // Δεύτερος placeholder %node
		);
	}

Αυτή είναι μια τυπική δομή PHP if/else. Χρησιμοποιούμε το %node δύο φορές. Το format() αντικαθιστά τους παρεχόμενους κόμβους διαδοχικά. Χρησιμοποιούμε το ?? new NopNode για να αποφύγουμε σφάλματα εάν το $this->elseContent είναι null – το NopNode απλά δεν εκτυπώνει τίποτα.

Υλοποίηση του getIterator() και για τα δύο περιεχόμενα

Τώρα έχουμε πιθανώς δύο παιδικούς κόμβους περιεχομένου ($thenContent και $elseContent). Πρέπει να παρέχουμε και τους δύο, εάν υπάρχουν:

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

Χρήση του βελτιωμένου tag

Το tag μπορεί τώρα να χρησιμοποιηθεί με την προαιρετική ρήτρα {else}:

{debug}
	<p>Εμφάνιση πληροφοριών debugging, επειδή το devMode είναι ΕΝΕΡΓΟ.</p>
{else}
	<p>Οι πληροφορίες debugging είναι κρυμμένες, επειδή το devMode είναι ΑΠΕΝΕΡΓΟΠΟΙΗΜΕΝΟ.</p>
{/debug}

Χειρισμός κατάστασης και ένθεσης

Τα προηγούμενα παραδείγματά μας ({datetime}, {debug}) ήταν σχετικά stateless εντός των μεθόδων τους print(). Είτε εξέδιδαν απευθείας περιεχόμενο είτε πραγματοποιούσαν έναν απλό έλεγχο υπό συνθήκη βασισμένο σε έναν global provider. Ωστόσο, πολλά tags χρειάζεται να διαχειριστούν κάποια μορφή κατάστασης κατά την απόδοση ή περιλαμβάνουν την αξιολόγηση εκφράσεων χρήστη που θα πρέπει να εκτελεστούν μόνο μία φορά για λόγους απόδοσης ή ορθότητας. Επιπλέον, πρέπει να εξετάσουμε τι συμβαίνει όταν τα προσαρμοσμένα tags μας είναι ένθετα.

Ας απεικονίσουμε αυτές τις έννοιες δημιουργώντας ένα tag {repeat $count}...{/repeat}. Αυτό το tag θα επαναλάβει το εσωτερικό του περιεχόμενο $count φορές.

Στόχος: Υλοποίηση του {repeat $count}, το οποίο επαναλαμβάνει το περιεχόμενό του τον καθορισμένο αριθμό φορών.

Η ανάγκη για προσωρινές & μοναδικές μεταβλητές

Φανταστείτε ότι ο χρήστης γράφει:

{repeat rand(1, 5)} Περιεχόμενο {/repeat}

Αν αφελώς δημιουργούσαμε έναν βρόχο PHP for με αυτόν τον τρόπο στη μέθοδό μας print():

// Απλοποιημένος, ΛΑΝΘΑΣΜΕΝΟΣ παραγόμενος κώδικας
for ($i = 0; $i < rand(1, 5); $i++) {
	// εκτύπωση περιεχομένου
}

Αυτό θα ήταν λάθος! Η έκφραση rand(1, 5) θα επαναξιολογούνταν σε κάθε επανάληψη του βρόχου, οδηγώντας σε απρόβλεπτο αριθμό επαναλήψεων. Πρέπει να αξιολογήσουμε την έκφραση $count μία φορά πριν ξεκινήσει ο βρόχος και να αποθηκεύσουμε το αποτέλεσμά της.

Θα δημιουργήσουμε κώδικα PHP που πρώτα αξιολογεί την έκφραση του αριθμού και την αποθηκεύει σε μια προσωρινή μεταβλητή runtime. Για να αποφύγουμε συγκρούσεις με μεταβλητές που ορίζονται από τον χρήστη του template και εσωτερικές μεταβλητές του Latte (όπως $ʟ_...), θα χρησιμοποιήσουμε τη σύμβαση του προθέματος $__ (διπλή κάτω παύλα) για τις προσωρινές μας μεταβλητές.

Ο παραγόμενος κώδικας θα έμοιαζε τότε κάπως έτσι:

$__count = rand(1, 5);
for ($__i = 0; $__i < $__count; $__i++) {
	// εκτύπωση περιεχομένου
}

Τώρα, ας εξετάσουμε την ένθεση:

{repeat $countA}       {* Εξωτερικός βρόχος *}
	{repeat $countB}   {* Εσωτερικός βρόχος *}
		...
	{/repeat}
{/repeat}

Αν τόσο το εξωτερικό όσο και το εσωτερικό tag {repeat} δημιουργούσαν κώδικα χρησιμοποιώντας τα ίδια ονόματα προσωρινών μεταβλητών (π.χ. $__count και $__i), ο εσωτερικός βρόχος θα αντικαθιστούσε τις μεταβλητές του εξωτερικού βρόχου, καταστρέφοντας τη λογική.

Πρέπει να διασφαλίσουμε ότι οι προσωρινές μεταβλητές που δημιουργούνται για κάθε παρουσία του tag {repeat} είναι μοναδικές. Αυτό το επιτυγχάνουμε χρησιμοποιώντας το PrintContext::generateId(). Αυτή η μέθοδος επιστρέφει έναν μοναδικό ακέραιο κατά τη φάση της μεταγλώττισης. Μπορούμε να προσαρτήσουμε αυτό το ID στα ονόματα των προσωρινών μας μεταβλητών.

Έτσι, αντί για $__count, θα δημιουργήσουμε $__count_1 για το πρώτο tag repeat, $__count_2 για το δεύτερο, κ.ο.κ. Παρομοίως, για τον μετρητή του βρόχου, θα χρησιμοποιήσουμε $__i_1, $__i_2, κ.ο.κ.

Υλοποίηση του RepeatNode

Ας δημιουργήσουμε την κλάση κόμβου.

<?php

namespace App\Latte;

use Latte\CompileException;
use Latte\Compiler\Nodes\AreaNode;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;

class RepeatNode extends StatementNode
{
	public ExpressionNode $count;
	public AreaNode $content;

	/**
	 * Συνάρτηση ανάλυσης για {repeat $count} ... {/repeat}
	 */
	public static function create(Tag $tag): \Generator
	{
		$tag->expectArguments(); // βεβαιώνεται ότι το $count παρέχεται
		$node = $tag->node = new self;
		// Αναλύει την έκφραση του αριθμού
		$node->count = $tag->parser->parseExpression();
		// Λήψη του εσωτερικού περιεχομένου
		[$node->content] = yield;
		return $node;
	}

	/**
	 * Δημιουργεί έναν βρόχο PHP 'for' με μοναδικά ονόματα μεταβλητών.
	 */
	public function print(PrintContext $context): string
	{
		// Δημιουργία μοναδικών ονομάτων μεταβλητών
		$id = $context->generateId();
		$countVar = '$__count_' . $id; // π.χ. $__count_1, $__count_2, κ.λπ.
		$iteratorVar = '$__i_' . $id;  // π.χ. $__i_1, $__i_2, κ.λπ.

		return $context->format(
			<<<'XX'
				// Αξιολόγηση της έκφρασης του αριθμού *μία φορά* και αποθήκευση
				%raw = (int) (%node);
				// Βρόχος με χρήση του αποθηκευμένου αριθμού και μοναδικής μεταβλητής επανάληψης
				for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line {
					%node // Απόδοση του εσωτερικού περιεχομένου
				}

				XX,
			$countVar,          // %0 - Μεταβλητή για αποθήκευση του αριθμού
			$this->count,       // %1 - Κόμβος έκφρασης για τον αριθμό
			$iteratorVar,       // %2 - Όνομα μεταβλητής επανάληψης βρόχου
			$this->position,    // %3 - Σχόλιο με αριθμό γραμμής για τον ίδιο τον βρόχο
			$this->content      // %4 - Κόμβος εσωτερικού περιεχομένου
		);
	}

	/**
	 * Παρέχει τους παιδικούς κόμβους (έκφραση αριθμού και περιεχόμενο).
	 */
	public function &getIterator(): \Generator
	{
		yield $this->count;
		yield $this->content;
	}
}

Η μέθοδος create() αναλύει την απαιτούμενη έκφραση $count χρησιμοποιώντας το parseExpression(). Πρώτα καλείται το $tag->expectArguments(). Αυτό διασφαλίζει ότι ο χρήστης παρείχε κάτι μετά το {repeat}. Ενώ το $tag->parser->parseExpression() θα αποτύγχανε αν δεν παρεχόταν τίποτα, το μήνυμα σφάλματος θα μπορούσε να αφορά μη αναμενόμενη σύνταξη. Η χρήση του expectArguments() παρέχει ένα πολύ σαφέστερο σφάλμα, δηλώνοντας συγκεκριμένα ότι λείπουν arguments για το tag {repeat}.

Η μέθοδος print() δημιουργεί τον κώδικα PHP που είναι υπεύθυνος για την εκτέλεση της λογικής επανάληψης κατά το runtime. Ξεκινά δημιουργώντας μοναδικά ονόματα για τις προσωρινές μεταβλητές PHP που θα χρειαστεί.

Η μέθοδος $context->format() καλείται με τον νέο placeholder %raw, ο οποίος εισάγει την ακατέργαστη συμβολοσειρά που παρέχεται ως το αντίστοιχο argument. Εδώ, εισάγει το μοναδικό όνομα μεταβλητής που είναι αποθηκευμένο στο $countVar (π.χ. $__count_1). Και τι γίνεται με τα %0.raw και %2.raw; Αυτό επιδεικνύει τους positional placeholders. Αντί για απλώς %raw, που παίρνει το επόμενο διαθέσιμο raw argument, το %2.raw παίρνει ρητά το argument στο index 2 (που είναι το $iteratorVar) και εισάγει την ακατέργαστη τιμή συμβολοσειράς του. Αυτό μας επιτρέπει να επαναχρησιμοποιήσουμε τη συμβολοσειρά $iteratorVar χωρίς να την περάσουμε πολλές φορές στη λίστα arguments του format().

Αυτή η προσεκτικά κατασκευασμένη κλήση format() δημιουργεί έναν αποδοτικό και ασφαλή βρόχο PHP που χειρίζεται σωστά την έκφραση του αριθμού και αποφεύγει τις συγκρούσεις ονομάτων μεταβλητών ακόμα και όταν τα tags {repeat} είναι ένθετα.

Εγγραφή και χρήση

Καταχωρήστε το tag στην επέκτασή σας:

use App\Latte\RepeatNode;

class MyLatteExtension extends Extension
{
	public function getTags(): array
	{
		return [
			'datetime' => DatetimeNode::create(...),
			'debug' => DebugNode::create(...),
			'repeat' => RepeatNode::create(...), // Εγγραφή του tag repeat
		];
	}
}

Χρησιμοποιήστε το στο template, συμπεριλαμβανομένης της ένθεσης:

{var $rows = rand(5, 7)}
{var $cols = rand(3, 5)}

{repeat $rows}
	<tr>
		{repeat $cols}
			<td>Εσωτερικός βρόχος</td>
		{/repeat}
	</tr>
{/repeat}

Αυτό το παράδειγμα δείχνει πώς να χειριστείτε την κατάσταση (μετρητές βρόχου) και πιθανά προβλήματα ένθεσης χρησιμοποιώντας προσωρινές μεταβλητές με πρόθεμα $__ και μοναδικές με ID από το PrintContext::generateId().

Pure n:attributes

Ενώ πολλά n:attributes όπως n:if ή n:foreach χρησιμεύουν ως βολικές συντομεύσεις για τα αντίστοιχα paired tags τους ({if}...{/if}, {foreach}...{/foreach}), το Latte επιτρέπει επίσης τον ορισμό tags που υπάρχουν μόνο με τη μορφή n:attribute. Αυτά χρησιμοποιούνται συχνά για την τροποποίηση των attributes ή της συμπεριφοράς του HTML element στο οποίο είναι προσαρτημένα.

Τυπικά παραδείγματα ενσωματωμένα στο Latte περιλαμβάνουν το n:class, το οποίο βοηθά στη δυναμική κατασκευή του attribute class, και το n:attr, το οποίο μπορεί να ορίσει πολλαπλά αυθαίρετα attributes.

Ας δημιουργήσουμε το δικό μας pure n:attribute: n:confirm, το οποίο προσθέτει ένα JavaScript confirmation dialog πριν από την εκτέλεση μιας ενέργειας (όπως η παρακολούθηση ενός συνδέσμου ή η υποβολή μιας φόρμας).

Στόχος: Υλοποίηση του n:confirm="'Είστε σίγουροι;'", το οποίο προσθέτει έναν onclick handler για την αποτροπή της προεπιλεγμένης ενέργειας εάν ο χρήστης ακυρώσει το confirmation dialog.

Υλοποίηση του ConfirmNode

Χρειαζόμαστε μια κλάση Node και μια συνάρτηση ανάλυσης.

<?php

namespace App\Latte;

use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
use Latte\Compiler\Nodes\Php\ExpressionNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;

class ConfirmNode extends StatementNode
{
	public ExpressionNode $message;

	public static function create(Tag $tag): self
	{
		$tag->expectArguments();
		$node = $tag->node = new self;
		$node->message = $tag->parser->parseExpression();
		return $node;
	}

	/**
	 * Δημιουργεί τον κώδικα του attribute 'onclick' με σωστό escaping.
	 */
	public function print(PrintContext $context): string
	{
		// Διασφαλίζει σωστό escaping για τα contexts JavaScript και HTML attribute.
		return $context->format(
			<<<'XX'
				echo ' onclick="', LR\Filters::escapeHtmlAttr('return confirm(' . LR\Filters::escapeJs(%node) . ')'), '"' %line;
				XX,
			$this->message,
			$this->position,
		);
	}

	public function &getIterator(): \Generator
	{
		yield $this->message;
	}
}

Η μέθοδος print() δημιουργεί κώδικα PHP ο οποίος τελικά, κατά την απόδοση του template, θα εκτυπώσει το HTML attribute onclick="...". Ο χειρισμός των ένθετων contexts (JavaScript μέσα σε ένα HTML attribute) απαιτεί προσεκτικό escaping. Το φίλτρο LR\Filters::escapeJs(%node) καλείται κατά το runtime και κάνει escape το μήνυμα σωστά για χρήση εντός JavaScript (η έξοδος θα ήταν σαν "Sure?"). Στη συνέχεια, το φίλτρο LR\Filters::escapeHtmlAttr(...) κάνει escape τους χαρακτήρες που είναι ειδικοί στα HTML attributes, οπότε αυτό θα άλλαζε την έξοδο σε return confirm(&quot;Sure?&quot;). Αυτό το διπλό runtime escaping διασφαλίζει ότι το μήνυμα είναι ασφαλές για JavaScript και ο προκύπτων κώδικας JavaScript είναι ασφαλής για ενσωμάτωση στο HTML attribute onclick.

Εγγραφή και χρήση

Καταχωρήστε το n:attribute στην επέκτασή σας. Μην ξεχάσετε το πρόθεμα n: στο κλειδί:

class MyLatteExtension extends Extension
{
	public function getTags(): array
	{
		return [
			'datetime' => DatetimeNode::create(...),
			'debug' => DebugNode::create(...),
			'repeat' => RepeatNode::create(...),
			'n:confirm' => ConfirmNode::create(...), // Εγγραφή του n:confirm
		];
	}
}

Τώρα μπορείτε να χρησιμοποιήσετε το n:confirm σε συνδέσμους, κουμπιά ή στοιχεία φόρμας:

<a href="delete.php?id=123" n:confirm='"Θέλετε πραγματικά να διαγράψετε το στοιχείο {$id}?"'>Διαγραφή</a>

Παραγόμενο HTML:

<a href="delete.php?id=123" onclick="return confirm(&quot;Θέλετε πραγματικά να διαγράψετε το στοιχείο 123?&quot;)">Διαγραφή</a>

Όταν ο χρήστης κάνει κλικ στον σύνδεσμο, ο browser εκτελεί τον κώδικα onclick, εμφανίζει το confirmation dialog και μεταβαίνει στο delete.php μόνο εάν ο χρήστης κάνει κλικ στο “OK”.

Αυτό το παράδειγμα δείχνει πώς μπορεί να δημιουργηθεί ένα pure n:attribute για να τροποποιήσει τη συμπεριφορά ή τα attributes του host HTML element του, δημιουργώντας τον κατάλληλο κώδικα PHP στη μέθοδό του print(). Μην ξεχνάτε το διπλό escaping που συχνά απαιτείται: μία φορά για το target context (JavaScript σε αυτή την περίπτωση) και ξανά για το context του HTML attribute.

Προχωρημένα θέματα

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

Λειτουργίες εξόδου tag

Το αντικείμενο Tag που περνά στη συνάρτησή σας create() έχει μια ιδιότητα outputMode. Αυτή η ιδιότητα επηρεάζει τον τρόπο με τον οποίο το Latte χειρίζεται τα γύρω κενά διαστήματα και την εσοχή, ειδικά όταν το tag χρησιμοποιείται σε δική του γραμμή. Μπορείτε να τροποποιήσετε αυτή την ιδιότητα στη συνάρτησή σας create().

  • Tag::OutputKeepIndentation (Προεπιλογή για τα περισσότερα tags όπως {=...}): Το Latte προσπαθεί να διατηρήσει την εσοχή πριν από το tag. Οι νέες γραμμές μετά το tag γενικά διατηρούνται. Αυτό είναι κατάλληλο για tags που εκτυπώνουν περιεχόμενο inline.
  • Tag::OutputRemoveIndentation (Προεπιλογή για block tags όπως {if}, {foreach}): Το Latte αφαιρεί την αρχική εσοχή και πιθανώς μία επόμενη νέα γραμμή. Αυτό βοηθά να διατηρηθεί ο παραγόμενος κώδικας PHP καθαρότερος και αποτρέπει επιπλέον κενές γραμμές στην έξοδο HTML που προκαλούνται από το ίδιο το tag. Χρησιμοποιήστε το για tags που αντιπροσωπεύουν δομές ελέγχου ή μπλοκ που δεν θα πρέπει να προσθέτουν κενά διαστήματα από μόνα τους.
  • Tag::OutputNone (Χρησιμοποιείται από tags όπως {var}, {default}): Παρόμοιο με το RemoveIndentation, αλλά σηματοδοτεί πιο έντονα ότι το ίδιο το tag δεν παράγει άμεση έξοδο, επηρεάζοντας πιθανώς την επεξεργασία των κενών διαστημάτων γύρω του ακόμη πιο επιθετικά. Κατάλληλο για δηλωτικά ή ρυθμιστικά tags.

Επιλέξτε τη λειτουργία που ταιριάζει καλύτερα στον σκοπό του tag σας. Για τα περισσότερα δομικά ή ελεγκτικά tags, το OutputRemoveIndentation είναι συνήθως κατάλληλο.

Πρόσβαση σε γονικά/πλησιέστερα tags

Μερικές φορές η συμπεριφορά ενός tag πρέπει να εξαρτάται από το context στο οποίο χρησιμοποιείται, συγκεκριμένα σε ποιο γονικό tag(s) βρίσκεται. Το αντικείμενο Tag που περνά στη συνάρτησή σας create() παρέχει τη μέθοδο closestTag(array $classes, ?callable $condition = null): ?Tag ακριβώς για αυτόν τον σκοπό.

Αυτή η μέθοδος αναζητά προς τα πάνω την ιεραρχία των τρεχόντως ανοιχτών tags (συμπεριλαμβανομένων των HTML elements που αντιπροσωπεύονται εσωτερικά κατά την ανάλυση) και επιστρέφει το αντικείμενο Tag του πλησιέστερου προγόνου που ταιριάζει με συγκεκριμένα κριτήρια. Εάν δεν βρεθεί ταιριαστός πρόγονος, επιστρέφει null.

Ο πίνακας $classes καθορίζει τι είδους προγονικά tags αναζητάτε. Ελέγχει εάν ο συσχετισμένος κόμβος του προγονικού tag ($ancestorTag->node) είναι μια παρουσία αυτής της κλάσης.

function create(Tag $tag)
{
	// Αναζήτηση του πλησιέστερου προγονικού tag του οποίου ο κόμβος είναι παρουσία του ForeachNode
	$foreachTag = $tag->closestTag([ForeachNode::class]);
	if ($foreachTag) {
		// Μπορούμε να αποκτήσουμε πρόσβαση στην ίδια την παρουσία του ForeachNode:
		$foreachNode = $foreachTag->node;
	}
}

Σημειώστε το $foreachTag->node: Αυτό λειτουργεί μόνο επειδή είναι σύμβαση στην ανάπτυξη των Latte tags να ανατίθεται αμέσως ο δημιουργημένος κόμβος στο $tag->node εντός της μεθόδου create(), όπως κάναμε πάντα.

Μερικές φορές η απλή σύγκριση του τύπου του κόμβου δεν αρκεί. Μπορεί να χρειαστεί να ελέγξετε μια συγκεκριμένη ιδιότητα του πιθανού προγονικού tag ή του κόμβου του. Το προαιρετικό δεύτερο argument για το closestTag() είναι ένα callable που δέχεται το πιθανό προγονικό αντικείμενο Tag και θα πρέπει να επιστρέφει εάν είναι έγκυρη αντιστοιχία.

function create(Tag $tag)
{
	$dynamicBlockTag = $tag->closestTag(
		[BlockNode::class],
		// Συνθήκη: το μπλοκ πρέπει να είναι δυναμικό
		fn(Tag $blockTag) => $blockTag->node->block->isDynamic(),
	);
}

Η χρήση του closestTag() επιτρέπει τη δημιουργία tags που είναι context-aware και επιβάλλουν τη σωστή χρήση εντός της δομής του template σας, οδηγώντας σε πιο στιβαρά και κατανοητά templates.

Placeholders του PrintContext::format()

Έχουμε χρησιμοποιήσει συχνά το PrintContext::format() για τη δημιουργία κώδικα PHP στις μεθόδους print() των κόμβων μας. Δέχεται μια συμβολοσειρά μάσκας και επακόλουθα arguments που αντικαθιστούν τους placeholders στη μάσκα. Ακολουθεί μια σύνοψη των διαθέσιμων placeholders:

  • %node: Το argument πρέπει να είναι μια παρουσία Node. Καλεί τη μέθοδο print() του κόμβου και εισάγει την προκύπτουσα συμβολοσειρά κώδικα PHP.
  • %dump: Το argument είναι οποιαδήποτε τιμή PHP. Εξάγει την τιμή σε έγκυρο κώδικα PHP. Κατάλληλο για scalars, arrays, null.
    • $context->format('echo %dump;', 'Hello')echo 'Hello';
    • $context->format('$arr = %dump;', [1, 2])$arr = [1, 2];
  • %raw: Εισάγει το argument απευθείας στον κώδικα PHP εξόδου χωρίς κανένα escaping ή τροποποίηση. Χρησιμοποιήστε με προσοχή, κυρίως για την εισαγωγή προ-δημιουργημένων τμημάτων κώδικα PHP ή ονομάτων μεταβλητών.
    • $context->format('%raw = 1;', '$variableName')$variableName = 1;
  • %args: Το argument πρέπει να είναι Expression\ArrayNode. Εκτυπώνει τα στοιχεία του πίνακα μορφοποιημένα ως arguments για μια κλήση συνάρτησης ή μεθόδου (διαχωρισμένα με κόμμα, χειρίζεται named arguments εάν υπάρχουν).
    • $argsNode = new ArrayNode([...]);
    • $context->format('myFunc(%args);', $argsNode)myFunc(1, name: 'Joe');
  • %line: Το argument πρέπει να είναι ένα αντικείμενο Position (συνήθως $this->position). Εισάγει ένα σχόλιο PHP /* line X */ που υποδεικνύει τον αριθμό γραμμής της πηγής.
    • $context->format('echo "Hi" %line;', $this->position)echo "Hi" /* line 42 */;
  • %escape(...): Δημιουργεί κώδικα PHP που κατά το runtime κάνει escape την εσωτερική έκφραση χρησιμοποιώντας τους τρέχοντες context-aware κανόνες escaping.
    • $context->format('echo %escape(%node);', $variableNode)
  • %modify(...): Το argument πρέπει να είναι ModifierNode. Δημιουργεί κώδικα PHP που εφαρμόζει τα φίλτρα που καθορίζονται στο ModifierNode στο εσωτερικό περιεχόμενο, συμπεριλαμβανομένου του context-aware escaping, εκτός εάν απενεργοποιηθεί με |noescape.
    • $context->format('%modify(%node);', $modifierNode, $variableNode)
  • %modifyContent(...): Παρόμοιο με το %modify, αλλά προορίζεται για την τροποποίηση μπλοκ συλληφθέντος περιεχομένου (συχνά HTML).

Μπορείτε να αναφερθείτε ρητά στα arguments με τον index τους (ξεκινώντας από το μηδέν): %0.node, %1.dump, %2.raw, κ.λπ. Αυτό επιτρέπει την επαναχρησιμοποίηση ενός argument πολλές φορές στη μάσκα χωρίς να το περάσετε επανειλημμένα στο format(). Δείτε το παράδειγμα του tag {repeat}, όπου χρησιμοποιήθηκαν τα %0.raw και %2.raw.

Παράδειγμα σύνθετης ανάλυσης arguments

Ενώ τα parseExpression(), parseArguments(), κ.λπ., καλύπτουν πολλές περιπτώσεις, μερικές φορές χρειάζεστε πιο σύνθετη λογική ανάλυσης χρησιμοποιώντας το χαμηλότερου επιπέδου TokenStream που είναι διαθέσιμο μέσω του $tag->parser->stream.

Στόχος: Δημιουργία ενός tag {embedYoutube $videoID, width: 640, height: 480}. Θέλουμε να αναλύσουμε το απαιτούμενο ID βίντεο (συμβολοσειρά ή μεταβλητή) ακολουθούμενο από προαιρετικά ζεύγη κλειδιού-τιμής για τις διαστάσεις.

<?php
namespace App\Latte;

class YoutubeNode extends StatementNode
{
	public ExpressionNode $videoId;
	public ?ExpressionNode $width = null;
	public ?ExpressionNode $height = null;

	public static function create(Tag $tag): self
	{
		$tag->expectArguments();
		$node = $tag->node = new self;
		// Ανάλυση του απαιτούμενου ID βίντεο
		$node->videoId = $tag->parser->parseExpression();

		// Ανάλυση προαιρετικών ζευγών κλειδιού-τιμής
		$stream = $tag->parser->stream; // Λήψη της ροής token
		while ($stream->tryConsume(',')) { // Απαιτεί διαχωρισμό με κόμμα
			// Αναμονή αναγνωριστικού 'width' ή 'height'
			$keyToken = $stream->consume(Token::Php_Identifier);
			$key = strtolower($keyToken->text);

			$stream->consume(':'); // Αναμονή διαχωριστικού άνω και κάτω τελείας

			$value = $tag->parser->parseExpression(); // Ανάλυση της έκφρασης τιμής

			if ($key === 'width') {
				$node->width = $value;
			} elseif ($key === 'height') {
				$node->height = $value;
			} else {
				throw new CompileException("Άγνωστο argument '$key'. Αναμενόταν 'width' ή 'height'.", $keyToken->position);
			}
		}

		return $node;
	}
}

Αυτό το επίπεδο ελέγχου σας επιτρέπει να ορίσετε πολύ συγκεκριμένες και σύνθετες συντάξεις για τα προσαρμοσμένα tags σας αλληλεπιδρώντας απευθείας με τη ροή token.

Χρήση του AuxiliaryNode

Το Latte παρέχει γενικούς “βοηθητικούς” κόμβους για ειδικές καταστάσεις κατά τη δημιουργία κώδικα ή εντός των compilation passes. Αυτοί είναι οι AuxiliaryNode και Php\Expression\AuxiliaryNode.

Θεωρήστε το AuxiliaryNode ως έναν ευέλικτο κόμβο container που αναθέτει τις βασικές του λειτουργίες – δημιουργία κώδικα και έκθεση παιδικών κόμβων – στα arguments που παρέχονται στον constructor του:

  • Ανάθεση print(): Το πρώτο argument του constructor είναι ένα PHP closure. Όταν το Latte καλεί τη μέθοδο print() στο AuxiliaryNode, εκτελεί αυτό το παρεχόμενο closure. Το closure δέχεται το PrintContext και οποιουσδήποτε κόμβους πέρασαν στο δεύτερο argument του constructor, επιτρέποντάς σας να ορίσετε εντελώς προσαρμοσμένη λογική δημιουργίας κώδικα PHP κατά το runtime.
  • Ανάθεση getIterator(): Το δεύτερο argument του constructor είναι ένας πίνακας αντικειμένων Node. Όταν το Latte χρειάζεται να διασχίσει τα παιδιά του AuxiliaryNode (π.χ. κατά τη διάρκεια των compilation passes), η μέθοδός του getIterator() απλώς παρέχει τους κόμβους που αναφέρονται σε αυτόν τον πίνακα.

Παράδειγμα:

$node = new AuxiliaryNode(
    // 1. Αυτό το closure γίνεται το σώμα του print()
    fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2),

    // 2. Αυτοί οι κόμβοι παρέχονται από τη μέθοδο getIterator() και περνούν στο παραπάνω closure
    [$argumentNode1, $argumentNode2]
);

Το Latte παρέχει δύο διακριτούς τύπους βασισμένους στο πού χρειάζεται να εισαγάγετε τον παραγόμενο κώδικα:

  • Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode: Χρησιμοποιήστε το όταν χρειάζεται να δημιουργήσετε ένα κομμάτι κώδικα PHP που αντιπροσωπεύει μια έκφραση
  • Latte\Compiler\Nodes\AuxiliaryNode: Χρησιμοποιήστε το για πιο γενικούς σκοπούς, όταν χρειάζεται να εισαγάγετε ένα μπλοκ κώδικα PHP που αντιπροσωπεύει μία ή περισσότερες εντολές

Ένας σημαντικός λόγος για τη χρήση του AuxiliaryNode αντί για τυπικούς κόμβους (όπως StaticMethodCallNode) εντός της μεθόδου print() ή ενός compilation pass είναι ο έλεγχος της ορατότητας για τα επόμενα compilation passes, ειδικά αυτά που σχετίζονται με την ασφάλεια, όπως το Sandbox.

Εξετάστε ένα σενάριο: Το compilation pass σας χρειάζεται να περιβάλλει μια έκφραση που παρέχεται από τον χρήστη ($userExpr) με μια κλήση σε μια συγκεκριμένη, αξιόπιστη βοηθητική συνάρτηση myInternalSanitize($userExpr). Εάν δημιουργήσετε έναν τυπικό κόμβο new FunctionCallNode('myInternalSanitize', [$userExpr]), θα είναι πλήρως ορατός στη διέλευση του AST. Εάν το Sandbox pass εκτελεστεί αργότερα και το myInternalSanitize δεν βρίσκεται στη λίστα επιτρεπόμενων του, το Sandbox μπορεί να μπλοκάρει ή να τροποποιήσει αυτή την κλήση, διαταράσσοντας πιθανώς την εσωτερική λογική του tag σας, ακόμα κι αν εσείς, ο συγγραφέας του tag, γνωρίζετε ότι αυτή η συγκεκριμένη κλήση είναι ασφαλής και απαραίτητη. Μπορείτε λοιπόν να δημιουργήσετε την κλήση απευθείας εντός του closure του AuxiliaryNode.

use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode;

// ... εντός του print() ή ενός compilation pass ...
$wrappedNode = new AuxiliaryNode(
	fn(PrintContext $context, $userExpr) => $context->format(
		'myInternalSanitize(%node)', // Άμεση δημιουργία κώδικα PHP
		$userExpr,
	),
	// ΣΗΜΑΝΤΙΚΟ: Παρ' όλα αυτά, περάστε τον αρχικό κόμβο έκφρασης χρήστη εδώ!
	[$userExpr],
);

Σε αυτή την περίπτωση, το Sandbox pass βλέπει το AuxiliaryNode, αλλά δεν αναλύει τον κώδικα PHP που δημιουργείται από το closure του. Δεν μπορεί να μπλοκάρει απευθείας την κλήση myInternalSanitize που δημιουργείται εντός του closure.

Ενώ ο ίδιος ο παραγόμενος κώδικας PHP είναι κρυμμένος από τα passes, οι είσοδοι σε αυτόν τον κώδικα (οι κόμβοι που αντιπροσωπεύουν δεδομένα ή εκφράσεις χρήστη) πρέπει ακόμα να είναι διασχίσιμοι. Γι' αυτό το δεύτερο argument του constructor του AuxiliaryNode είναι κρίσιμο. Πρέπει να περάσετε έναν πίνακα που περιέχει όλους τους αρχικούς κόμβους (όπως $userExpr στο παραπάνω παράδειγμα) που χρησιμοποιεί το closure σας. Το getIterator() του AuxiliaryNode θα παρέχει αυτούς τους κόμβους, επιτρέποντας στα compilation passes όπως το Sandbox να τους αναλύσουν για πιθανά προβλήματα.

Βέλτιστες πρακτικές

  • Σαφής σκοπός: Βεβαιωθείτε ότι το tag σας έχει σαφή και απαραίτητο σκοπό. Μην δημιουργείτε tags για εργασίες που μπορούν εύκολα να αντιμετωπιστούν με φίλτρα ή συναρτήσεις.
  • Υλοποιήστε σωστά το getIterator(): Πάντα υλοποιείτε το getIterator() και παρέχετε αναφορές (&) σε όλους τους παιδικούς κόμβους (arguments, περιεχόμενο) που αναλύθηκαν από το template. Αυτό είναι απαραίτητο για τα compilation passes, την ασφάλεια (Sandbox) και πιθανές μελλοντικές βελτιστοποιήσεις.
  • Δημόσιες ιδιότητες για κόμβους: Κάντε τις ιδιότητες που περιέχουν παιδικούς κόμβους δημόσιες, ώστε τα compilation passes να μπορούν να τις τροποποιήσουν εάν χρειαστεί.
  • Χρησιμοποιήστε το PrintContext::format(): Αξιοποιήστε τη μέθοδο format() για τη δημιουργία κώδικα PHP. Χειρίζεται τα εισαγωγικά, κάνει σωστά escape τους placeholders και προσθέτει αυτόματα σχόλια με τον αριθμό γραμμής.
  • Προσωρινές μεταβλητές ($__): Κατά τη δημιουργία runtime κώδικα PHP που χρειάζεται προσωρινές μεταβλητές (π.χ. για αποθήκευση ενδιάμεσων αθροισμάτων, μετρητές βρόχου), χρησιμοποιήστε τη σύμβαση του προθέματος $__ για να αποφύγετε συγκρούσεις με μεταβλητές χρήστη και εσωτερικές μεταβλητές Latte $ʟ_.
  • Ένθεση και μοναδικά ID: Εάν το tag σας μπορεί να είναι ένθετο ή χρειάζεται κατάσταση συγκεκριμένη για την παρουσία κατά το runtime, χρησιμοποιήστε το $context->generateId() εντός της μεθόδου print() για να δημιουργήσετε μοναδικά επιθέματα για τις προσωρινές σας μεταβλητές $__.
  • Providers για εξωτερικά δεδομένα: Χρησιμοποιήστε providers (καταχωρημένους μέσω του Extension::getProviders()) για πρόσβαση σε runtime δεδομένα ή υπηρεσίες ($this->global->…) αντί να κωδικοποιείτε τιμές ή να βασίζεστε σε global state. Χρησιμοποιήστε προθέματα κατασκευαστή για τα ονόματα των providers.
  • Εξετάστε τα n:attributes: Εάν το paired tag σας λειτουργεί λογικά σε ένα μόνο HTML element, το Latte πιθανότατα παρέχει αυτόματη υποστήριξη n:attribute. Λάβετε αυτό υπόψη για την ευκολία του χρήστη. Εάν δημιουργείτε ένα tag που τροποποιεί ένα attribute, εξετάστε εάν ένα pure n:attribute είναι η καταλληλότερη μορφή.
  • Testing: Γράψτε tests για τα tags σας, καλύπτοντας τόσο την ανάλυση διαφόρων συντακτικών εισόδων όσο και την ορθότητα της εξόδου του παραγόμενου κώδικα PHP.

Ακολουθώντας αυτές τις οδηγίες, μπορείτε να δημιουργήσετε ισχυρά, στιβαρά και συντηρήσιμα προσαρμοσμένα tags που ενσωματώνονται άψογα με τη μηχανή templating του Latte.

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

έκδοση: 3.0