ΚΕΝΤΡΟ ΠΛΗ.ΝΕ.Τ. Ν. ΦΛΩΡΙΝΑΣ

Προχωρημένα Θέματα στην PHP

  

Οι Κανονικές Εκφράσεις (Regular Expressions)

Μια κανονική έκφραση (regular expression) είναι ένα κομμάτι (string) κειμένου που περιέχει ειδικούς κωδικούς που του δίνουν τη δυνατότητα να μπορεί να χρησιμοποιηθεί με κάποιες συναρτήσεις της PHP για εντοπισμό και διαχείριση κειμένου. Για παράδειγμα, η επόμενη είναι μια κανονική έκφραση που κάνει αναζήτηση για το κείμενο PHP :

PHP

Για να μπορέσουμε να χρησιμοποιήσουμε μια κανονική έκφραση, πρέπει να είμαστε εξοικειωμένοι με τις συναρτήσεις των κανονικών εκφράσεων που υπάρχουν στην PHP. Η ereg() είναι η βασικότερη απ’ αυτές και μπορεί να χρησιμοποιηθεί για να καθορίσουμε αν μια κανονική έκφραση ικανοποιείται από ένα συγκεκριμένο string κειμένου. Ας δούμε τον επόμενο κώδικα :

$text = "Κανόνες της PHP!";

if (ereg("PHP", $text)) {

echo( '$το κείμενο περιέχει το string "PHP".' );

} else {

echo( '$το κείμενο δεν περιέχει το string "PHP".' );

}

Σ’ αυτό το παράδειγμα, η κανονική έκφραση ικανοποιείται επειδή το string που είναι αποθηκευμένο στη μεταβλητή $text περιέχει το PHP. Ο παραπάνω κώδικας θα εξάγει συνεπώς τα εξής :

$το κείμενο περιέχει το string "PHP".

Δεν πρέπει να ξεχνάμε όταν τα μονά εισαγωγικά αποτρέπουν την PHP από το να εμφανίσει την τιμή της μεταβλητής $text.

Η eregi() είναι μια συνάρτηση που συμπεριφέρεται σχεδόν παρόμοια με την ereg(), εκτός από το ότι αγνοεί τα πεζά και τα κεφαλαία γράμματα όταν αναζητά ταιριάσματα :

$text = "Τι είναι η Php;";

if (eregi("PHP", $text)) {

echo( '$το κείμενο περιέχει το string "PHP".' );

} else {

echo( '$το κείμενο δεν περιέχει το string "PHP".' );

}

Αυτό εμφανίζει πάλι το ίδιο μήνυμα :

$το κείμενο περιέχει το string "PHP".

Θα δούμε μερικά παραδείγματα για να μάθουμε τη βασική σύνταξη των κανονικών εκφράσεων. Πρώτα απ’ όλα, το σύμβολο caret (Ù) μπορεί να χρησιμοποιηθεί για να δείξει την αρχή ενός string, ενώ το σύμβολο του δολαρίου ($) χρησιμοποιείται για να δείξει το τέλος :

PHP                // Ταιριάζει με το "What is PHP?"

ÙPHP             // Ταιριάζει με το "PHP rules!", όχι με το "What is PHP?"

PHP$              // Ταιριάζει με το "I love PHP", όχι με το "What is PHP?"

ÙPHP$           // Ταιριάζει με το "PHP" και τίποτα άλλο

Προφανώς, θα υπάρχουν φορές που θα θέλουμε να χρησιμοποιήσουμε τα σύμβολα ^, $ ή και άλλους ειδικούς χαρακτήρες για να συμπεριλάβουμε τον αντίστοιχο χαρακτήρα στο string αναζήτησης. Για να αφαιρέσουμε το ειδικό νόημα ενός χαρακτήρα, προτάσσουμε τον χαρακτήρα \ (backslash), ως εξής : 

\$\$\$  // Ταιριάζει με το "Show me the $$$!"

Οι αγκύλες μπορούν να χρησιμοποιηθούν για να ορίσουν ένα σύνολο χαρακτήρων που μπορεί να ταιριάξει. Για παράδειγμα, η ακόλουθη κανονική έκφραση ταιριάζει μ’ ένα μόνο από τα ψηφία 1 έως 5.

[12345]   // Ταιριάζει με το "1" και το "3", αλλά όχι με το "a" ή το "12"

Μπορούμε να καθορίσουμε και περιοχές αριθμών ή γραμμάτων.

[1-5]               // Το ίδιο όπως προηγουμένως

[a-z]               // Ταιριάζει μ’ ένα οποιοδήποτε πεζό γράμμα

[0-9a-zA-Z]   // Ταιριάζει μ’ ένα οποιοδήποτε γράμμα ή ψηφίο

Οι χαρακτήρες ?, + και * έχουν επίσης ειδικό νόημα. Συγκεκριμένα, το ? σημαίνει ότι ο προηγούμενος χαρακτήρας είναι προαιρετικός, το + σημαίνει έναν ή περισσότερους από τους προηγούμενους χαρακτήρες και το * σημαίνει κανέναν ή έναν από τους προηγούμενους χαρακτήρες.

bana?na        // Ταιριάζει με τα "banana" και "banna",

// αλλά όχι με το "banaana"

bana+na        // Ταιριάζει με τα "banana" και "banaana",

// αλλά όχι με το "banna"

bana*na        // Ταιριάζει με τα "banna", "banana" και "banaaana",

// αλλά όχι με το "bnana"

Ù[a-zA-z]+$  // Ταιριάζει μ’ ένα οποιοδήποτε string που έχει έναν

// τουλάχιστον χαρακτήρα

Οι παρενθέσεις μπορούν να χρησιμοποιηθούν για να ομαδοποιήσουμε strings και να εφαρμόσουμε τα ?, + ή * σ’ αυτά σαν σύνολο.

ba(na)+na     // Ταιριάζει με τα "banana" και "banananana",

// αλλά όχι με το "bana" ή το "banaana"

Ακολουθούν μερικοί κωδικοί για να μπορέσουμε να ταιριάξουμε τους ειδικούς χαρακτήρες στις κανονικές εκφράσεις :

\n         // Ταιριάζει μ’ έναν χαρακτήρα νέας γραμμής (newline character)

Ù         // Ταιριάζει μ’ έναν οποιονδήποτε χαρακτήρα εκτός από τον

            // χαρακτήρα νέας γραμμής

\r         // Ταιριάζει μ’ έναν χαρακτήρα carriage return

\t          // Ταιριάζει με τον χαρακτήρα tab

 

Αντικατάσταση Strings με Κανονικές Εκφράσεις

Χρησιμοποιώντας την ereg() ή την eregi() με τη σύνταξη των κανονικών εκφράσεων που μόλις είδαμε, μπορούμε να εντοπίσουμε εύκολα την παρουσία tags σ’ ένα δεδομένο string κειμένου. Αυτό που πρέπει να κάνουμε, όμως, είναι να σημειώσουμε με ακρίβεια αυτά τα tags και να τα αντικαταστήσουμε με κατάλληλα HTML tags.

Για να γίνει αυτό, πρέπει να δούμε μερικές ακόμα συναρτήσεις κανονικών εκφράσεων που υπάρχουν στην PHP : την ereg_replace() και την eregi_ replace().

Η ereg_replace(), σαν την ereg(), δέχεται μια κανονική έκφραση και ένα string κειμένου και προσπαθεί να ταιριάξει την κανονική έκφραση με το string. Επιπλέον, όμως, η ereg_replace() δέχεται ένα δεύτερο string κειμένου και αντικαθιστά κάθε ταίριασμα της κανονικής έκφρασης σ’ αυτό το string. Η σύνταξη της ereg_replace() είναι ως εξής :

$newstring = ereg_replace(<regexp>, <replacewith>, <oldstring>);

όπου η <regexp> είναι η  κανονική έκφραση και το <replacewith> είναι το string που θα αντικαταστήσει τα ταιριάσματα στην <regexp> όπου υπάρχει το <oldstring>. Η συνάρτηση επιστρέφει το νέο string που προκύπτει από τη λειτουργία της αντικατάστασης. Στην παραπάνω πρόταση, αυτό απoθηκεύεται στη μεταβλητή $newstring.

Η eregi_replace(), όπως είναι αναμενόμενο, είναι παρόμοια με την ereg_replace(), εκτός από το ότι δεν ελέγχει τα πεζά/κεφαλαία όταν κάνει αναζήτηση για ταιριάσματα.

Είμαστε τώρα έτοιμοι να αρχίσουμε να δημιουργούμε τη δική μας προσαρμοσμένη γλώσσα σήμανσης (custom markup language).

 

Έντονο και Πλάγιο Κείμενο

Θα ξεκινήσουμε υλοποιώντας tags για να δημιουργήσουμε έντονο και πλάγιο κείμενο. Ας υποθέσουμε ότι θέλουμε το [B] να ξεκινάει το έντονο κείμενο (bold text) και το [EB] να τελειώνει το έντονο κείμενο. Προφανώς, θα πρέπει να αντικαταστήσουμε το [B] με το <B> και το [EB] με το </B>.

Η λειτουργία αυτή αποτελεί μια απλή εφαρμογή της συνάρτησης eregi_replace() :

$joketext = eregi_replace("\[b]", "<B>", $joketext);

$joketext = eregi_replace("\[eb]", "</B>", $joketext);

Εφόσον το [ κανονικά δείχνει την αρχή ενός συνόλου αποδεκτών χαρακτήρων σε μια κανονική έκφραση, τοποθετούμε τον χαρακτήρα \ (backslash) πριν απ’ αυτόν για να αφαιρέσουμε το ειδικό νόημα που έχει. Χωρίς ένα αντίστοιχο [, το ] χάνει το ειδικό νόημα που έχει και δεν χρειάζεται συνεπώς έναν χαρακτήρα backslash, αν και θα μπορούσαμε να τοποθετήσουμε ένα backslash μπροστά του επίσης.

Παρατηρούμε επίσης, ότι, εφόσον χρησιμοποιούμε τη συνάρτηση eregi_replace(), η οποία δεν ξεχωρίζει τα πεζά από τα κεφαλαία γράμματα (case insensitive), το [B] και το [b] θα εργάζονται σαν tags στη δική μας προσαρμοσμένη γλώσσα σήμανσης (custom markup language).

Το πλάγιο κείμενο μπορεί να γίνει με τον ίδιο τρόπο :

$joketext = eregi_replace("\[i]", "<I>", $joketext);

$joketext = eregi_replace("\[ei]", "</I>", $joketext);

 

Οι Παράγραφοι

Ενώ μπορούμε να δημιουργήσουμε tags για παραγράφους όπως ακριβώς κάναμε για το έντονο και πλάγιο κείμενο, υπάρχει και μια πιο απλή λύση. Εφόσον ο χρήστης θα καταχωρήσει το περιεχόμενο σ’ ένα πεδίο φόρμας (form field) που θα του δίνει τη δυνατότητα να μορφοποιήσει το κείμενο χρησιμοποιώντας το πλήκτρο enter, θα ορίσουμε το linefeed (\n) να δείχνει μια αλλαγή γραμμής (line break), <BR>) και το διπλό linefeed (\n\n) να δείχνει μια νέα παράγραφο (<P>).

