Περάσματα Μεταγλώττισης
Τα περάσματα μεταγλώττισης παρέχουν έναν ισχυρό μηχανισμό για την ανάλυση και τροποποίηση προτύπων Latte μετά την ανάλυσή τους σε αφηρημένο συντακτικό δέντρο (AST) και πριν από τη δημιουργία του τελικού κώδικα PHP. Αυτό επιτρέπει προηγμένη χειραγώγηση προτύπων, βελτιστοποιήσεις, ελέγχους ασφαλείας (όπως το Sandbox) και συλλογή πληροφοριών σχετικά με τα πρότυπα. Αυτός ο οδηγός θα σας καθοδηγήσει στη δημιουργία των δικών σας περασμάτων μεταγλώττισης.
Τι είναι ένα πέρασμα μεταγλώττισης;
Για να κατανοήσετε το ρόλο των περασμάτων μεταγλώττισης, ρίξτε μια ματιά στη διαδικασία μεταγλώττισης του Latte. Όπως μπορείτε να δείτε, τα περάσματα μεταγλώττισης λειτουργούν σε μια κρίσιμη φάση, επιτρέποντας βαθιά παρέμβαση μεταξύ της αρχικής ανάλυσης και της τελικής εξόδου κώδικα.
Στον πυρήνα του, ένα πέρασμα μεταγλώττισης είναι απλά ένα PHP callable
αντικείμενο (όπως μια συνάρτηση, στατική μέθοδος ή μέθοδος
στιγμιοτύπου), που δέχεται ένα όρισμα: τον ριζικό κόμβο του AST του
προτύπου, ο οποίος είναι πάντα ένα στιγμιότυπο του
Latte\Compiler\Nodes\TemplateNode
.
Ο πρωταρχικός στόχος ενός περάσματος μεταγλώττισης είναι συνήθως ένας ή και οι δύο από τους παρακάτω:
- Ανάλυση: Διασχίστε το AST και συλλέξτε πληροφορίες σχετικά με το πρότυπο (π.χ. βρείτε όλα τα ορισμένα μπλοκ, ελέγξτε τη χρήση συγκεκριμένων tags, διασφαλίστε την τήρηση ορισμένων περιορισμών ασφαλείας).
- Τροποποίηση: Αλλάξτε τη δομή του AST ή τα χαρακτηριστικά των κόμβων (π.χ. προσθέστε αυτόματα χαρακτηριστικά HTML, βελτιστοποιήστε ορισμένους συνδυασμούς tags, αντικαταστήστε τα παρωχημένα tags με νέα, εφαρμόστε κανόνες sandbox).
Καταχώριση
Τα περάσματα μεταγλώττισης καταχωρούνται χρησιμοποιώντας τη μέθοδο
επέκτασης getPasses()
. Αυτή η
μέθοδος επιστρέφει έναν συσχετιστικό πίνακα, όπου τα κλειδιά είναι
μοναδικά ονόματα περασμάτων (που χρησιμοποιούνται εσωτερικά και για
ταξινόμηση) και οι τιμές είναι PHP callable αντικείμενα που υλοποιούν τη
λογική του περάσματος.
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Extension;
class MyExtension extends Extension
{
public function getPasses(): array
{
return [
'modificationPass' => $this->modifyTemplateAst(...),
// ... άλλα περάσματα ...
];
}
public function modifyTemplateAst(TemplateNode $templateNode): void
{
// Υλοποίηση...
}
}
Τα περάσματα που καταχωρούνται από τις βασικές επεκτάσεις του Latte και
τις δικές σας επεκτάσεις εκτελούνται διαδοχικά. Η σειρά μπορεί να
είναι σημαντική, ειδικά αν ένα πέρασμα εξαρτάται από τα αποτελέσματα ή
τις τροποποιήσεις ενός άλλου. Το Latte παρέχει έναν βοηθητικό μηχανισμό
για τον έλεγχο αυτής της σειράς, εάν χρειάζεται· δείτε την τεκμηρίωση
για το Extension::getPasses()
για
λεπτομέρειες.
Παράδειγμα 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') ) ) ) )
Διάσχιση του AST με το NodeTraverser
Η χειροκίνητη συγγραφή αναδρομικών συναρτήσεων για τη διάσχιση της πολύπλοκης δομής του AST είναι κουραστική και επιρρεπής σε σφάλματα. Το Latte παρέχει ένα ειδικό εργαλείο για αυτόν τον σκοπό: το Latte\Compiler\NodeTraverser. Αυτή η κλάση υλοποιεί το πρότυπο σχεδίασης Visitor, χάρη στο οποίο η διάσχιση του AST γίνεται συστηματική και εύκολα διαχειρίσιμη.
Η βασική χρήση περιλαμβάνει τη δημιουργία ενός στιγμιότυπου του
NodeTraverser
και την κλήση της μεθόδου του traverse()
, περνώντας
τον ριζικό κόμβο του AST και ένα ή δύο “visitor” callable αντικείμενα:
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
(new NodeTraverser)->traverse(
$templateNode,
// 'enter' visitor: Καλείται κατά την είσοδο στον κόμβο (πριν από τα παιδιά του)
enter: function (Node $node) {
echo "Είσοδος στον κόμβο τύπου: " . $node::class . "\n";
// Εδώ μπορείτε να εξετάσετε τον κόμβο
if ($node instanceof Nodes\TextNode) {
// echo "Βρέθηκε κείμενο: " . $node->content . "\n";
}
},
// 'leave' visitor: Καλείται κατά την έξοδο από τον κόμβο (μετά τα παιδιά του)
leave: function (Node $node) {
echo "Έξοδος από τον κόμβο τύπου: " . $node::class . "\n";
// Εδώ μπορείτε να εκτελέσετε ενέργειες μετά την επεξεργασία των παιδιών
},
);
Μπορείτε να παρέχετε μόνο τον enter
visitor, μόνο τον leave
visitor,
ή και τους δύο, ανάλογα με τις ανάγκες σας.
enter(Node $node)
: Αυτή η συνάρτηση εκτελείται για κάθε κόμβο
πριν ο διασχιστής επισκεφθεί οποιοδήποτε από τα παιδιά αυτού του
κόμβου. Είναι χρήσιμη για:
- Συλλογή πληροφοριών κατά τη διάσχιση του δέντρου προς τα κάτω.
- Λήψη αποφάσεων πριν από την επεξεργασία των παιδιών (όπως η απόφαση να τα παραλείψετε, δείτε Βελτιστοποίηση διάσχισης).
- Πιθανή τροποποίηση του κόμβου πριν από την επίσκεψη των παιδιών (λιγότερο συχνή).
leave(Node $node)
: Αυτή η συνάρτηση εκτελείται για κάθε κόμβο
αφού όλα τα παιδιά του (και τα ολόκληρα υποδέντρα τους) έχουν πλήρως
επισκεφθεί (τόσο η είσοδος όσο και η έξοδος). Είναι το πιο συνηθισμένο
μέρος για:
Και οι δύο visitors enter
και leave
μπορούν προαιρετικά να
επιστρέψουν μια τιμή για να επηρεάσουν τη διαδικασία διάσχισης. Η
επιστροφή null
(ή τίποτα) συνεχίζει κανονικά τη διάσχιση, η
επιστροφή ενός στιγμιότυπου Node
αντικαθιστά τον τρέχοντα κόμβο,
και η επιστροφή ειδικών σταθερών όπως NodeTraverser::RemoveNode
ή
NodeTraverser::StopTraversal
τροποποιεί τη ροή, όπως εξηγείται στις επόμενες
ενότητες.
Πώς λειτουργεί η διάσχιση
Ο NodeTraverser
χρησιμοποιεί εσωτερικά τη μέθοδο getIterator()
, την
οποία πρέπει να υλοποιεί κάθε κλάση Node
(όπως συζητήθηκε στο Δημιουργία
προσαρμοσμένων tags). Επαναλαμβάνει τα παιδιά που λαμβάνονται μέσω του
getIterator()
, καλεί αναδρομικά το traverse()
σε αυτά και
διασφαλίζει ότι οι visitors enter
και leave
καλούνται στη σωστή
σειρά πρώτα-σε-βάθος για κάθε κόμβο στο δέντρο που είναι προσβάσιμος
μέσω των iterators. Αυτό τονίζει ξανά γιατί η σωστά υλοποιημένη
getIterator()
στους προσαρμοσμένους κόμβους tag σας είναι απολύτως
απαραίτητη για τη σωστή λειτουργία των περασμάτων μεταγλώττισης.
Ας γράψουμε ένα απλό πέρασμα που μετρά πόσες φορές χρησιμοποιείται
το tag {do}
(που αντιπροσωπεύεται από το Latte\Essential\Nodes\DoNode
) στο
πρότυπο.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\DoNode;
function countDoTags(TemplateNode $templateNode): void
{
$count = 0;
(new NodeTraverser)->traverse(
$templateNode,
enter: function (Node $node) use (&$count): void {
if ($node instanceof DoNode) {
$count++;
}
},
// Ο 'leave' visitor δεν χρειάζεται για αυτήν την εργασία
);
echo "Βρέθηκε το tag {do} $count φορές.\n";
}
$latte = new Latte\Engine;
$ast = $latte->parse($templateSource);
countDoTags($ast);
Σε αυτό το παράδειγμα, χρειαζόμασταν μόνο τον visitor enter
για να
ελέγξουμε τον τύπο κάθε κόμβου που επισκεφθήκαμε.
Στη συνέχεια, θα εξετάσουμε πώς αυτοί οι visitors τροποποιούν πραγματικά το AST.
Τροποποίηση του AST
Ένας από τους κύριους σκοπούς των περασμάτων μεταγλώττισης είναι η
τροποποίηση του αφηρημένου συντακτικού δέντρου. Αυτό επιτρέπει
ισχυρούς μετασχηματισμούς, βελτιστοποιήσεις ή την επιβολή κανόνων
απευθείας στη δομή του προτύπου πριν από τη δημιουργία κώδικα PHP. Το
NodeTraverser
παρέχει διάφορους τρόπους για να το επιτύχετε αυτό
εντός των visitors enter
και leave
.
Σημαντική σημείωση: Η τροποποίηση του AST απαιτεί προσοχή. Λανθασμένες αλλαγές – όπως η αφαίρεση βασικών κόμβων ή η αντικατάσταση ενός κόμβου με έναν μη συμβατό τύπο – μπορεί να οδηγήσουν σε σφάλματα κατά τη δημιουργία κώδικα ή να προκαλέσουν απροσδόκητη συμπεριφορά κατά την εκτέλεση του προγράμματος. Πάντα δοκιμάζετε διεξοδικά τα περάσματα τροποποίησής σας.
Αλλαγή ιδιοτήτων κόμβων
Ο απλούστερος τρόπος τροποποίησης του δέντρου είναι η άμεση αλλαγή των δημόσιων ιδιοτήτων των κόμβων που επισκέπτονται κατά τη διάσχιση. Όλοι οι κόμβοι αποθηκεύουν τα αναλυμένα ορίσματά τους, το περιεχόμενο ή τα χαρακτηριστικά τους σε δημόσιες ιδιότητες.
Παράδειγμα: Ας δημιουργήσουμε ένα πέρασμα που βρίσκει όλους τους
στατικούς κόμβους κειμένου (TextNode
, που αντιπροσωπεύουν κανονικό
HTML ή κείμενο εκτός των tags Latte) και μετατρέπει το περιεχόμενό τους σε
κεφαλαία απευθείας στο AST.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\TextNode;
function uppercaseStaticText(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// Μπορούμε να χρησιμοποιήσουμε το 'enter', επειδή το TextNode δεν έχει παιδιά για επεξεργασία
enter: function (Node $node) {
// Είναι αυτός ο κόμβος ένα στατικό μπλοκ κειμένου;
if ($node instanceof TextNode) {
// Ναι! Τροποποιούμε άμεσα τη δημόσια ιδιότητά του 'content'.
$node->content = mb_strtoupper(html_entity_decode($node->content));
}
// Δεν χρειάζεται να επιστρέψουμε τίποτα· η αλλαγή εφαρμόζεται απευθείας.
},
);
}
Σε αυτό το παράδειγμα, ο visitor enter
ελέγχει αν ο τρέχων $node
είναι τύπου TextNode
. Αν ναι, ενημερώνουμε απευθείας τη δημόσια
ιδιότητά του $content
χρησιμοποιώντας το mb_strtoupper()
. Αυτό
αλλάζει άμεσα το περιεχόμενο του στατικού κειμένου που είναι
αποθηκευμένο στο AST πριν από τη δημιουργία κώδικα PHP. Επειδή
τροποποιούμε το αντικείμενο απευθείας, δεν χρειάζεται να επιστρέψουμε
τίποτα από τον visitor.
Αποτέλεσμα: Αν το πρότυπο περιείχε
<p>Hello</p>{= $var }<span>World</span>
, μετά από αυτό το πέρασμα το
AST θα αντιπροσωπεύει κάτι σαν:
<p>HELLO</p>{= $var }<span>WORLD</span>
. Αυτό ΔΕΝ ΕΠΗΡΕΑΖΕΙ το
περιεχόμενο του $var.
Αντικατάσταση κόμβων
Μια πιο ισχυρή τεχνική τροποποίησης είναι η πλήρης αντικατάσταση
ενός κόμβου με έναν άλλο. Αυτό γίνεται επιστρέφοντας ένα νέο
στιγμιότυπο Node
από τον visitor enter
ή leave
. Ο
NodeTraverser
αντικαθιστά στη συνέχεια τον αρχικό κόμβο με τον
επιστρεφόμενο στη δομή του γονικού κόμβου.
Παράδειγμα: Ας δημιουργήσουμε ένα πέρασμα που βρίσκει όλες τις
χρήσεις της σταθεράς PHP_VERSION
(που αντιπροσωπεύεται από το
ConstantFetchNode
) και τις αντικαθιστά απευθείας με ένα string literal
(StringNode
) που περιέχει την πραγματική έκδοση της PHP που
ανιχνεύθηκε κατά τη μεταγλώττιση. Αυτή είναι μια μορφή
βελτιστοποίησης κατά τη μεταγλώττιση.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\Php\Expression\ConstantFetchNode;
use Latte\Compiler\Nodes\Php\Scalar\StringNode;
function inlinePhpVersion(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// Το 'leave' χρησιμοποιείται συχνά για αντικατάσταση, διασφαλίζοντας ότι τα παιδιά (αν υπάρχουν)
// επεξεργάζονται πρώτα, αν και το 'enter' θα λειτουργούσε επίσης εδώ.
leave: function (Node $node) {
// Είναι αυτός ο κόμβος πρόσβαση σε σταθερά και το όνομα της σταθεράς είναι 'PHP_VERSION';
if ($node instanceof ConstantFetchNode && (string) $node->name === 'PHP_VERSION') {
// Δημιουργούμε ένα νέο StringNode που περιέχει την τρέχουσα έκδοση της PHP
$newNode = new StringNode(PHP_VERSION);
// Προαιρετικό, αλλά καλή πρακτική: αντιγράφουμε τις πληροφορίες θέσης
$newNode->position = $node->position;
// Επιστρέφουμε το νέο StringNode. Ο Traverser θα αντικαταστήσει
// το αρχικό ConstantFetchNode με αυτό το $newNode.
return $newNode;
}
// Αν δεν επιστρέψουμε ένα Node, ο αρχικός $node διατηρείται.
},
);
}
Εδώ ο visitor leave
αναγνωρίζει το συγκεκριμένο ConstantFetchNode
για
το PHP_VERSION
. Στη συνέχεια, δημιουργεί ένα εντελώς νέο StringNode
που περιέχει την τιμή της σταθεράς PHP_VERSION
κατά τη
μεταγλώττιση. Επιστρέφοντας αυτό το $newNode
λέει στον traverser να
αντικαταστήσει το αρχικό ConstantFetchNode
στο AST.
Αποτέλεσμα: Αν το πρότυπο περιείχε {= PHP_VERSION }
και η
μεταγλώττιση εκτελείται σε PHP 8.2.1, το AST μετά από αυτό το πέρασμα θα
αντιπροσωπεύει ουσιαστικά {= '8.2.1' }
.
Επιλογή enter
vs. leave
για αντικατάσταση:
- Χρησιμοποιήστε το
leave
εάν η δημιουργία του νέου κόμβου εξαρτάται από τα αποτελέσματα της επεξεργασίας των παιδιών του παλιού κόμβου, ή αν θέλετε απλώς να διασφαλίσετε ότι τα παιδιά επισκέπτονται πριν από την αντικατάσταση (συνήθης πρακτική). - Χρησιμοποιήστε το
enter
εάν θέλετε να αντικαταστήσετε έναν κόμβο πριν τα παιδιά του επισκεφθούν καν.
Αφαίρεση κόμβων
Μπορείτε να αφαιρέσετε εντελώς έναν κόμβο από το AST επιστρέφοντας την
ειδική σταθερά NodeTraverser::RemoveNode
από τον visitor.
Παράδειγμα: Ας αφαιρέσουμε όλα τα σχόλια προτύπου ({* ... *}
),
τα οποία αντιπροσωπεύονται από το CommentNode
στο AST που παράγεται
από τον πυρήνα του Latte (αν και συνήθως επεξεργάζονται νωρίτερα, αυτό
χρησιμεύει ως παράδειγμα).
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Compiler\Nodes\CommentNode;
function removeCommentNodes(TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// Το 'enter' είναι εντάξει εδώ, επειδή δεν χρειαζόμαστε πληροφορίες για τα παιδιά για να αφαιρέσουμε το σχόλιο
enter: function (Node $node) {
if ($node instanceof CommentNode) {
// Σηματοδοτούμε στον traverser να αφαιρέσει αυτόν τον κόμβο από το AST
return NodeTraverser::RemoveNode;
}
},
);
}
Προειδοποίηση: Χρησιμοποιήστε το RemoveNode
με προσοχή. Η
αφαίρεση ενός κόμβου που περιέχει βασικό περιεχόμενο ή επηρεάζει τη
δομή (όπως η αφαίρεση του κόμβου περιεχομένου ενός βρόχου) μπορεί να
οδηγήσει σε κατεστραμμένα πρότυπα ή άκυρο παραγόμενο κώδικα. Είναι
ασφαλέστερο για κόμβους που είναι πραγματικά προαιρετικοί ή αυτόνομοι
(όπως σχόλια ή tags εντοπισμού σφαλμάτων) ή για κενούς δομικούς κόμβους
(π.χ. ένα κενό FragmentNode
μπορεί να αφαιρεθεί με ασφάλεια σε ορισμένα
πλαίσια από ένα πέρασμα καθαρισμού).
Αυτές οι τρεις μέθοδοι – τροποποίηση ιδιοτήτων, αντικατάσταση κόμβων και αφαίρεση κόμβων – παρέχουν τα βασικά εργαλεία για τη χειραγώγηση του AST εντός των περασμάτων μεταγλώττισής σας.
Βελτιστοποίηση διάσχισης
Το AST των προτύπων μπορεί να είναι αρκετά μεγάλο, περιέχοντας
ενδεχομένως χιλιάδες κόμβους. Η διάσχιση κάθε μεμονωμένου κόμβου
μπορεί να είναι περιττή και να επηρεάσει την απόδοση της
μεταγλώττισης, εάν το πέρασμά σας ενδιαφέρεται μόνο για συγκεκριμένα
τμήματα του δέντρου. Το NodeTraverser
προσφέρει τρόπους
βελτιστοποίησης της διάσχισης:
Παράλειψη παιδιών
Αν γνωρίζετε ότι μόλις συναντήσετε έναν συγκεκριμένο τύπο κόμβου,
κανένας από τους απογόνους του δεν μπορεί να περιέχει τους κόμβους που
αναζητάτε, μπορείτε να πείτε στον traverser να παραλείψει την επίσκεψη των
παιδιών του. Αυτό γίνεται επιστρέφοντας τη σταθερά
NodeTraverser::DontTraverseChildren
από τον visitor enter
. Αυτό παραλείπει
ολόκληρους κλάδους κατά τη διάσχιση, εξοικονομώντας δυνητικά
σημαντικό χρόνο, ειδικά σε πρότυπα με πολύπλοκες εκφράσεις PHP εντός
των tags.
Διακοπή διάσχισης
Αν το πέρασμά σας χρειάζεται να βρει μόνο την πρώτη εμφάνιση κάτι
(ένα συγκεκριμένο τύπο κόμβου, την ικανοποίηση μιας συνθήκης), μπορείτε
να σταματήσετε εντελώς ολόκληρη τη διαδικασία διάσχισης μόλις το
βρείτε. Αυτό επιτυγχάνεται επιστρέφοντας τη σταθερά
NodeTraverser::StopTraversal
από τον visitor enter
ή leave
. Η μέθοδος
traverse()
σταματά να επισκέπτεται οποιουσδήποτε άλλους κόμβους.
Αυτό είναι εξαιρετικά αποτελεσματικό εάν χρειάζεστε μόνο την πρώτη
αντιστοίχιση σε ένα δυνητικά πολύ μεγάλο δέντρο.
Χρήσιμος βοηθός NodeHelpers
Ενώ το NodeTraverser
προσφέρει λεπτομερή έλεγχο, το Latte παρέχει
επίσης μια πρακτική βοηθητική κλάση, το Latte\Compiler\NodeHelpers, το οποίο
ενσωματώνει το NodeTraverser
για διάφορες κοινές εργασίες αναζήτησης
και ανάλυσης, απαιτώντας συχνά λιγότερο προπαρασκευαστικό κώδικα.
find (Node $startNode, callable $filter): array
Αυτή η στατική μέθοδος βρίσκει όλους τους κόμβους στο υποδέντρο
που ξεκινούν από το $startNode
(συμπεριλαμβανομένου), οι οποίοι
ικανοποιούν το callback $filter
. Επιστρέφει έναν πίνακα των
αντιστοιχούντων κόμβων.
Παράδειγμα: Βρείτε όλους τους κόμβους μεταβλητών (VariableNode
)
σε ολόκληρο το πρότυπο.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Php\Expression\VariableNode;
use Latte\Compiler\Nodes\TemplateNode;
function findAllVariables(TemplateNode $templateNode): array
{
return NodeHelpers::find(
$templateNode,
fn($node) => $node instanceof VariableNode,
);
}
findFirst (Node $startNode, callable $filter): ?Node
Παρόμοιο με το find
, αλλά σταματά τη διάσχιση αμέσως μόλις
βρεθεί ο πρώτος κόμβος που ικανοποιεί το callback $filter
.
Επιστρέφει το αντικείμενο Node
που βρέθηκε ή null
εάν δεν
βρεθεί κανένας αντιστοιχών κόμβος. Αυτό είναι ουσιαστικά ένα πρακτικό
περιτύλιγμα γύρω από το NodeTraverser::StopTraversal
.
Παράδειγμα: Βρείτε τον κόμβο {parameters}
(το ίδιο με το
χειροκίνητο παράδειγμα προηγουμένως, αλλά συντομότερο).
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\TemplateNode;
use Latte\Essential\Nodes\ParametersNode;
function findParametersNodeHelper(TemplateNode $templateNode): ?ParametersNode
{
return NodeHelpers::findFirst(
$templateNode->head, // Αναζήτηση μόνο στην κύρια ενότητα για αποτελεσματικότητα
fn($node) => $node instanceof ParametersNode,
);
}
toValue (ExpressionNode $node, bool $constants = false): mixed
Αυτή η στατική μέθοδος προσπαθεί να αξιολογήσει ένα ExpressionNode
κατά τη μεταγλώττιση και να επιστρέψει την αντίστοιχη τιμή PHP.
Λειτουργεί αξιόπιστα μόνο για απλούς κόμβους literal (StringNode
,
IntegerNode
, FloatNode
, BooleanNode
, NullNode
) και στιγμιότυπα
ArrayNode
που περιέχουν μόνο τέτοια αξιολογήσιμα στοιχεία.
Εάν το $constants
οριστεί σε true
, θα προσπαθήσει επίσης να
επιλύσει το ConstantFetchNode
και το ClassConstantFetchNode
ελέγχοντας το
defined()
και χρησιμοποιώντας το constant()
.
Εάν ο κόμβος περιέχει μεταβλητές, κλήσεις συναρτήσεων ή άλλα
δυναμικά στοιχεία, δεν μπορεί να αξιολογηθεί κατά τη μεταγλώττιση και
η μέθοδος θα πετάξει InvalidArgumentException
.
Περίπτωση χρήσης: Απόκτηση της στατικής τιμής ενός ορίσματος tag κατά τη μεταγλώττιση για λήψη αποφάσεων κατά τη μεταγλώττιση.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Php\ExpressionNode;
function getStaticStringArgument(ExpressionNode $argumentNode): ?string
{
try {
$value = NodeHelpers::toValue($argumentNode);
return is_string($value) ? $value : null;
} catch (\InvalidArgumentException $e) {
// Το όρισμα δεν ήταν στατικό string literal
return null;
}
}
toText (?Node $node): ?string
Αυτή η στατική μέθοδος είναι χρήσιμη για την εξαγωγή απλού περιεχομένου κειμένου από απλούς κόμβους. Λειτουργεί κυρίως με:
TextNode
: Επιστρέφει το$content
του.FragmentNode
: Συνενώνει το αποτέλεσμα τουtoText()
για όλα τα παιδιά του. Αν κάποιο παιδί δεν είναι μετατρέψιμο σε κείμενο (π.χ. περιέχειPrintNode
), επιστρέφειnull
.NopNode
: Επιστρέφει μια κενή συμβολοσειρά.- Άλλοι τύποι κόμβων: Επιστρέφει
null
.
Περίπτωση χρήσης: Απόκτηση του στατικού περιεχομένου κειμένου της τιμής ενός χαρακτηριστικού HTML ή ενός απλού στοιχείου HTML για ανάλυση κατά τη διάρκεια ενός περάσματος μεταγλώττισης.
use Latte\Compiler\NodeHelpers;
use Latte\Compiler\Nodes\Html\AttributeNode;
function getStaticAttributeValue(AttributeNode $attr): ?string
{
// το $attr->value είναι συνήθως AreaNode (όπως FragmentNode ή TextNode)
return NodeHelpers::toText($attr->value);
}
// Παράδειγμα χρήσης σε πέρασμα:
// if ($node instanceof Html\ElementNode && $node->name === 'meta') {
// $nameAttrValue = getStaticAttributeValue($node->getAttributeNode('name'));
// if ($nameAttrValue === 'description') { ... }
// }
Το NodeHelpers
μπορεί να απλοποιήσει τα περάσματα μεταγλώττισής
σας παρέχοντας έτοιμες λύσεις για κοινές εργασίες διάσχισης και
ανάλυσης του AST.
Πρακτικά παραδείγματα
Ας εφαρμόσουμε τις έννοιες της διάσχισης και τροποποίησης του AST για να λύσουμε ορισμένα πρακτικά προβλήματα. Αυτά τα παραδείγματα δείχνουν κοινά πρότυπα που χρησιμοποιούνται στα περάσματα μεταγλώττισης.
Αυτόματη προσθήκη loading="lazy"
σε
<img>
Οι σύγχρονοι περιηγητές υποστηρίζουν εγγενή τεμπέλικη φόρτωση (lazy
loading) για εικόνες χρησιμοποιώντας το χαρακτηριστικό loading="lazy"
. Ας
δημιουργήσουμε ένα πέρασμα που προσθέτει αυτόματα αυτό το
χαρακτηριστικό σε όλα τα tags <img>
που δεν έχουν ήδη το
χαρακτηριστικό loading
.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
use Latte\Compiler\Nodes\Html;
function addLazyLoading(Nodes\TemplateNode $templateNode): void
{
(new NodeTraverser)->traverse(
$templateNode,
// Μπορούμε να χρησιμοποιήσουμε το 'enter', επειδή τροποποιούμε τον κόμβο απευθείας
// και δεν εξαρτόμαστε από τα παιδιά για αυτήν την απόφαση.
enter: function (Node $node) {
// Είναι ένα στοιχείο HTML με όνομα 'img';
if ($node instanceof Html\ElementNode && $node->name === 'img') {
// Διασφαλίζουμε ότι ο κόμβος χαρακτηριστικών υπάρχει
$node->attributes ??= new Nodes\FragmentNode;
// Ελέγχουμε αν υπάρχει ήδη το χαρακτηριστικό 'loading' (ανεξαρτήτως πεζών-κεφαλαίων)
foreach ($node->attributes->children as $attrNode) {
if ($attrNode instanceof Html\AttributeNode
&& $attrNode->name instanceof Nodes\TextNode // Στατικό όνομα χαρακτηριστικού
&& strtolower($attrNode->name->content) === 'loading'
) {
return;
}
}
// Προσθέτουμε ένα κενό εάν τα χαρακτηριστικά δεν είναι κενά
if ($node->attributes->children) {
$node->attributes->children[] = new Nodes\TextNode(' ');
}
// Δημιουργούμε ένα νέο κόμβο χαρακτηριστικού: loading="lazy"
$node->attributes->children[] = new Html\AttributeNode(
name: new Nodes\TextNode('loading'),
value: new Nodes\TextNode('lazy'),
quote: '"',
);
// Η αλλαγή εφαρμόζεται απευθείας στο αντικείμενο, δεν χρειάζεται να επιστρέψουμε τίποτα.
}
},
);
}
Εξήγηση:
- Ο visitor
enter
αναζητά κόμβουςHtml\ElementNode
με όνομαimg
. - Επαναλαμβάνει τα υπάρχοντα χαρακτηριστικά
(
$node->attributes->children
) και ελέγχει αν το χαρακτηριστικόloading
είναι ήδη παρόν. - Αν δεν βρεθεί, δημιουργεί ένα νέο
Html\AttributeNode
που αντιπροσωπεύει τοloading="lazy"
.
Έλεγχος κλήσεων συναρτήσεων
Τα περάσματα μεταγλώττισης αποτελούν τη βάση του Latte Sandbox. Αν και το πραγματικό Sandbox είναι εξελιγμένο, μπορούμε να επιδείξουμε τη βασική αρχή του ελέγχου απαγορευμένων κλήσεων συναρτήσεων.
Στόχος: Αποτροπή της χρήσης της δυνητικά επικίνδυνης συνάρτησης
shell_exec
εντός των εκφράσεων του προτύπου.
use Latte\Compiler\Node;
use Latte\Compiler\NodeTraverser;
use Latte\Compiler\Nodes;
use Latte\Compiler\Nodes\Php;
use Latte\SecurityViolationException;
function checkForbiddenFunctions(Nodes\TemplateNode $templateNode): void
{
$forbiddenFunctions = ['shell_exec' => true, 'exec' => true]; // Απλή λίστα
$traverser = new NodeTraverser;
(new NodeTraverser)->traverse(
$templateNode,
enter: function (Node $node) use ($forbiddenFunctions) {
// Είναι ένας κόμβος άμεσης κλήσης συνάρτησης;
if ($node instanceof Php\Expression\FunctionCallNode
&& $node->name instanceof Php\NameNode
&& isset($forbiddenFunctions[strtolower((string) $node->name)])
) {
throw new SecurityViolationException(
"Η συνάρτηση {$node->name}() δεν επιτρέπεται.",
$node->position,
);
}
},
);
}
Εξήγηση:
- Ορίζουμε μια λίστα απαγορευμένων ονομάτων συναρτήσεων.
- Ο visitor
enter
ελέγχει τοFunctionCallNode
. - Αν το όνομα της συνάρτησης (
$node->name
) είναι στατικόNameNode
, ελέγχουμε την αναπαράστασή του σε πεζά γράμματα έναντι της απαγορευμένης λίστας μας. - Αν βρεθεί απαγορευμένη συνάρτηση, πετάμε
Latte\SecurityViolationException
, η οποία υποδεικνύει σαφώς την παραβίαση του κανόνα ασφαλείας και σταματά τη μεταγλώττιση.
Αυτά τα παραδείγματα δείχνουν πώς τα περάσματα μεταγλώττισης με τη
χρήση του NodeTraverser
μπορούν να αξιοποιηθούν για ανάλυση,
αυτόματες τροποποιήσεις και επιβολή περιορισμών ασφαλείας
αλληλεπιδρώντας άμεσα με τη δομή AST του προτύπου.
Βέλτιστες πρακτικές
Κατά τη συγγραφή περασμάτων μεταγλώττισης, λάβετε υπόψη αυτές τις οδηγίες για τη δημιουργία στιβαρών, συντηρήσιμων και αποτελεσματικών επεκτάσεων:
- Η σειρά έχει σημασία: Έχετε υπόψη τη σειρά με την οποία
εκτελούνται τα περάσματα. Αν το πέρασμά σας εξαρτάται από τη δομή AST που
δημιουργήθηκε από άλλο πέρασμα (π.χ. βασικά περάσματα του Latte ή άλλο
προσαρμοσμένο πέρασμα), ή αν άλλα περάσματα μπορεί να εξαρτώνται από
τις τροποποιήσεις σας, χρησιμοποιήστε τον μηχανισμό ταξινόμησης που
παρέχεται από το
Extension::getPasses()
για να ορίσετε εξαρτήσεις (before
/after
). Δείτε την τεκμηρίωση τουExtension::getPasses()
για λεπτομέρειες. - Μία ευθύνη: Επιδιώξτε περάσματα που εκτελούν μία καλά καθορισμένη εργασία. Για σύνθετους μετασχηματισμούς, εξετάστε το ενδεχόμενο διαχωρισμού της λογικής σε πολλαπλά περάσματα – ίσως ένα για ανάλυση και ένα άλλο για τροποποίηση βάσει των αποτελεσμάτων της ανάλυσης. Αυτό βελτιώνει τη σαφήνεια και τη δυνατότητα δοκιμής.
- Απόδοση: Να θυμάστε ότι τα περάσματα μεταγλώττισης προσθέτουν
χρόνο στη μεταγλώττιση του προτύπου (αν και αυτό συνήθως συμβαίνει
μόνο μία φορά, μέχρι να αλλάξει το πρότυπο). Αποφύγετε υπολογιστικά
δαπανηρές λειτουργίες στα περάσματά σας, αν είναι δυνατόν. Αξιοποιήστε
τις βελτιστοποιήσεις διάσχισης όπως
NodeTraverser::DontTraverseChildren
καιNodeTraverser::StopTraversal
όποτε γνωρίζετε ότι δεν χρειάζεται να επισκεφθείτε ορισμένα τμήματα του AST. - Χρησιμοποιήστε το
NodeHelpers
: Για κοινές εργασίες όπως η εύρεση συγκεκριμένων κόμβων ή η στατική αξιολόγηση απλών εκφράσεων, ελέγξτε αν τοLatte\Compiler\NodeHelpers
προσφέρει μια κατάλληλη μέθοδο πριν γράψετε τη δική σας λογικήNodeTraverser
. Μπορεί να εξοικονομήσει χρόνο και να μειώσει την ποσότητα του προπαρασκευαστικού κώδικα. - Χειρισμός σφαλμάτων: Αν το πέρασμά σας ανιχνεύσει ένα σφάλμα ή
μια άκυρη κατάσταση στο AST του προτύπου, πετάξτε
Latte\CompileException
(ήLatte\SecurityViolationException
για θέματα ασφαλείας) με ένα σαφές μήνυμα και το σχετικό αντικείμενοPosition
(συνήθως$node->position
). Αυτό παρέχει χρήσιμη ανατροφοδότηση στον προγραμματιστή του προτύπου. - Idempotence (αν είναι δυνατόν): Ιδανικά, η εκτέλεση του περάσματός σας πολλές φορές στο ίδιο AST θα πρέπει να παράγει το ίδιο αποτέλεσμα με την εκτέλεσή του μία φορά. Αυτό δεν είναι πάντα εφικτό, αλλά απλοποιεί τον εντοπισμό σφαλμάτων και τη συλλογιστική σχετικά με τις αλληλεπιδράσεις των περασμάτων, εάν επιτευχθεί. Για παράδειγμα, βεβαιωθείτε ότι το πέρασμα τροποποίησής σας ελέγχει αν η τροποποίηση έχει ήδη εφαρμοστεί πριν την εφαρμόσει ξανά.
Ακολουθώντας αυτές τις πρακτικές, μπορείτε να αξιοποιήσετε αποτελεσματικά τα περάσματα μεταγλώττισης για να επεκτείνετε τις δυνατότητες του Latte με ισχυρό και αξιόπιστο τρόπο, συμβάλλοντας σε ασφαλέστερη, βελτιστοποιημένη ή πιο πλούσια σε λειτουργίες επεξεργασία προτύπων.