Μία προσέγγιση ανάπτυξης στο web βασισμένη σε κόμβους
Από τα πρώτα του βήματα, ο Παγκόσμιος Ιστός (World Wide Web, WWW) υιοθέτησε την έννοια του URL, ώστε να παρέχει ένα μηχανισμό προσδιορισμού της τοποθεσίας καθενός από τους αμέτρητους πόρους του. Στις περισσότερες περιπτώσεις, αφότου το τμήμα του scheme (ή protocol) αναγνωριστεί από το πρόγραμμα πελάτη, χρησιμοποιείται ένας nameserver για να αντιστοιχηθεί το κομμάτι του domain με μία διεύθυνση IP, κι έπειτα η αίτηση (request) αποστέλλεται στην κατάλληλη “πόρτα” (port) ενός web server για περαιτέρω επεξεργασία. Αυτός, με τη σειρά του, αναλύει την αίτηση και τελικά παράγει μία απόκριση (response).
Ένα θεμελιώδες τμήμα της παραπάνω ανάλυσης αίτησης, είναι η αντιστοίχιση της διαδρομής προς το ζητούμενο πόρο. Για να το κάνει αυτό, ο web server χρησιμοποιεί έναν προκαθορισμένο μηχανισμό όπου, στην απλούστερη μορφή του, υλοποιείται σαν μία 1-προς-1 αντιστοίχιση με ένα τμήμα του τοπικού συστήματος αρχείων. Για παράδειγμα ένα URL σαν το http://www.example.com/path/to/image.png θα αντιστοιχούσε σε μία τοπική διαδρομή στο σύστημα αρχείων σαν την /var/www/path/to/image.png (αν υποθέσουμε ότι το /var/www είναι το “document root” του server). Αν η διαδρομή μπορέσει να ακολουθηθεί, το αρχείο επιστρέφεται ως απόκριση. Αλλιώς το πρόγραμμα πελάτης θα λάμβανε, τυπικά, έναν κωδικό HTTP 404.
Αυτή η υλοποίηση είναι παραπάνω από επαρκής για τις περισσότερες περιπτώσεις, διότι παρέχει μία σαφή αντιστοιχία, από το web στο τοπικό σύστημα, όχι μόνο για στατικούς πόρους αλλά και για δυναμικούς. Στη δεύτερη περίπτωση, ο πόρος θεωρείται ότι είναι μία διαδικασία (όπως ένα script) που χρειάζεται να εκτελεστεί για να παράγει την απόκριση σε πραγματικό χρόνο. Κατά τη φάση εκτέλεσης, το script λαμβάνει υπόψιν διάφορες άλλες παραμέτρους, κάποιες από τις οποίες χρησιμοποιούνται για να επεκτείνουν τις “συντεταγμένες” του ζητούμενου πόρου πέρα από την τοποθεσία του script. Μερικές από τις πιο κοινές πρακτικές προς αυτή την κατεύθυνση είναι οι εξής:
- Η μεταβλητή PATH_INFO, που ακολουθεί το script, π.χ. http://www.example.com/path/to/script.php/extra/path/info
- Το τμήμα QUERY του URL, π.χ. http://www.example.com/path/to/script.php?area=extra&segment=path&item=info
- Μία συνδυασμένη προσέγγιση, που παρέχει μία διαδρομή σαν όρισμα της QUERY, π.χ. http://www.example.com/path/to/script.php?path=/extra/path/info
Μπορούμε να εφαρμόσουμε rewrite rules στον web server για να παρέχουμε στο χρήστη πιο φιλικά και όμορφα URLs, ενώ το script θα συνεχίζει να έχει πρόσβαση στα διάφορα τμήματα του URL με τον τρόπο που σχεδιάστηκε αρχικά. Ωστόσο το αντικείμενο αυτού του άρθρου δεν είναι αυτό. Είναι το γεγονός ότι, καμιά φορά ένα τμήμα του URL θα πρέπει να είναι εικονικό (virtual), και η διαχείρισή του να γίνεται από το script. Επίσης, αφορά καταστάσεις όπου, από την τοποθεσία του script και κάτω, η ιεραρχία παραμένει δενδροειδής — με άλλα λόγια, ένα namespace. Από εδώ και πέρα, θα περιορίσουμε την έννοια της διαδρομής σε αυτό το namespace, και θα το απεικονίζουμε σαν μία τυπική διαδρομή, ανεξάρτητα από την επιλεγμένη τεχνική υλοποίησης.
Ο Κόμβος (Node)
Έστω ότι το script.php πρέπει να διαχειριστεί το παρακάτω namespace, που έχει απεικονιστεί σαν ένα array της php για να είναι πιο αναγνώσιμο:
$namespace=array(
'water'=>array(
'fish'=>array('bass','trout','barracuda'),
'misc'=>array('seaweed','crab'),
),
'air'=>array(
'eagle',
'bee'=>array('bee-hive'),
),
'admin'=>array(
'users'=>array('alice','bob','charlie'),
'articles'=>array('bass','trout','barracuda','seaweed','crab','eagle','bee-hive'),
'categories'=>array(
'water',
'air'=>array('bee'),
),
),
);
Αυτό προσομοιάζει (ελάχιστα…) με ένα τμήμα ενός απλού, φανταστικού CMS. Η πρόσβαση σε κάθε άρθρο γίνεται μέσω της πλήρης διαδρομής της κατηγορίας του, αφού οι κατηγορίες είναι ιεραρχημένες. Αλλά στην περίπτωση της επεξεργασίας, τα άρθρα είναι προσβάσιμα μέσω μίας διαδρομής του τύπου /admin/articles/{id}. Το ίδιο ισχύει για χρήστες και κατηγορίες, μόνο που για τις τελευταίες, η ιεράρχηση διατηρείται.
Όπως όλες τις δενδροειδείς δομές, το namespace αποτελείται από κλαδιά και φύλλα. Ένα φύλλο είναι ένας κόμβος χωρίς θυγατρικούς, ενώ ένα κλαδί το αντίθετο. Παρόλο που αυτό ισχύει από δομικής πλευράς, υπάρχει ένας άλλος, σημαντικότερος διαχωρισμός κλαδιών και φύλλων: Αυτός που αφορά την αίτηση. Όταν, για παράδειγμα, ζητείται το /admin/users, o κομβος-φύλλο (τερματικός), για την αίτηση, είναι ο users. Αυτός ο κόμβος θα πρέπει να αποκριθεί με, ας πούμε, μία λίστα χρηστών για να επιλέξει ο διαχειριστής. Με άλλα λόγια, θα πρέπει να ενεργοποιήσει μία ρουτίνα “χειρισμού περιεχομένου” προκειμένου να παράγει την απόκριση. Παρακάτω, όταν αναφερόμαστε σε κόμβους φύλλα ή τερματικούς, θα το εννοούμε από τη σκοπιά της αίτησης.
Τώρα, τι θα συνέβαινε αν ζητούσαμε το /air/bee/bee-hive/print; Επειδή το print πλεονάζει, προφανώς το bee-hive είναι η τελευταία μας ευκαιρία να εξυπηρετήσουμε την αίτηση. Παρ’ όλα αυτά, η πλεονάζουσα διαδρομή θα πρέπει να δοθεί στο “χειριστή περιεχομένου” της bee-hive ως ένα πιθανό σύνολο παραμέτρων.
Προκειμένου να εξυπηρετηθεί μία αίτηση, θα πρέπει να διατρέξουμε το δένδρο από τη ρίζα ως το ζητούμενο φύλλο. Έτσι, όλοι οι πατρικοί κόμβοι του φύλλου θα έχουν την ευκαιρία να διαμορφώσουν το περιβάλλον μέσα στο οποίο θα εκτελεστεί ο χειριστής περιεχομένου. Για παράδειγμα, ο bass θα έχει διαφορετική συμπεριφορά, ανάλογα αν η πρόσβαση γίνεται μέσω του /water/fish ή του /admin/articles. Αυτό υπονοεί ότι, ένας κόμβος δεν αρκεί να ξέρει τους θυγατρικούς του, αλλά θα πρέπει να γνωρίζει και τον πατρικό του. Γνωρίζοντας τον πατρικό ενός κόμβου, αυτόματα γνωρίζουμε και τον πατρικό του πατρικού του και, αναδρομικά, μπορούμε τελικά να φτάσουμε ως τη ρίζα (δεδομένου ότι ο πατρικός της ρίζας θα είναι πάντα NULL).
Τώρα, ας ορίσουμε έναν βασικό, αφηρημένο κόμβο που ενσωματώνει όλα τα παραπάνω:
<?php
abstract class NodeBase
{
private $parent=null;
private $nodes=array();
final function parent() { return $this->parent; }
final function root() { return ($this->parent?$this->parent->root():$this); }
function __construct(NodeBase $parent=NULL)
{
$this->parent=$parent;
}
abstract function create_my_nodes();
final function traverse(array $path)
{
$this->nodes=$this->create_my_nodes();
if(($i=array_shift($path))&&isset($this->nodes[$i])&&($node=$this->nodes[$i]))
$node->traverse($path); // there is a child named $i
else // reached the end of the path, or the remaining cannot be resolved
{
array_unshift($path,$i);
$this->leaf_execute($path);
}
}
abstract function leaf_execute();
}
Η μέθοδος leaf_execute() παίζει το ρόλο του χειριστή περιεχομένου. Η υλοποίησή της, όπως και της create_my_nodes(), που ορίζει τους θυγατρικούς ενός κόμβου, αφήνεται στους πιο συγκεκριμένους τύπους κόμβων. Η πρώτη θα πρέπει να δίνει σαν έξοδο το κατάλληλο περιεχόμενο, αν ο κόμβος θα πρέπει να συμπεριφερθεί ως τερματικός, ενώ η δεύτερη θα πρέπει να επιστρέφει ένα associative array με τους θυγατρικούς κόμβους. Θα παρατηρήσατε ότι η create_my_nodes() δεν καλείται μέσα στον constructor του κόμβου, αλλά μόλις πριν αποφασίσουμε αν θα διατρέξουμε περαιτέρω το δένδρο. Όχι μόνο διότι θα ήταν άσκοπο να γίνει πιο πριν, αλλά επίσης διότι η κατασκευή νέων κόμβων μέσα στον constructor μπορεί να δημιουργήσει σοβαρά προβλήματα — αλλά θα έρθουμε σ’ αυτό παρακάτω…
Πέρα από αυτά, η traverse() είναι αρκετά σαφής: Η παράμετρος $path είναι ένα array που περιέχει τη διαδρομή που απομένει να ακολουθηθεί. Αν το πρώτο στοιχείο αντιστοιχεί σε θυγατρικό κόμβο, εκείνος διατρέχεται αναδρομικά έχοντας σαν παράμετρο την υπόλοιπη διαδρομή. Αλλιώς, ο τρέχων κόμβος θεωρείται τερματικός, και η πλεονάζουσα διαδρομή, αν υπάρχει, παραδίδεται στο χειριστή περιεχομένου του.
Παράδειγμα: Μία ρίζα χωρίς θυγατρικούς
Για να παρουσιάσουμε μία απλή leaf_execute() όπως επίσης και την απλούστερη δυνατή εφαρμογή, ας υποθέσουμε ότι ο παραπάνω κώδικας βρίσκεται σε ένα αρχείο nodebase.class.php. Έστω το παρακάτω script.php:
<?php
include "nodebase.class.php";
class MyRoot extends NodeBase
{
function create_my_nodes() {return array();} // no children for me please!
function leaf_execute()
{
echo "Hello, I'm a childless ".get_class();
}
}
$root=new MyRoot(NULL);
$root->traverse(array());
Όταν εκτελείται το script.php, το πρόγραμμα-πελάτης θα πρέπει να λάβει ένα μήνυμα που θα λέει “Hello, I’m a childless MyRoot”. Παρατηρήστε ότι η MyRoot::create_my_nodes() επιστρέφει ένα κενό array, εννοώντας ότι δεν έχει θυγατρικούς κόμβους. Επιπλέον, όταν δημιουργούμε το στιγμιότυπο του ριζικού κόμβου, του δίνουμε NULL πατρικό κόμβο, όπως ταιριάζει σε όλες τις ρίζες. Τέλος, διατρέχουμε όλη τη “δομή” χρησιμοποιώντας ένα κενό array ως διαδρομή, απλά επειδή δεν υπάρχει διαδρομή για να ακολουθήσουμε.
Παράδειγμα: Ένα άπειρο δέντρο
Αυτό το παράδειγμα θα δείξει τη δημιουργία αφ’ ενός θυγατρικών κόμβων και αφ’ ετέρου ενός namespace. Έχει επίσης σκοπό να καταδείξει το λόγο για τον οποίο οι θυγατρικοί κάποιου κόμβου θα πρέπει να δημιουργούνται το αργότερο δυνατόν.
Θεωρήστε τις παρακάτω κλάσεις, αποθηκευμένες σε ένα infinite.php:
<?php
class Branch extends NodeBase
{
function create_my_nodes()
{
return array(
'branch'=>new Branch($this), // branch over and over
'leaf'=>new Leaf($this),
);
}
function leaf_execute()
{
echo "Hi, I'm a branch";
}
}
class Leaf extends NodeBase
{
function create_my_nodes() {return array();} // a leaf has no children
function leaf_execute()
{
echo "Hi, I'm a leaf";
}
}
Είναι φανερό ότι ένας κόμβος Branch έχει πάντα δύο θυγατρικούς: Έναν Leaf και έναν επίσης Branch. Αναδρομικά, ο δεύτερος θα έχει επίσης έναν Leaf και έναν Branch, κ.ο.κ μέχρι το άπειρο. Αν κατασκευάζαμε τους θυγατρικούς ενός κόμβου κατά τη δική του δημιουργία, θα δημιουργούσαμε ολόκληρο το namespace πριν καν διατρέξουμε τη ρίζα! Πέρα από την άσκοπη δημιουργία ενός, δυνητικά, μεγάλου namespace, στη δική μας περίπτωση θα οδηγούσαμε την εκτέλεση σε ατέρμονη αναδρομή (infinite recursion).
Τώρα θεωρήστε το ακόλουθο script.php. Αυτή τη φορά θα χρησιμοποιήσουμε τη μεταβλητή PATH_INFO για τον προσδιορισμό της ζητούμενης διαδρομής.
<?php
include "nodebase.class.php";
include "infinite.php";
class MyRoot extends NodeBase
{
function create_my_nodes()
{
return array('test'=>new Branch($this));
}
function leaf_execute()
{
echo "The Root Of an Infinite tree!";
}
}
$TheRoot=new MyRoot(null);
$p=isset($_SERVER['PATH_INFO'])?$_SERVER['PATH_INFO']:'/';
$path=explode('/',trim($p,'/'));
$TheRoot->traverse($path);
Η φιλοσοφία είναι η ίδια με τα προηγούμενα. Ένας κόμβος MyRoot κατασκευάζεται, με έναν θυγατρικό τύπου Branch ονόματι “test“. Επιπλέον, χρησιμοποιούμε τη μεταβλητή PATH_INFO για να παράγουμε το array που αντιπροσωπεύει τη ζητούμενη διαδρομή. Έγκυρες διαδρομές θεωρούνται οι /, /test, /test/leaf, /test/branch, /test/branch/leaf, /test/branch/branch, /test/branch/branch/leaf κ.ο.κ.
Αυτά τα δύο παραδείγματα μόλις που ακουμπούν την επιφάνεια μίας τέτοιας τεχνικής προγραμματισμού. Έπονται περισσότερα, σε επόμενα άρθρα, συμπεριλαμβανομένου ίσως και του αρχικού “CMS”.
Να είστε ασφαλείς!