Φυσικά, εφόσον τα PC's παριστάνουν τις νέες γραμμές σαν ένα ζευγάρι του linefeed με το carriage return (\n\r) θα πρέπει πρώτα να αφαιρέσουμε τα carriage returns. Ο κώδικας γι’ όλα αυτά είναι ο εξής :

// Αφαιρεί τα carriage returns

$joketext = ereg_replace("\r", "", $joketext);

// Χειρισμός των παραγράφων

$joketext = ereg_replace("\n\n", "<P>", $joketext);

// Χειρισμός των αλλαγών γραμμής (line breaks)

$joketext = ereg_replace("\n", "<BR>", $joketext);

Το κείμενο θα εμφανίζεται τώρα σε παραγράφους όπως θα ανέμενε ο χρήστης και δεν χρειάζεται να μάθει να δημιουργεί προσαρμοσμένα tags για να το κάνει.

 

Οι Υπερσύνδεσμοι (Hyperlinks)

Ενώ μπορεί να φανεί ανόητο το να υποστηρίζουμε τους υπερσυνδέσμους (hyperlinks) στο κείμενο των jokes, αυτό το χαρακτηριστικό έχει νόημα σ’ άλλες εφαρμογές. Οι υπερσύνδεσμοι είναι λίγο περισσότερο πολύπλοκοι από την απλή μετατροπή ενός κώδικα σ’ ένα HTML tag. Πρέπει να μπορούμε να εξάγουμε ένα URL όπως και το κείμενο που πρέπει να εμφανίζεται σαν ο σύνδεσμος (link). 

Ένα άλλο χαρακτηριστικό των συναρτήσεων ereg_replace() και eregi_replace() θα φανεί εδώ. Περιβάλλοντας ένα τμήμα της κανονικής έκφρασης με παρενθέσεις, μπορούμε να αποσπάσουμε το αντίστοιχο τμήμα του κειμένου που ταιριάζει και να το χρησιμοποιήσουμε στο string αντικατάστασης με τον κώδικα \\n, όπου το n είναι 1 για το πρώτο τμήμα της κανονικής έκφρασης που βρίσκεται σε παρενθέσεις, 2 για το δεύτερο και έως 9 για το ένατο. Ας δούμε το ακόλουθο παράδειγμα :

$text = "banana";

$text = eregi_replace("(.*)(nana)", "\\2\\1", $text);

echo($text);               // εμφανίζει "nanaba"

Στο παραπάνω, το \\1 αντικαθίσταται με το ba στο string αντικατάστασης, το οποίο αντιστοιχεί στο (.*) (κανένας ή περισσότεροι χαρακτήρες διάφοροι της νέας γραμμής, new line) στην κανονική έκφραση. Το \\2 αντικαθίσταται με το nana, το οποίο αντιστοιχεί στο (nana) στην κανονική έκφραση.

Η ίδια αρχή μπορεί να χρησιμοποιηθεί για να δημιουργήσουμε τους υπερσυνδέσμους μας (hyperlinks). Θα ξεκινήσουμε με μια απλή φόρμα ενός link, όπου το κείμενο του link είναι το ίδιο με το URL. Θέλουμε να υποστηρίξουμε την εξής σύνταξη :

Visit [L]http://www.php.net/[EL].

            Ο αντίστοιχος HTML κώδικας είναι ο εξής :

Visit <A HREF="http://www.php.net/"> http://www.php.net/ </A>

Πρώτα, χρειαζόμαστε μια κανονική έκφραση που θα ταιριάζει με τους συνδέσμους (links) αυτής της φόρμας, ως εξής :

\[L][-_./a-zA-Z0-9!&%#?,'=:~]+\[EL]

Ξανά, έχουμε τοποθετήσει backslashes μπροστά από τις αγκύλες στα [L] και [EL] για να δείξουμε ότι θα τα αντιμετωπίσουμε κυριολεκτικά. Χρησιμοποιούμε μετά αγκύλες (square brackets) για να εμφανίσουμε όλους τους χαρακτήρες που θέλουμε να δεχθούμε σαν μέρος του URL. Τοποθετούμε ένα + μετά από τις αγκύλες για να δείξουμε ότι το URL θα αποτελείται από έναν ή περισσότερους χαρακτήρες που παίρνονται από τη λίστα.

Για να εξάγουμε το link, θα πρέπει να βρούμε το URL και να το εξάγουμε σαν το HREF attribute του tag Α καθώς και σαν το κείμενο του link. Για να βρούμε το URL, περιβάλλουμε το αντίστοιχο τμήμα της κανονικής έκφρασης με παρενθέσεις :

\[L]([-_./a-zA-Z0-9!&%#?,'=:~]+)\[EL]

Έτσι, κάνουμε τη μετατροπή link με τον εξής κώδικα :

$joketext = ereg_replace(

"\[L]([-_./a-zA-Z0-9!&%#?,'=:~]+)\[EL]",

"<A HREF=\"\\1\">\\1</A>", $joketext);

Έπρεπε να τοποθετήσουμε backslashes μπροστά από τα διπλά εισαγωγικά στον HTML κώδικα για να μην τα μπερδέψει η PHP με τα εισαγωγικά που περιβάλλουν το string αντικατάστασης. Το \\1 αντικαθίσταται με το URL για το link και η έξοδος είναι η αναμενόμενη.

Θα θέλαμε επίσης να υποστηρίξουμε τα hyperlinks που περιέχουν κείμενο link που διαφέρει από τα δικά τους URL. Ας υποθέσουμε ότι η μορφή του link μας είναι ως εξής :

Check out [L=http://www.php.net/]PHP[EL].

Ακολουθεί η κανονική έκφραση :

            \[L=([-_./a-zA-Z0-9!&%#?,'=:~]+)]([-_./a-zA-Z0-9 !&%#?,'=:~]+)\[EL]

Το παραπάνω κάνει αυτό ακριβώς που θέλουμε, καθώς βρίσκει και το URL (\\1) και το κείμενο (\\2) για το link. Ο PHP κώδικας για να γίνει η αντικατάσταση είναι ο εξής :

$joketext = ereg_replace(

"\[L=([-_./a-zA-Z0-9!&%#?,'=:~]+)]".

"([-_./a-zA-Z0-9 !&%#?,'=:~]+)\[EL]",

"<A HREF=\"\\1\">\\2</A>", $joketext);

 

Διαχωρισμός Κειμένου σε Σελίδες

Αν και κανένα joke δεν είναι πιθανό να είναι τόσο μεγάλο που να χρειάζεται περισσότερες από μία σελίδες, πολλά content-driven sites περιέχουν μεγάλο περιεχόμενο που είναι συχνά καλύτερο να παρουσιασθεί διασπασμένο σε σελίδες. Υπάρχει μια ακόμη συνάρτηση στην PHP που το κάνει αυτό πολύ εύκολα. Η split() είναι μια συνάρτηση που δέχεται μια κανονική έκφραση και ένα string κειμένου και χρησιμοποιεί ταιριάσματα για την κανονική έκφραση για να διασπάσει το κείμενο σ’ έναν πίνακα (array). Ας δούμε το ακόλουθο παράδειγμα :

$regexp="[ \n\r\t]+";   // Ένας ή περισσότεροι whitespace characters

$text="This is a test.";

$textarray=split($regexp, $text);

echo("$textarray[0]<BR>");         // Εμφανίζει "This<BR>"

echo("$textarray[1]<BR>");         // Εμφανίζει "is<BR>"

echo("$textarray[2]<BR>");         // Εμφανίζει "a<BR>"

echo("$textarray[3]<BR>");         // Εμφανίζει "test.<BR>"

Αν αντί να αναζητούμε έναν whitespace character αναζητούμε ένα [PAGEBREAK] tag και αντί να εμφανίζουμε όλα τα κομμάτια του κειμένου εμφανίσουμε μόνο τη σελίδα που μας ενδιαφέρει, η οποία δείχνεται από μια μεταβλητή $page που μεταβιβάζεται μαζί με την αίτηση για τη σελίδα (page request), για παράδειγμα, μπορούμε να διαιρέσουμε το περιεχόμενο σε σελίδες.

// Αν δεν έχει καθορισθεί κάποια σελίδα, default είναι η

// πρώτη σελίδα ($page = 0)

if (!isset($page)) $page = 0;

// Διαιρούμε το κείμενο σ’ έναν πίνακα (array) από σελίδες

$textarray=split("\[PAGEBREAK]", $text);

// Επιλέγουμε τη σελίδα που θέλουμε

$pagetext=$textarray[$page];

Φυσικά, θα θελήσουμε να έχουμε κάποιον τρόπο μετακίνησης ανάμεσα στις σελίδες. Θα τοποθετήσουμε έναν σύνδεσμο (link) προς την προηγούμενη σελίδα στην κορυφή της τρέχουσας σελίδας και έναν σύνδεσμο προς την επόμενη σελίδα στη βάση της τρέχουσας σελίδας. Αν αυτή είναι η πρώτη σελίδα δεν θα χρειασθούμε ένα link προς την προηγούμενη σελίδα. Γνωρίζουμε ότι βρισκόμαστε στην πρώτη σελίδα αν το $page είναι ίσο με 0. Παρόμοια, δεν χρειαζόμαστε ένα link προς την επόμενη σελίδα όταν βρισκόμαστε στην τελευταία σελίδα.

Για να είμαστε σίγουροι ότι βρισκόμαστε στην τελευταία σελίδα, χρειαζόμαστε μια συνάρτηση της PHP με όνομα count(), η οποία δέχεται έναν πίνακα (array) και επιστρέφει τον αριθμό των στοιχείων του πίνακα. Αν μεταβιβάσουμε τον πίνακα των σελίδων, η count() θα μας πει πόσες σελίδες υπάρχουν. Αν υπάρχουν 10 σελίδες, τότε το $textarray[9] θα περιέχει την τελευταία σελίδα. Έτσι, γνωρίζουμε ότι βρισκόμαστε στην τελευταία σελίδα αν το $page είναι ίσο με count($textarray) – 1.

Ο κώδικας για τα links μετάβασης ανάμεσα στις σελίδες είναι ο εξής :

if ($page != 0) {

            $prevpage = $page - 1;

            echo("<P><A HREF=\"$PHP_SELF?id=$id&page=$prevpage\">".

                        "Προηγούμενη Σελίδα</A></P>");

}

// Το περιεχόμενο της σελίδας εμφανίζεται εδώ …

if ($page < count($textarray) - 1) {

$nextpage = $page + 1;

echo("<P><A HREF=\"$PHP_SELF?id=$id&page=$nextpage\">".

"Επόμενη Σελίδα</A></P>");

}

Ολόκληρος ο κώδικας για την εμφάνιση του κειμένου του joke είναι ως εξής :

<!-- joke.php -->

...
// Παίρνουμε (get) το κείμενο του joke από τη βάση δεδομένων

$joke = mysql_query("SELECT JokeText FROM Jokes ".

"WHERE ID=$id");

$joke = mysql_fetch_array($joke);

$joketext = $joke["JokeText"];

// Φιλτράρουμε τον HTML κώδικα

$joketext = htmlspecialchars($joketext);

// Αν δεν έχει καθορισθεί κάποια σελίδα, default είναι η πρώτη σελίδα

($page = 0)

if (!isset($page)) $page = 0;

// Διαιρούμε το κείμενο σ’ έναν πίνακα (array) από σελίδες

$textarray=split("\[PAGEBREAK]",$joketext);

// Επιλέγουμε τη σελίδα που θέλουμε

$joketext=$textarray[$page];

// Bold και italics

$joketext = eregi_replace("\[b]","<B>", $joketext);

$joketext = eregi_replace("\[eb]","</B>", $joketext);

$joketext = eregi_replace("\[i]","<I>", $joketext);

$joketext = eregi_replace("\[ei]","</I>", $joketext);

// Παράγραφοι και αλλαγές γραμμών

$joketext = ereg_replace("\r", "", $joketext);

$joketext = ereg_replace("\n\n", "<P>", $joketext);

$joketext = ereg_replace("\n", "<BR>", $joketext);

// Υπερσύνδεσμοι (Hyperlinks)

$joketext = ereg_replace(

            "\[L]([-_./a-zA-Z0-9!&%#?,'=:~]+)\[EL]",

            "<A HREF=\"\\1\">\\1</A>", $joketext);

$joketext = ereg_replace(

            "\[L=([-_./a-zA-Z0-9!&%#?,'=:~]+)]".

            "([-_./a-zA-Z0-9 !&%#?,'=:~]+)\[EL]",

            "<A HREF=\"\\1\">\\2</A>", $joketext);

if ($page != 0) {

            $prevpage = $page - 1;

            echo("<P><A HREF=\"$PHP_SELF?id=$id&page=$prevpage\">".

                        "Προηγούμενη Σελίδα</A></P>");

}

echo( "<P>$joketext" );

if ($page < count($textarray) - 1) {

            $nextpage = $page + 1;

            echo("<P><A HREF=\"$PHP_SELF?id=$id&page=$nextpage\">".

                        "Επόμενη Σελίδα</A></P>");

}

...

 

Αυτόματη Υποβολή Περιεχομένου

Θα δούμε τώρα πώς μπορούμε να δεχόμαστε jokes αλλά να μην εμφανίζονται αμέσως στο site. Θα προσθέσουμε μια νέα στήλη στον πίνακα Jokes με όνομα Visible η οποία θα δέχεται μόνο μία από δύο τιμές : 'Y' και 'N'. Τα νέα jokes που θα υποβάλλονται θα γίνονται ίσα με Visible='N' αυτόματα και δεν θα μπορούν να αποκλειστούν από το να εμφανισθούν στο site, προσθέτοντας απλά την έκφραση WHERE Visible='Y' σ’ ένα ερώτημα (query) του πίνακα Jokes για το οποίο τα αποτελέσματα προορίζονται για κοινή θέα.

Τα jokes που έχουν τη στήλη Visible='N' θα βρίσκονται στη βάση δεδομένων περιμένοντας τροποποίηση από έναν content manager, ο οποίος θα μπορεί να τα τροποποιήσει πριν τα κάνει ορατά ή απλά να τα διαγράψει αμέσως. Η δημιουργία μιας στήλης που μπορεί να περιέχει μία από δύο τιμές, η μια από τις οποίες είναι η προκαθορισμένη (default), έχει να κάνει μ’ ένα νέο είδος στήλης της MySQL που αποκαλείται ENUM :

mysql> ALTER TABLE Jokes ADD COLUMN

-> Visible ENUM('N', 'Y') NOT NULL;

Η πρώτη τιμή που υπάρχει στις παρενθέσεις, δηλ. το 'N' στην προκειμένη περίπτωση, είναι η προκαθορισμένη (default) τιμή, η οποία εκχωρείται στις νέες καταχωρήσεις αν δεν έχει καθορισθεί μια τιμή στην εντολή INSERT.

 

Προχωρημένη SQL

Καθώς έχουμε δουλέψει με το παράδειγμα ενός Internet Joke Database website, είχαμε την ευκαιρία να εξερευνήσουμε τις περισσότερες πλευρές της Structured Query Language (SQL), από την βασική μορφή του ερωτήματος CREATE TABLE μέχρι τους δύο τρόπους σύνταξης των ερωτημάτων INSERT. Θα δούμε τώρα μερικά ακόμα τρικ της SQL που δεν τα έχουμε συναντήσει.

 

Ταξινόμηση με την ORDER BY

Η Order By είναι μια προαιρετική έκφραση σ’ ένα ερώτημα SELECT με την οποία μπορούμε να καθορίσουμε μια στήλη (column) για να ταξινομήσουμε τον πίνακα των αποτελεσμάτων. Για παράδειγμα, για να ταξινομήσουμε με βάση τη στήλη Name του πίνακα Authors, δίνουμε την εξής εντολή :

mysql> SELECT Name, eMail FROM Authors ORDER BY Name;

+-----------------+----------------------+

| Name            | eMail                |

+-----------------+----------------------+

| Antonopoulos Antonis   | ant@hellas.gr   |

| Basileiadis Basileios     | bas@hellas.gr |

| Konstantinou Konstantinos  | kon@hellas.gr  |

| Papadopoulos Nikolaos     | pap@hellas.gr   |

+-----------------+----------------------+

Αν προσθέσουμε τη λέξη κλειδί DESC μετά από το όνομα της στήλης ταξινόμησης, τα αποτελέσματα θα εμφανισθούν σε φθίνουσα σειρά, ως εξής :

mysql> SELECT Name, eMail FROM Authors ORDER BY Name DESC;

+-----------------+----------------------+

| Name            | eMail                |

+-----------------+----------------------+

| Papadopoulos Nikolaos     | pap@hellas.gr   |

| Konstantinou Konstantinos  | kon@hellas.gr  |

| Basileiadis Basileios     | bas@hellas.gr |

| Antonopoulos Antonis   | ant@hellas.gr   |

+-----------------+----------------------+

Μπορούμε να ταξινομήσουμε και με βάση περισσότερες από μία στήλες, χωρίζοντάς τες με κόμμα στην έκφραση ORDER BY, οπότε η MySQL θα ταξινομήσει τις καταχωρήσεις με βάση την πρώτη στήλη, μετά με βάση τη δεύτερη στήλη όπου υπάρχουν ίδιες τιμές στην πρώτη στήλη κοκ. Μπορούμε να χρησιμοποιήσουμε τη λέξη κλειδί DESC σε όποια στήλη θέλουμε στην έκφραση ORDER BY για να αντιστρέψουμε τη σειρά ταξινόμησης.

 

Η Έκφραση LIMIT

Συχνά θα εργαζόμαστε μ’ έναν μεγάλο πίνακα μιας βάσης δεδομένων, αλλά θα μας ενδιαφέρουν λίγες μόνο καταχωρήσεις του. Ας υποθέσουμε ότι θέλουμε να παρακολουθούμε τη δημοτικότητα των διάφορων jokes στο site. Θα μπορούμε να προσθέσουμε μια στήλη με όνομα TimesViewed στον πίνακα Jokes.

Ξεκινώντας με μια τιμή 0 για τα καινούργια jokes και προσθέτοντας το 1 στην τιμή του joke που ζητήθηκε κάθε φορά που εμφανίζεται η σελίδα του joke, θα μπορούμε να παρακολουθούμε πόσες φορές έχει διαβασθεί το κάθε joke στη βάση δεδομένων.

Ο PHP κώδικας για το query που αυξάνει κατά 1 τη στήλη TimesViewed ενός joke μ’ ένα δεδομένο ID, είναι ως εξής :

$sql = "UPDATE Jokes SET TimesViewed=TimesViewed+1 ".

"WHERE ID=$id";

if (!mysql_query($sql)) {

echo("<P> Λάθος στην πρόσθεση ".

"γι' αυτό το joke! </P>\n");

}

Μια κοινή χρήση αυτού του μετρητή θα ήταν για να παρουσιάσουμε μια λίστα των "Top 10 Jokes" στην πρώτη σελίδα του site, για παράδειγμα. Μπορούμε να χρησιμοποιήσουμε την έκφραση ORDER BY TimesViewed DESC για να εμφανίσουμε τα jokes από τις υψηλότερες προς τις χαμηλότερες τιμές της στήλης TimesViewed. Θα πρέπει απλά να επιλέξουμε τις 10 πρώτες τιμές από την κορυφή της λίστας.

Με την έκφραση (clause) LIMIT μπορούμε να καθορίσουμε ότι θέλουμε έναν μόνο συγκεκριμένο αριθμό αποτελεσμάτων. Σύμφωνα με το παραπάνω παράδειγμα, χρειαζόμαστε μόνο τα 10 πρώτα :

$sql = "SELECT * FROM Jokes ORDER BY TimesViewed

DESC LIMIT 10";

Επίσης, θα μπορούμε να αποφύγουμε τη λέξη κλειδί DESC και να εμφανίσουμε τα 10 λιγότερο δημοφιλή jokes. Το επόμενο query θα εμφανίσει από το 21ο έως το 25ο πιο δημοφιλή jokes :

$sql = "SELECT * FROM Jokes ORDER BY TimesViewed

DESC LIMIT 20, 5";

Πρέπει να έχουμε υπόψη μας ότι η πρώτη καταχώρηση στη λίστα των αποτελεσμάτων έχει αριθμό 0 και έτσι η 21η καταχώρηση της λίστας θα έχει αριθμό 20.

 

Οι Εκφράσεις LOCK και UNLOCK

Στο ερώτημα UPDATE που χρησιμοποιήσαμε παραπάνω, υπάρχει ένας μικρός κίνδυνος. Τι θα συμβεί αν τη στιγμή που αλλάζει η τιμή της στήλης TimesViewed, κάποιος άλλος χρήστης εμφανίσει το ίδιο joke; Το PHP script θα εκτελεσθεί για δεύτερη φορά για την καινούργια αίτηση (request). Όταν εκτέλεσε το SELECT για να πάρει την τρέχουσα τιμή της στήλης TimesViewed, θα πήρε την ίδια τιμή με το πρώτο script, επειδή η τιμή δεν έχει ακόμη ενημερωθεί.

Και τα δύο scripts θα προσθέσουν μετά το 1 στην ίδια τιμή και θα γράψουν την καινούργια τιμή στον πίνακα. Το αποτέλεσμα θα είναι ότι δύο χρήστες είδαν το joke, αλλά ο μετρητής TimesViewed αυξήθηκε μόνο κατά 1. Για να αποφύγουμε προβλήματα όπως το παραπάνω, μπορούμε να κλειδώσουμε έναν ή περισσότερους πίνακες με τους οποίους εργαζόμαστε σ’ ένα ερώτημα και να έχουμε έτσι αποκλειστική πρόσβαση σ’ αυτούς για όσο διαρκεί το ερώτημα. Η σύνταξη για το κλείδωμα ενός πίνακα είναι ως εξής :

LOCK TABLES tblName { READ | WRITE }

Όταν κλειδώνουμε έναν πίνακα, πρέπει να καθορίσουμε αν θέλουμε ένα "read lock" ή ένα "write lock". Το πρώτο εμποδίζει άλλες διεργασίες από το να κάνουν αλλαγές στον πίνακα αλλά επιτρέπει σ’ άλλες να διαβάσουν τον πίνακα. Το δεύτερο απαγορεύει όλες τις προσβάσεις στον πίνακα.

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

UNLOCK TABLES

Ακολουθεί ένα παράδειγμα ενός PHP κώδικα :

mysql_query("LOCK TABLES inventory WRITE, shipping WRITE");

// … εντολές …

mysql_query("UNLOCK TABLES");

 

Ψευδώνυμα (Aliases) Στηλών και Πινάκων

Μερικές φορές είναι βολικό να αναφερόμαστε στις στήλες και στους πίνακες της MySQL με διαφορετικά ονόματα. Θα δούμε ένα παράδειγμα μιας βάσης δεδομένων που χρησιμοποιείται στο σύστημα κρατήσεων online μιας αεροπορικής εταιρείας. Για να παρουσιάσει τις πτήσεις που προσφέρονται από την εταιρεία, η βάση δεδομένων περιέχει τους εξής δύο πίνακες : Flights και Cities.

Η κάθε καταχώρηση στον πίνακα Flights παριστάνει μια πραγματική πτήση ανάμεσα σε δύο πόλεις, την αρχή και τον προορισμό της πτήσης. Προφανώς, οι Origin και Destination θα αποτελούν στήλες του πίνακα Flights, μαζί μ’ άλλες στήλες για πράγματα όπως η ημερομηνία και η ώρα της πτήσης, ο τύπος του αεροσκάφους, ο αριθμός της πτήσης και τα διάφορα ναύλα.

Ο πίνακας Cities περιέχει μια λίστα όλων των πόλεων προς τις οποίες πετάει η εταιρεία. Έτσι, και οι δύο στήλες Origin και Destination στον πίνακα Flights θα πρέπει να περιέχουν κωδικούς (ID's) για να αναφέρονται στις καταχωρήσεις του πίνακα Cities. Θα δούμε τώρα μερικά ερωτήματα (queries).

Για να πάρουμε μια λίστα όλων των πτήσεων με τις πόλεις προέλευσής τους :

mysql> SELECT Flights.Number, Cities.Name

-> FROM Flights, Cities

-> WHERE Flights.Origin = Cities.ID;

+--------+-----------+

| Number | Name      |

+--------+-----------+
| CP110  | Montreal  |

| CP226  | Sydney    |


| QF2026 | Melbourne |

  ...      ...

Για να πάρουμε μια λίστα όλων των πτήσεων με τις πόλεις προορισμού τους :

mysql> SELECT Flights.Number, Cities.Name

-> FROM Flights, Cities

-> WHERE Flights.Destination = Cities.ID;

+--------+----------+

| Number | Name     |

+--------+----------+

| CP110  | Sydney   |

| CP226  | Montreal |

| QF2026 | Sydney   |

...      ...

Τι μπορούμε να κάνουμε τώρα αν θέλουμε να εμφανίσουμε την προέλευση και τον προορισμό της κάθε πτήσης μ’ ένα μόνο ερώτημα; Ένα ερώτημα που μπορεί να σκεφθούμε να χρησιμοποιήσουμε είναι το εξής :

mysql> SELECT Flights.Number, Cities.Name, Cities.Name

-> FROM Flights, Cities

-> WHERE Flights.Origin = Cities.ID

-> AND Flights.Destination = Cities.ID;

Empty set (0.01 sec)

Το παραπάνω ερώτημα δεν δουλεύει επειδή λέμε στην MySQL να ενώσει (join) τους πίνακες Flights και Cities και να εμφανίσει τον αριθμό πτήσης, το όνομα της πόλης και το όνομα της πόλης ξανά όλων των καταχωρήσεων που επιστρέφονται αν ταιριάξουμε το Origin με το City ID και το Destination με το City ID.

Μ’ άλλα λόγια, οι στήλες Origin, Destination και City ID πρέπει να είναι ίσες μεταξύ τους. Αυτό έχει σαν αποτέλεσμα μια λίστα όλων των πτήσεων όπου η προέλευση και ο προορισμός είναι ίδιοι. Δεν θα υπάρχουν λοιπόν καθόλου καταχωρήσεις που να ταιριάζουν με την παραπάνω περιγραφή και έτσι είχαμε το αποτέλεσμα "Empty set".

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

Η λύση είναι να δώσουμε στον πίνακα Cities δύο διαφορετικά προσωρινά ονόματα (ψευδώνυμα, aliases) για τους σκοπούς αυτού του ερωτήματος (query). Αν γράψουμε μετά από το όνομα ενός πίνακα την έκφραση AS Alias στο τμήμα FROM του ερωτήματος SELECT, μπορούμε να του δώσουμε ένα προσωρινό όνομα με το οποίο να αναφερόμαστε σ’ αυτό οπουδήποτε στο ερώτημα. Ακολουθεί το παραπάνω ερώτημα ξανά, αλλά αυτή τη φορά έχουμε δώσει στον πίνακα Cities το ψευδώνυμο (alias) Origins.

mysql> SELECT Flights.Number, Origins.Name

-> FROM Flights, Cities AS Origins

-> WHERE Flights.Origin = Origins.ID;

Αναφερόμενοι στον πίνακα Cities δύο φορές, χρησιμοποιώντας δύο διαφορετικά ψευδώνυμα (aliases), μπορούμε να χρησιμοποιήσουμε μια ένωση (join) τριών πινάκων, όπου οι δύο από τους πίνακες θα είναι ουσιαστικά ίδιοι, για να πάρουμε το αποτέλεσμα που θέλουμε :

mysql> SELECT Flights.Number, Origins.Name,

->        Destinations.Name

-> FROM Flights, Cities AS Origins,

->      Cities AS Destinations

-> WHERE Flights.Origin = Origins.ID

-> AND Flights.Destination = Destinations.ID;

+--------+-----------+----------+

| Number | Name      | Name     |

+--------+-----------+----------+

| CP110  | Montreal  | Sydney   |

| CP226  | Sydney    | Montreal |

| QF2026 | Melbourne | Sydney   |

...      ...         ...

Μπορούμε επίσης να ορίσουμε ψευδώνυμα για τα ονόματα στηλών. Θα μπορούμε να το χρησιμοποιήσουμε αυτό, για παράδειγμα, για να διαχωρίσουμε τις δύο στήλες "Name" στον παραπάνω πίνακα αποτελεσμάτων :

mysql> SELECT F.Number, O.Name AS Origin,

->        D.Name AS Destination

-> FROM Flights AS F, Cities AS O, Cities AS D

-> WHERE F.Origin = O.ID AND F.Destination = D.ID;

+--------+-----------+-------------+

| Number | Origin    | Destination |

+--------+-----------+-------------+

| CP110  | Montreal  | Sydney      |

| CP226  | Sydney    | Montreal    |

| QF2026 | Melbourne | Sydney      |

...      ...         ...

 

Η Έκφραση GROUP BY

Στα προηγούμενα, είχαμε δει το παρακάτω query, το οποίο μας εμφανίζει πόσα jokes είναι αποθηκευμένα στον πίνακα Jokes :

mysql> SELECT COUNT(*) FROM Jokes;

+----------+

| COUNT(*) |

+----------+

|        4 |

+----------+

Η συνάρτηση COUNT() της MySQL ανήκει σε μια ειδική κατηγορία συναρτήσεων άθροισης (summary functions) ή συναρτήσεων ομαδοποίησης (group-by functions). Σ’ αντίθεση μ’ άλλες συναρτήσεις οι οποίες επηρεάζουν τις καταχωρήσεις στο αποτέλεσμα του ερωτήματος SELECT, οι συναρτήσεις άθροισης ομαδοποιούν μαζί όλα τα αποτελέσματα και επιστρέφουν ένα μόνο αποτέλεσμα.

Στο παραπάνω παράδειγμα, η συνάρτηση COUNT() επιστρέφει τον συνολικό αριθμό των γραμμών του αποτελέσματος.

Ας υποθέσουμε ότι θέλουμε να εμφανίσουμε μια λίστα των συγγραφέων με τον αριθμό των jokes που αντιστοιχεί στον καθένα. Η πρώτη μας σκέψη θα ήταν να πάρουμε μια λίστα των ονομάτων και των ID’s όλων των συγγραφέων και μετά να χρησιμοποιήσουμε τη συνάρτηση COUNT() για να μετρήσουμε τον αριθμό των αποτελεσμάτων όταν κάνουμε SELECT στα jokes με το ID του κάθε συγγραφέα. Ο PHP κώδικας θα είναι ως εξής :

// Μια λίστα όλων των συγγραφέων

$authors = mysql_query( "SELECT Name, ID FROM Authors" );

// Επεξεργασία του κάθε συγγραφέα

while ($author = mysql_fetch_array($authors)) {

$name = $author["Name"];

$id = $author["ID"];

// Μέτρηση των jokes που ανήκουν σ' αυτόν τον συγγραφέα

$result = mysql_query(

"SELECT COUNT(*) AS NumJokes ".

"FROM Jokes WHERE AID=$id" );

$row = mysql_fetch_array($result);

$numjokes = $row["NumJokes"];

// Εμφάνιση του συγγραφέα και του αριθμού των jokes

echo("<P>$name ($numjokes jokes)</P>");

}

Χρησιμοποιήσαμε το AS στο δεύτερο query παραπάνω για να δώσουμε ένα φιλικότερο όνομα (NumJokes) στο αποτέλεσμα της COUNT(*). Η τεχνική αυτή θα δουλέψει αλλά θα χρειασθεί n+1 ξεχωριστά ερωτήματα, όπου n είναι ο αριθμός των συγγραφέων στη βάση δεδομένων. Υπάρχει, όμως, μια λύση σ’ αυτό.

Προσθέτοντας την έκφραση GROUP BY σ’ ένα ερώτημα SELECT, μπορούμε να πούμε στην MySQL να ομαδοποιήσει (group) τα αποτελέσματα του ερωτήματος σε σύνολα που να έχουν την ίδια τιμή στις στήλες που έχουμε καθορίσει. Οι συναρτήσεις άθροισης, όπως είναι η COUNT(), επενεργούν μετά σ’ αυτές τις ομάδες και όχι στο συνολικό αποτέλεσμα.

Το ακόλουθο query, για παράδειγμα, εμφανίζει τον αριθμό των jokes που έχουν αποδοθεί σε κάθε συγγραφέα στη βάση δεδομένων :

mysql> SELECT Authors.Name, COUNT(*) AS NumJokes

-> FROM Jokes, Authors

-> WHERE AID = Authors.ID

-> GROUP BY AID;

+-----------------+----------+

| Name            | NumJokes |

+-----------------+----------+

| Kevin Yank      |        3 |

| Joan Smith      |        1 |

| Ted E. Bear     |        5 |

+-----------------+----------+

Ομαδοποιώντας τα αποτελέσματα ως προς το ID του συγγραφέα (AID), παίρνουμε μια ταξινόμηση των αποτελεσμάτων για τον κάθε συγγραφέα. Θα μπορούσαμε να είχαμε χρησιμοποιήσει και την έκφραση GROUP BY Authors.ID και να πάρουμε το ίδιο αποτέλεσμα.

 

Η Έκφραση LEFT JOIN

Μπορούμε να δούμε από τα παραπάνω αποτελέσματα ότι ο Kevin Yank έχει τρία jokes στο όνομά του, ο Joan Smith έχει ένα και ο Ted E. Bear έχει πέντε. Αυτό που δεν δείχνουν αυτά τα αποτελέσματα είναι ότι υπάρχει ένας τέταρτος συγγραφέας, η Amy Mathieson, που δεν έχει κανένα joke στο όνομά της. Εφόσον δεν υπάρχουν καταχωρήσεις στον πίνακα Jokes με AID's που να ταιριάζει το δικό της ID, δεν θα υπάρχουν αποτελέσματα που να ικανοποιούν την έκφραση WHERE στο παραπάνω ερώτημα γι’ αυτήν και συνεπώς θα αποκλειστεί από τον πίνακα των αποτελεσμάτων.

Η MySQL παρέχει μια άλλη μέθοδο για την ένωση (joining) πινάκων, δηλ. την εμφάνιση πληροφοριών από πολλούς πίνακες ταυτόχρονα, που απoκαλείται left join, και είναι σχεδιασμένη γι’ αυτήν ακριβώς την κατάσταση. Για να κατανοήσουμε πώς διαφέρουν τα left joins από τα standard joins, πρέπει πρώτα να θυμηθούμε πώς δουλεύουν τα standard joins.

Η MySQL εκτελεί ένα standard join δύο πινάκων εμφανίζοντας όλους τους δυνατούς συνδυασμούς των γραμμών αυτών των πινάκων. Σε μια απλή περίπτωση, ένα standard join δύο πινάκων με δύο γραμμές για τον καθένα θα περιέχει τέσσερις γραμμές. Αφού υπολογισθούν οι γραμμές του αποτελέσματος, η MySQL μετά κοιτάει την έκφραση WHERE για να πάρει οδηγίες για το ποιες γραμμές πρέπει να εμφανισθούν, όπως για παράδειγμα εκείνες που η στήλη AID του πίνακα 1 θα ταιριάζει με τη στήλη ID του πίνακα 2.

Ο λόγος που το παραπάνω δεν ικανοποιεί τους σκοπούς μας είναι ότι θα θέλαμε να συμπεριλάβουμε επίσης και τις γραμμές του πίνακα 1 (Authors) που δεν έχουν κάποιες γραμμές που να ταιριάζουν στον πίνακα 2 (Jokes). Ένα left join κάνει ό,τι ακριβώς χρειαζόμαστε, αναγκάζοντας μια γραμμή να εμφανισθεί στα αποτελέσματα για κάθε γραμμή του πρώτου (αριστερού) πίνακα, ακόμη κι αν δεν βρεθούν καταχωρήσεις που να ταιριάζουν στον δεύτερο (δεξιό) πίνακα. Αυτές οι καταχωρήσεις παίρνουν τιμές NULL γι’ όλες τις στήλες του δεξιού πίνακα.

Για να κάνουμε ένα left join ανάμεσα σε δύο πίνακες στην MySQL, διαχωρίζουμε τα ονόματα των δύο πινάκων στην έκφραση FROM με το LEFT JOIN αντί με κόμμα. Γράφουμε μετά το όνομα του δεύτερου πίνακα με ON <συνθήκη>, όπου η <συνθήκη> καθορίζει τα κριτήρια ταιριάσματος των γραμμών στους δύο πίνακες. Ακολουθεί το αναθεωρημένο ερώτημα για την εμφάνιση των συγγραφέων με τον αριθμό των jokes που τους έχει αποδοθεί :

mysql> SELECT Authors.Name, COUNT(*) AS NumJokes

-> FROM Authors LEFT JOIN Jokes

-> ON AID = Authors.ID

-> GROUP BY AID;

+---------------+----------+

| Name          | NumJokes |

+---------------+----------+

| Amy Mathieson |        1 |

| Kevin Yank    |        3 |

| Joan Smith    |        1 |

| Ted E. Bear   |        5 |

+---------------+----------+

Πρέπει να έχουμε υπόψη μας ότι η συνάρτηση COUNT(*) μετράει τον αριθμό των γραμμών που επιστρέφονται για κάθε συγγραφέα. Αν κοιτάξουμε στα μη ομαδοποιημένα αποτελέσματα του LEFT JOIN, θα δούμε τα εξής :

mysql> SELECT Authors.Name, Jokes.ID AS JokeID

-> FROM Authors LEFT JOIN Jokes

-> ON AID = Authors.ID;

+---------------+--------+

| Name          | JokeID |

+---------------+--------+

| Kevin Yank    |      1 |

| Kevin Yank    |      2 |

| Kevin Yank    |      4 |

| Joan Smith    |      3 |

| Ted E. Bear   |      5 |

| Ted E. Bear   |      6 |

| Ted E. Bear   |      7 |

| Ted E. Bear   |      8 |

| Ted E. Bear   |      9 |

| Amy Mathieson |   NULL |

+---------------+--------+

Τώρα η Amy Mathieson έχει μια γραμμή με τιμή NULL. Το ότι η τιμή του Joke ID είναι NULL δεν επηρεάζει την συνάρτηση COUNT(*)× αυτή το μετράει σαν μια γραμμή. Αν αντί για το *, προσδιορίσουμε το όνομα μιας στήλης, όπως Jokes.ID, θα αγνοήσει τις τιμές NULL αυτής της στήλης και θα μας δώσει το αποτέλεσμα που ψάχνουμε :

mysql> SELECT Authors.Name, COUNT(Jokes.ID) AS NumJokes

-> FROM Authors LEFT JOIN Jokes

-> ON AID = Authors.ID

-> GROUP BY AID;

+---------------+----------+

| Name          | NumJokes |

+---------------+----------+

| Amy Mathieson |        0 |

| Kevin Yank    |        3 |

| Joan Smith    |        1 |

| Ted E. Bear   |        5 |

+---------------+----------+

 

Η Έκφραση HAVING

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

mysql> SELECT Authors.Name, COUNT(Jokes.ID) AS NumJokes

-> FROM Authors LEFT JOIN Jokes

-> ON AID = Authors.ID

-> WHERE NumJokes = 0

-> GROUP BY AID;

ERROR 1054: Unknown column 'NumJokes' in 'where clause'

Ο λόγος που η έκφραση WHERE NumJokes = 0 δεν μας έκανε τη δουλειά είναι επειδή οι συνθήκες στην έκφραση WHERE επηρεάζουν τις καταχωρήσεις που έχουν επιλεγεί πριν ομαδοποιηθούν σύμφωνα με την έκφραση GROUP BY.

Έτσι, αν θελήσουμε να αποκλείσουμε από το μέτρημα τα jokes που περιέχουν τη λέξη "chicken", θα μπορούσαμε να χρησιμοποιήσουμε την έκφραση WHERE, όμως εφόσον η στήλη NumJokes δεν υπάρχει πριν επενεργήσει η GROUP BY, θα πρέπει να χρησιμοποιήσουμε μια διαφορετική μέθοδο για να ορίσουμε συνθήκες.

Οι συνθήκες που επηρεάζουν τα αποτελέσματα αφού έχει γίνει η ομαδοποίηση (grouping) πρέπει να εμφανισθούν σε μια ειδική έκφραση HAVING. Έτσι λοιπόν, το σωστό ερώτημα είναι το εξής :

mysql> SELECT Authors.Name, COUNT(Jokes.ID) AS NumJokes

-> FROM Authors LEFT JOIN Jokes

-> ON AID = Authors.ID

-> GROUP BY AID

-> HAVING NumJokes = 0;

+---------------+----------+

| Name          | NumJokes |

+---------------+----------+

| Amy Mathieson |        0 |

+---------------+----------+

Μερικές συνθήκες εργάζονται και στις δύο εκφράσεις HAVING και WHERE. Για παράδειγμα, αν θελήσουμε να αποκλείσουμε έναν συγκεκριμένο συγγραφέα με το όνομά του, θα μπορούσαμε να χρησιμοποιήσουμε το Authors.Name != "AuthorName" σε μια από τις εκφράσεις WHERE ή HAVING, επειδή αν φιλτράρουμε τον συγγραφέα πριν ή μετά από την ομαδοποίηση, θα πάρουμε τα ίδια αποτελέσματα.

Σε μερικές περιπτώσεις, είναι πάντα καλύτερο να χρησιμοποιούμε την έκφραση WHERE, επειδή η MySQL είναι καλύτερη στο να βελτιστοποιεί εσωτερικά τέτοια ερωτήματα και έτσι εκτελούνται ταχύτερα.

 

Προχωρημένη PHP

Η ισχύς της PHP βρίσκεται στην τεράστια βιβλιοθήκη της από ενσωματωμένες συναρτήσεις που περιέχει, οι οποίες μας δίνουν τη δυνατότητα να κάνουμε πολύ πολύπλοκες εργασίες χωρίς να χρειασθεί να εγκαταστήσουμε νέες βιβλιοθήκες ή να ανησυχούμε για λεπτομέρειες, όπως συμβαίνει συχνά μ’ άλλες δημοφιλείς server-side γλώσσες όπως είναι η Perl. Θα δούμε μερικά από τα χρήσιμα χαρακτηριστικά που έχει να προσφέρει η PHP σε κάποιον που δημιουργεί ένα database driven Web site.

Θα ξεκινήσουμε με τη συνάρτηση include() της PHP, η οποία μας δίνει τη δυνατότητα να χρησιμοποιήσουμε ένα κομμάτι κώδικα της PHP σε πολλές σελίδες. Θα δούμε επίσης το πώς μπορούμε να προσθέσουμε ένα επιπλέον επίπεδο ασφάλειας στο site μας μ’ αυτό το χαρακτηριστικό.

Η PHP, ενώ είναι γενικά γρήγορη και αποδοτική, παρ’ όλα αυτά επιβαρύνει τον χρόνο φόρτωσης (load time) και το φορτίο εργασίας (workload) του μηχανήματος πάνω στο οποίο τρέχει ο server. Σε sites με μεγάλη κυκλοφορία, αυτό το φορτίο μπορεί να αυξηθεί σε μη αποδεκτά επίπεδα. Αλλά αυτό δεν σημαίνει ότι πρέπει να εγκαταλείψουμε την database-driven φύση του site μας. Θα δούμε πώς μπορούμε να χρησιμοποιήσουμε την PHP στον παρασκήνιο για να δημιουργήσουμε ημιδυναμικές (semi-dynamic) σελίδες που δεν φορτώνουν τον server τόσο πολύ.

Μια κοινή ερώτηση είναι πώς μπορούμε να χρησιμοποιήσουμε ένα tag <INPUT TYPE=FILE> για να δεχθούμε uploads αρχείων από τους επισκέπτες του site. Θα μάθουμε πώς μπορούμε να το κάνουμε αυτό με την PHP.

Τέλος, ένα εξαιρετικά ισχυρό χαρακτηριστικό της PHP είναι η ικανότητά της να στέλνει εύκολα μηνύματα email με δυναμικά παραγόμενο περιεχόμενο. Είτε θέλουμε να χρησιμοποιήσουμε την PHP για να επιτρέψουμε στους επισκέπτες να στέλνουν με email παραλλαγές του περιεχομένου του site μας στους φίλους τους ή απλά να παρέχουμε έναν τρόπο στους χρήστες για να ανακτούν τα ξεχασμένα passwords τους, η συνάρτηση email() της PHP κάνει καλή δουλειά.

 

Server-Side Includes με την PHP

Αν έχετε δουλέψει για λίγο στο Internet, θα έχετε ίσως συναντήσει τον όρο Server-Side Includes (SSI's). Στην ουσία, τα SSI's μάς δίνουν τη δυνατότητα να καταχωρήσουμε το περιεχόμενο ενός αρχείου που είναι αποθηκευμένο στον Web server μας, στη μέση ενός άλλου. Η πιο κοινή χρήση αυτής της δυνατότητας είναι για να ενθυλακώσουμε (encapsulate) τα στοιχεία κοινής σχεδίασης ενός Web site σε μικρά HTML αρχεία που θα μπορούν έτσι να συμπεριληφθούν στις Web pages on the fly (στον αέρα).

Οποιαδήποτε αλλαγή σ’ αυτά τα μικρά αρχεία επηρεάζει αμέσως όλα τα αρχεία που τα συμπεριλαμβάνουν. Και όπως ακριβώς ένα PHP script, ο Web browser δεν χρειάζεται να γνωρίζει ποια είναι αυτά εφόσον ο Web server είναι αυτός που κάνει όλη τη δουλειά πριν στείλει την ζητούμενη σελίδα στον browser. Η PHP έχει μια συνάρτηση που παρέχει παρόμοιες δυνατότητες. Αλλά εκτός από το να περιλαμβάνει κανονική HTML και άλλα στατικά στοιχεία στα συμπεριλαμβανόμενα αρχεία, μπορούμε επίσης να συμπεριλάβουμε κοινά στοιχεία script. Ας δούμε ένα παράδειγμα :

<!-- include-me.inc -->

<?php

echo( "<P>Hallo from Florina!\n" );

?>

Το παραπάνω αρχείο, include-me.inc, περιέχει κάποιον απλό κώδικα PHP. Το όνομα του αρχείου έχει κατάληξη .inc και όχι .php. Η ιδέα εδώ είναι να ονομάσουμε το αρχείο με κάτι διαφορετικό απ’ αυτό που θα περίμενε ο Web server για ένα PHP script. Αυτό εξασφαλίζει ότι το αρχείο θα μπορεί να εκτελεσθεί μόνο αν συμπεριληφθεί σ’ ένα από τα .php αρχεία και επίσης μας βοηθάει να διαχωρίσουμε τις ιστοσελίδες της PHP από τα include αρχεία της PHP.

Θα χρειαστούμε επίσης και το εξής αρχείο :

<!-- testinclude.php -->

<HTML>

<HEAD>

            <TITLE> Test of PHP Includes </TITLE>

</HEAD>

<BODY>

<?php

            include("include-me.inc");

?>

</BODY>

</HTML>

Αυτό το αρχείο μοιάζει περισσότερο με τα PHP scripts που γνωρίζουμε καθώς έχει επέκταση .php ή .php3 αν το απαιτεί ο  server. Στην κλήση της συνάρτησης include() καθορίζουμε το όνομα του αρχείου που θέλουμε να συμπεριλάβουμε (include-me.inc) και η PHP θα προσπαθήσει να πάρει το αρχείο αυτό και να το ενσωματώσει. Κάνουμε upload και τα δύο παραπάνω αρχεία στον Web server και φορτώνουμε το testinclude.php στον browser. Θα δούμε μια Web page που θα περιέχει το μήνυμα από το include αρχείο, όπως αναμενόταν.

Αν αυτό το παράδειγμα δεν δουλέψει, θα χρειασθεί να κάνουμε configure την επιλογή include_path στο αρχείο php.ini. Ανοίγουμε το αρχείο μ’ έναν text editor και ψάχνουμε μια γραμμή που ξεκινά με το include_path. Αυτή η ρύθμιση δουλεύει όπως ακριβώς η γνωστή μας μεταβλητή περιβάλλοντος PATH και περιέχει μια λίστα των καταλόγων όπου θα πρέπει να ψάξει η PHP για τα αρχεία που της ζητάμε να κάνει include. Τη ρυθμίζουμε ώστε να περιέχει το "." (το τρέχον directory).

Ανάλογα με το αν ο server μας τρέχει κάτω από τα Windows ή το UNIX, θα χρειασθεί να περιβάλλουμε τη ρύθμιση με εισαγωγικά :

Στο UNIX :

include_path=.:/another/directory

Στα Windows :

include_path=".;c:\another\directory"

 

Αυξημένη Ασφάλεια με τα Includes

Τα scripts της PHP θα περιέχουν μερικές φορές ευαίσθητες πληροφορίες όπως usernames, passwords και άλλα για τα οποία δεν θέλουμε να υπάρχει πρόσβαση από τρίτους. Μέχρι τώρα έχουμε χρησιμοποιήσει τη συνάρτηση mysql_connect(), η οποία ζητάει από μας να καταχωρήσουμε το username και password που έχουμε στην MySQL σ’ ένα script της PHP που χρειάζεται πρόσβαση σε μια βάση δεδομένων.

Ενώ μπορούμε να ρυθμίσουμε την MySQL έτσι ώστε το username και το password που χρησιμοποιούνται από την PHP να μην μπορούν να χρησιμοποιηθούν από ενδεχόμενους hackers, ορίζοντας το πεδίο Host στον πίνακα user, θα είμαστε ήσυχοι γνωρίζοντας ότι το username και το password που έχουμε, προστατεύονται από ένα αυξημένο επίπεδο ασφάλειας.

Αλλά εφόσον η PHP επεξεργάζεται στον server, δεν υπάρχει περίπτωση να δει κάποιος το δικό μου password; Σωστά, αλλά ας δούμε τι θα συμβεί αν η PHP σταματήσει να εργάζεται στον server. Τότε οι σελίδες της PHP θα αντιμετωπισθούν σαν απλά αρχεία κειμένου (text files) μαζί μ’ όλο τον κώδικα της PHP, καθώς και το password, και θα είναι ορατές σ’ όλους.

Για να προστατευτούμε απ’ αυτήν την παράβαση ασφάλειας, θα πρέπει να τοποθετήσουμε όλο τον κώδικα τον σχετικό με την ασφάλεια σ’ ένα αρχείο include και να τοποθετήσουμε αυτό το αρχείο σ’ ένα directory που δεν αποτελεί μέρος της δομής καταλόγων του Web server. Προσθέτοντας αυτό το directory στη ρύθμιση include_path της PHP (στο php.ini), θα μπορούμε να αναφερόμαστε απευθείας στα αρχεία με τη συνάρτηση include() της PHP αλλά θα πρέπει να τα στριμώξουμε κάπου μακριά όπου ο Web server δεν μπορεί να τα εμφανίσει σαν Web pages.

Για παράδειγμα, αν ο Web server περιμένει όλες οι Web pages να βρίσκονται στον κατάλογο και στους υποκαταλόγους του /home/httpd/, θα μπορούμε να δημιουργήσουμε έναν κατάλογο με όνομα /home/phplib/ για να τοποθετήσουμε εκεί όλα τα αρχεία include. Προσθέτουμε αυτόν τον κατάλογο στο include_path και είμαστε έτοιμοι.

Το παρακάτω παράδειγμα δείχνει πώς μπορούμε να τοποθετήσουμε τον κώδικα σύνδεσης της βάσης δεδομένων σ’ ένα αρχείο include :

<!-- dbConnect.inc (in /home/phplib/) -->

<?php

$cnx = mysql_connect("localhost", "root", "rootpassword");

?>

Και ένα αρχείο που χρησιμοποιεί αυτό το include :

<!-- dbSample.php (in /home/httpd/) -->

<?php

// Σύνδεση με την MySQL

include("dbConnect.inc");

mysql_select_db("myDatabase",$cnx);

...

Όπως μπορούμε να δούμε, αν η PHP σταματήσει να δουλεύει στον server, αυτό που θα φανεί ελεύθερα θα είναι μια κλήση της συνάρτησης include(). Το username και το password είναι αποθηκευμένα με ασφάλεια στο αρχείο dbConnect.inc, για το οποίο δεν υπάρχει απευθείας πρόσβαση από το Web.

 

Ημι-Δυναμικές Σελίδες

Θα πρέπει να έχουμε υπόψη μας ότι οι δυναμικά παραγόμενες database-driven σελίδες απαιτούν πολύ μεγαλύτερη υπολογιστική ισχύ από τον υπολογιστή που τρέχει το λογισμικό του Web server σε σχέση με τα απλά αρχεία HTML, επειδή η αίτηση (request) για κάθε σελίδα είναι σαν ένα μικρό πρόγραμμα που εκτελείται σ’ αυτόν τον υπολογιστή. Ενώ μερικές σελίδες ενός database-driven site πρέπει πάντα να εμφανίζουν δεδομένα της τελευταίας στιγμής που τα επιλέγουν από τη βάση δεδομένων, για άλλες δεν υπάρχει τέτοια απαίτηση.

Μετατρέποντας τις δυναμικές σελίδες σε ημι-δυναμικές (semi-dynamic), οι οποίες είναι στατικές σελίδες που αναδημιουργούνται δυναμικά σε τακτικά χρονικά διαστήματα για να ανανεώσουν το περιεχόμενό τους, θα μειώσουμε κατά πολύ το φορτίο που επιβάλλουν τα database-driven συστατικά του site στην επίδοση του Web server.

Ας υποθέσουμε ότι έχουμε το αρχείο index.php σαν την πρώτη μας σελίδα (front page), που περιέχει μια σύνοψη (summary) του νέου περιεχομένου στο site. Κάνοντας μια εξέταση στα server logs, ίσως ανακαλύψουμε ότι είναι μια από τις πιο δημοφιλείς σελίδες του site. Αντιλαμβανόμαστε λοιπόν ότι αυτή η σελίδα δεν είναι απαραίτητο να δημιουργείται δυναμικά για κάθε αίτηση (request).

Καθώς ενημερώνεται κάθε φορά που προστίθεται νέο περιεχόμενο στο site, θα είναι τόσο δυναμική όσο χρειάζεται. Χρησιμοποιώντας ένας script της PHP, μπορούμε να δημιουργήσουμε ένα στατικό στιγμιότυπο (snap-shot) της εξόδου της δυναμικής σελίδας και να το τοποθετήσουμε online στη θέση της δυναμικής έκδοσης, με το όνομα index.html.

Οι συναρτήσεις της PHP που θα χρειασθούμε είναι οι εξής :

Δημιουργούμε ένα αρχείο με όνομα generateindex.php, το οποίο θα αναλάβει να φορτώσει το αρχείο index.php, τη δυναμική έκδοση της πρώτης σελίδας, όπως θα έκανε ένας Web browser, και μετά δημιουργεί τη στατική έκδοση αυτού του αρχείου σαν μια ενημερωμένη έκδοση του αρχείου index. html.

Αν πάει κάτι στραβά στην όλη διαδικασία, θα θέλουμε να αποφύγουμε την καταστροφή του καλού αντιγράφου του αρχείου index.html, έτσι θα πρέπει αυτό το script να δημιουργήσει την καινούργια στατική έκδοση σ’ ένα προσωρινό αρχείο (tempindex.html) και μετά να την αντιγράψει πάνω στο αρχείο index.html αν όλα είναι εντάξει.

Ακολουθεί ο κώδικας για το αρχείο generateindex.php, με επαρκή σχολιασμό :

<!-- generateindex.php -->

<?php

            // Ορίζουμε τα αρχεία που θα χρησιμοποιήσουμε

            $srcurl                       = "http://localhost/index.php";

            $tempfilename          = "tempindex.html";

            $targetfilename        = "index.html";

?>

<HTML>

<HEAD>

            <TITLE> Δημιουργία του <?php echo("$targetfilename"); ?>

</TITLE>

</HEAD>

<BODY>

<P> Δημιουργία του <?php echo("$targetfilename"); ?> ... </P>

<?php

            // Ξεκινάμε διαγράφοντας το προσωρινό αρχείο. Για να μην εμφανισθεί

// κάποιο μήνυμα λάθους, χρησιμοποιούμε το @.

            @unlink($tempfilename);

            // Φορτώνουμε τη δυναμική σελίδα ζητώντας την μ’ ένα URL. Η PHP

// θα επεξεργαστεί από τον Web server πριν την λάβουμε, έτσι αυτό που

// θα λάβουμε θα είναι μια στατική HTML σελίδα. Το 'r' δείχνει ότι

// θέλουμε μόνο να διαβάσουμε απ’ αυτό το αρχείο.

            $dynpage = fopen($srcurl, 'r');

            // Έλεγχος για λάθη

            if (!$dynpage) {

                        echo("<P> Δεν μπορεί να φορτωθεί το $srcurl. Στατική σελίδα ".

                                    "η ενημέρωση διακόπηκε!</P>");

                        exit();

            }

// Διαβάζουμε τα περιεχόμενα του URL σε μια μεταβλητή της PHP

// variable. Ξεκαθαρίζουμε ότι θέλουμε να διαβάσουμε μέχρι 1 MB

// δεδομένων.

            $htmldata = fread($dynpage, 1024*1024);

            // Κλείνουμε τη σύνδεση με το πηγαίο αρχείο.

            fclose($dynpage);

            // Ανοίγουμε το προσωρινό αρχείο για να γράψουμε σ’ αυτό ('w').

            $tempfile = fopen($tempfilename, 'w');

            // Έλεγχος για λάθη

            if (!$tempfile) {

                        echo("<P> Δεν μπορεί να ανοίξει το προσωρινό αρχείο ".

                                    "($tempfilename) για γράψιμο. Στατική σελίδα ".

                                    "η ενημέρωση διακόπηκε!</P>");

                        exit();

            }

            // Γράφουμε τα δεδομένα για τη στατική σελίδα στο προσωρινό αρχείο.

            fwrite($tempfile, $htmldata);

            // Κλείνουμε το προσωρινό αρχείο.

            fclose($tempfile);

            // Αν έχουμε φθάσει μέχρι εδώ, τότε το προσωρινό αρχείο γράφτηκε με

// επιτυχία και μπορούμε τώρα να το αντιγράψουμε στην κορυφή της

// στατικής σελίδας.

            $ok = copy($tempfilename, $targetfilename);

            // Τέλος, διαγράφουμε το προσωρινό αρχείο.

            unlink($tempfilename);

?>

<P> Η στατική σελίδα ενημερώθηκε με επιτυχία! </P>

</BODY>

</HTML>

Τώρα, κάθε φορά που εκτελείται το generateindex.php, όπως όταν το ζητάμε μ’ έναν browser, δημιουργείται ένα πρόσφατο αντίγραφο του αρχείου index.html από το αρχείο index.php. Μετακινώντας τα αρχεία index.php και generateindex.php σ’ έναν κατάλογο όπου υπάρχει περιορισμένη πρόσβαση, μπορούμε να βεβαιωθούμε ότι μόνο οι site administrators θα μπορούν να ενημερώσουν την πρώτη σελίδα του site.

Αν θέλουμε να ενημερώνεται αυτόματα η πρώτη μας σελίδα, θα πρέπει να ορίσουμε τον server να εκτελεί το αρχείο generateindex.php σε τακτά χρονικά διαστήματα, όπως κάθε ώρα.

 

Χειρισμός των File Uploads

Όλα τα παραδείγματα των database-driven Web sites που έχουμε δει μέχρι τώρα είχαν να κάνουν με sites που βασίζονταν σε δεδομένα κειμένου. Για να μπορέσουν οι χρήστες να φορτώσουν (upload) κι άλλα δεδομένα, όπως αρχεία εικόνων, θα πρέπει να τους δώσουμε αυτή τη δυνατότητα και θα πρέπει να μπορούμε να παρακολουθούμε την όλη διαδικασία.

Θα ξεκινήσουμε με τα βασικά : θα δημιουργήσουμε μια φόρμα στην HTML που θα δίνει τη δυνατότητα στους χρήστες να κάνουν upload αρχείων. Στην HTML αυτό γίνεται εύκολα με το tag <INPUT TYPE=FILE>. Όμως, εξ ορισμού, στέλνεται μόνο το όνομα του αρχείου που έχει επιλεγεί από τον χρήστη.

Για να μπορέσει να υποβληθεί και το ίδιο το αρχείο μαζί με τα δεδομένα της φόρμας, θα πρέπει να προσθέσουμε το ENCTYPE="multipart/form-data" στο tag <FORM>, ως εξής :

<FORM ACTION="fileupload.php" METHOD=POST

ENCTYPE="multipart/form-data">

            <P> Επιλέξτε αρχείο για upload :

            <INPUT TYPE=FILE NAME="uploadedfile"></P>

            <P><INPUT TYPE=SUBMIT NAME="submit"

VALUE="Submit"></P>

</FORM>

Όπως μπορούμε να δούμε, ένα script της PHP (fileupload.php) θα χειρισθεί τα δεδομένα που υποβάλλονται με τη φόρμα παραπάνω. Όπως θα περιμέναμε, θα δημιουργηθεί αυτόματα μια μεταβλητή της PHP με όνομα $uploadedfile (από το χαρακτηριστικό NAME του tag <INPUT> tag).

Όμως, αντί να αποθηκεύει τα περιεχόμενα του αρχείου που γίνεται upload, η $uploadedfile περιέχει το όνομα του αρχείου που είναι αποθηκευμένο στον σκληρό δίσκο του Web server, στον κατάλογο που έχει ορισθεί από τη μεταβλητή περιβάλλοντος TEMP. Αυτός ο κατάλογος είναι ο C:\Windows\TEMP\ στα περισσότερα συστήματα των Windows 9x.

Αυτό το αρχείο κρατείται μόνο για όσο χρόνο εκτελείται το script της PHP που είναι υπεύθυνο για τον χειρισμό της υποβολής της φόρμας, έτσι αν θελήσουμε να το χρησιμοποιήσουμε για κάτι, θα πρέπει να κάνουμε ένα αντίγραφό του κάπου αλλού με την συνάρτηση copy() που είδαμε προηγούμενα.

Εκτός από τη $uploadedfile, τρεις άλλες μεταβλητές δημιουργούνται αυτόματα επίσης. Η $uploadedfile_name περιέχει το όνομα του αρχείου πριν από την υποβολή του (τα υποβαλλόμενα αρχεία αποθηκεύονται σαν phpx, όπου το x είναι ένας αριθμός, στον κατάλογο TEMP), η $uploadedfile_size περιέχει το μέγεθος (σε bytes) του αρχείου και η $uploadedfile_type περιέχει τον τύπο MIME, δηλ. text/plain, image/gif κοκ, του αρχείου.

Δεν πρέπει να ξεχνάμε ότι το "uploadedfile" είναι απλά το NAME του INPUT tag που υπέβαλλε το αρχείο, έτσι τα πραγματικά ονόματα αυτών των μεταβλητών θα εξαρτώνται απ’ αυτό το χαρακτηριστικό (attribute). Μπορούμε να χρησιμοποιήσουμε αυτές τις μεταβλητές για να αποφασίσουμε αν θα αποδεχθούμε ή θα απορρίψουμε ένα αρχείο που γίνεται upload. Για παράδειγμα, από τα αρχεία γραφικών που διαθέτουμε θα μας ενδιαφέρουν μόνο τα JPEG και τα GIF αρχεία.

Αυτά τα αρχεία έχουν τύπους MIME image/pjpeg και image/gif αντίστοιχα, έτσι ο κώδικας για την επικύρωση των αρχείων που γίνονται uploaded μπορεί να είναι ως εξής :

if ("image/pjpeg" == $uploadedfile_type or "image/gif" ==

$uploadedfile_type) {

// Χειρισμός του αρχείου …

} else {

echo("<P>Παρακαλώ στείλτε (submit) ένα JPEG ή GIF αρχείο εικόνας (image file).\n");

}

Ενώ μπορούμε να χρησιμοποιήσουμε μια παρόμοια τεχνική για να απορρίψουμε τα αρχεία που είναι πάρα πολύ μεγάλα (ελέγχοντας τη μεταβλητή $uploadedfile_size), αυτό δεν αποτελεί συνήθως μια καλή ιδέα. Πριν μπορέσει να ελεγχθεί αυτή η τιμή, το αρχείο έχει ήδη γίνει upload και είναι απoθηκευμένο στον κατάλογο TEMP. Αν προσπαθήσουμε να απορρίψουμε αρχεία εξαιτίας του περιορισμένου χώρου στον δίσκο ή/και του εύρους ζώνης, το γεγονός ότι τα μεγάλα αρχεία μπορούν ακόμη να γίνουν uploaded, ίσως να είναι ένα πρόβλημα για μας.

Αντί γι’ αυτό, μπορούμε να ενημερώσουμε νωρίτερα την PHP για το μεγαλύτερο μέγεθος αρχείου που θέλουμε να κάνουμε αποδεκτό. Υπάρχουν δύο τρόποι για να το κάνουμε αυτό. Ο πρώτος είναι αλλάζοντας τη ρύθμιση upload_max_filesize στο αρχείο php.ini. Η προκαθορισμένη τιμή είναι 2MB, έτσι αν θελήσουμε να αποδεχθούμε uploads μεγαλύτερα απ’ αυτά θα πρέπει να αλλάξουμε αυτήν την τιμή.

Η δεύτερη μέθοδος είναι να συμπεριλάβουμε ένα κρυμμένο πεδίο INPUT στη φόρμα μας με το όνομα MAX_FILE_SIZE και το μέγιστο μέγεθος αρχείου που θέλουμε να κάνουμε αποδεκτό μ’ αυτή τη φόρμα. Για λόγους ασφαλείας, αυτή η τιμή δεν μπορεί να υπερβεί τη ρύθμιση upload_max_ filesize στο αρχείο php.ini, αλλά παρέχει έναν τρόπο αποδοχής διαφορετικών μέγιστων μεγεθών σε διαφορετικές σελίδες.

Η ακόλουθη φόρμα, για παράδειγμα, θα επιτρέπει uploads έως και 1 kilobyte (1.024 bytes) :

<FORM ACTION="fileupload.php" METHOD=POST

ENCTYPE="multipart/form-data">

<P> Επιλέξτε αρχείο για upload :

<INPUT TYPE=FILE NAME="uploadedfile"></P>

<P><INPUT TYPE=SUBMIT NAME="submit"

VALUE="Submit"></P>

<INPUT TYPE=HIDDEN NAME=MAX_FILE_SIZE

VALUE=1024>

</FORM>

 

Εκχώρηση Μοναδικών Ονομάτων Αρχείων

Όπως είπαμε παραπάνω, για να διατηρήσουμε ένα αρχείο που γίνεται upload, πρέπει να το αντιγράψουμε σ’ έναν άλλον κατάλογο για σιγουριά. Και ενώ έχουμε πρόσβαση με τη μεταβλητή $uploadedfile_name στο όνομα του κάθε αρχείου που γίνεται upload, δεν υπάρχει καμία εγγύηση ότι δεν θα γίνουν upload δύο αρχεία με το ίδιο όνομα. Σε μια τέτοια περίπτωση, η αποθήκευση του αρχείου με το αρχικό του όνομα μπορεί να έχει σαν αποτέλεσμα τα νεότερα uploads να καλύπτουν τα παλαιότερα.

Γι’ αυτόν τον λόγο, θα θελήσουμε συχνά να υιοθετήσουμε ένα σχήμα για να μπορούμε να εκχωρούμε ένα μοναδικό όνομα αρχείου σ’ όλα τα αρχεία που γίνονται upload. Χρησιμοποιώντας την ώρα του συστήματος, την οποία μπορούμε να πάρουμε με τη συνάρτηση time() της PHP, μπορούμε εύκολα να έχουμε ένα όνομα που να βασίζεται στον αριθμό των δευτερολέπτων μετά την 1/1/1970. Τι θα συμβεί, όμως, αν δύο αρχεία τύχει να γίνουν upload μέσα σε λιγότερο από ένα δευτερόλεπτο;

Για να το αποφύγουμε αυτό, θα χρησιμοποιήσουμε επίσης και τη διεύθυνση ΙΡ του πελάτη, η οποία αποθηκεύεται αυτόματα στη μεταβλητή $REMOTE_HOST από την PHP, για το όνομα του αρχείου. Εφόσον είναι απίθανο να λάβουμε δύο αρχεία από την ίδια ΙΡ διεύθυνση μέσα σε λιγότερο από ένα δευτερόλεπτο, αυτό αποτελεί μια αποδεκτή λύση για τους σκοπούς μας.

// Επιλέγουμε μια επέκταση αρχείου (file extension)

if ( "image/pjpeg" == $uploadedfile_type )

$extension = ".jpg";

else

$extension = ".gif";

// Το πλήρες path/filename

$filename = "C:\\Uploads\\" . time() .

$REMOTE_HOST . $extension;

// Αντιγράφουμε το αρχείο

if (copy($uploadedfile, $filename)) {

echo("<P>Το αρχείο αποθηκεύθηκε επιτυχώς ως $filename.");

} else {

echo("<P>Δεν μπόρεσε να αποθηκευθεί το αρχείο ως $filename!");

}

Πρέπει να χρησιμοποιήσουμε τους δύο χαρακτήρες \\ για τη διαδρομή (path) στα Windows εφόσον ο χαρακτήρας \ χρησιμοποιείται για να δηλώσει ειδικούς χαρακτήρες στα strings κειμένου της PHP. Σε περιβάλλον UNIX, μπορούμε να χρησιμοποιήσουμε τους χαρακτήρες / όπως συνήθως.

 

Το e-mail στην PHP

Το email είναι ένα ισχυρό εργαλείο στο Internet. Είτε θέλουμε να παρέχουμε ένα εβδομαδιαίο "what's new" newsletter στους χρήστες μας ή έναν τρόπο να ανακτήσουν ένα ξεχασμένο password, το e-mail είναι η λύση. Η PHP κάνει την εργασία με το e-mail εξαιρετικά εύκολη επιτρέποντάς μας να στέλνουμε μηνύματα κάνοντας μια απλή κλήση στην συνάρτηση mail().

Πριν μπορέσουμε να στείλουμε e-mail χρησιμοποιώντας τη συνάρτηση mail(), θα πρέπει πρώτα να ορίσουμε τις επιλογές της PHP σχετικά με το e-mail. Ακολουθούν οι σχετικές γραμμές ενός ασυνήθιστου αρχείου php.ini στα Windows :

[mail function]

SMTP            = localhost                              ;μόνο για win32

sendmail_from   = me@localhost.com      ;μόνο για win32

;sendmail_path  =                                        ;μόνο για unix

Ανάλογα με το αν χρησιμοποιούμε την έκδοση για τα Windows ή το UNIX, η PHP θα στείλει mail μέσω ενός SMTP server ή με το τοπικό σύστημα sendmail, αντίστοιχα. Αν εργαζόμαστε με τα Windows, όμως, είναι πολύ πιθανό ο ISP να μας έχει ήδη προμηθεύσει έναν SMTP για να τον χρησιμοποιήσουμε. Είναι ο ίδιος server που ορίσαμε για να στέλνουμε μηνύματα. Θέτουμε τη ρύθμιση SMTP στη διεύθυνση του hostname/IP αυτού του server.

Το sendmail_from θα πρέπει να τεθεί ίσο με την προκαθορισμένη διεύθυνση e-mail από την οποία θα θέλαμε να προέρχονται τα e-mails που στέλνονται από την PHP. Τέλος, το sendmail_path στο UNIX δεν θα πρέπει να έχει σχόλια και πρέπει να τεθεί ίσο με τη διαδρομή και το όνομα αρχείου του προγράμματος sendmail στο σύστημά μας. Στο Linux, αυτή είναι συνήθως η /usr/sbin/ sendmail.

Αφού τεθεί αυτή η ρύθμιση και επανεκκινήσει ο Web server, η PHP θα πρέπει να είναι εφοδιασμένη με πλήρεις δυνατότητες για e-mail. Η αποστολή ενός e-mail στην PHP δεν μπορεί να είναι τώρα ευκολότερη :

mail("to-address@somewhere.com", "Message Subject",

"Αυτό είναι το σώμα (body) του μηνύματος.");

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

mail("to1@mail.net, to2@mail.net, ...", "Message Subject",

"Message body");

Η προσθήκη επιπλέον επικεφαλίδων (headers), για τον καθορισμό των διευθύνσεων From: ή Reply-To:, για παράδειγμα, είναι επίσης πολύ εύκολη. Τις προσθέτουμε απλά σαν μια τέταρτη παράμετρο, χωρισμένες με τα ζευγάρια των χαρακτήρων carriage return-newline, ως εξής :

mail("to@mail.net", "Message Subject", "Message body",

"From: webmaster@host.com\r\nReply-to:admin@host.com");

Σε συνδυασμό με μια βάση δεδομένων, είναι πολύ εύκολη η διαχείριση μιας mailing list. Απλά παίρνουμε τη λίστα των διευθύνσεων από τη βάση δεδομένων και χρησιμοποιούμε τη συνάρτηση mail() για να στείλουμε τα μηνύματα. Η εξατομίκευση των μηνυμάτων είναι επίσης πολύ εύκολη. Ας δούμε το ακόλουθο παράδειγμα :

// Ανακτούμε τα $email και $password από τη βάση δεδομένων που

// βασίζονται στο $username που παρέχεται από μια φόρμα

mail($email, "Your Password",

"Γεια σας!

Μόλις συμπληρώσατε μια φόρμα στο Web site μας

που φανερώνει ότι έχετε χάσει το password σας.

Όπως το ζητήσατε, σας το στέλνουμε με email.

username: $username

password: $password

Παρακαλώ καταγράψτε αυτά τα στοιχεία σ' ένα

ασφαλές μέρος έτσι ώστε να τα έχετε διαθέσιμα

αμέσως κατά την επόμενη επίσκεψή σας στο site μας!

 -Ο Webmaster.

");

Αν εργαζόμαστε στο UNIX και δεν έχουμε διαθέσιμο ένα τοπικό σύστημα sendmail για να στείλουμε e-mail, η PHP είναι εφοδιασμένη με πλήρεις δυνατότητες δικτύωσης TCP/IP, οι οποίες της δίνουν τη δυνατότητα να συνδεθεί σ’ έναν SMTP για να στείλει μηνύματα όταν χρειασθεί. Παρόμοια, αν χρειασθεί να επισυνάψουμε αρχεία σε εξερχόμενα μηνύματα, η PHP μπορεί να το κάνει κι αυτό.

Δυστυχώς, η ενσωματωμένη συνάρτηση mail() δεν υποστηρίζει κανένα απ’ αυτά τα χαρακτηριστικά και αν τα χρειασθούμε θα πρέπει να γράψουμε τη δική μας συνάρτηση emailing από την αρχή. Ο σχετικός κώδικας υπάρχει στο βιβλίο "Professional PHP Programming" της WROX Press, στο Κεφάλαιο 17. Ο πλήρης κώδικας υπάρχει και στο Web site της εταιρείας.

 

back.gif (9867 bytes)

Επιστροφή