Komprimer dit site og øg hastigheden
Apache har et lille lækkert modul der hedder mod_deflate, der gør det muligt at gzippe sine sider inden de sendes til brugeren. Det sparer båndbredte og øger hastigheden.
På et testsite fyldte forsiden til at begynde med 309 KB. Efter jeg tilføjede mod_deflate til min .htaccess fik jeg sitet ned på 143 KB. Hvilket betyder rigtig meget hvis ens site har et stort load. Det kan også have betydning ift. ens placering i søgemaskiner. Da loadtiden bl.a. kan indgå i søgemaskinens vurdering af, hvilken placering siden skal have.
For at bruge mod_deflate skal man indsætte følgende i sin .htaccess:
1 2 3 | <IfModule mod_deflate.c> AddOutputFilterByType DEFLATE text/html text/css </IfModule> |
Hvilket aktiverer mod_deflate for text/html og text/css. Man kan selvfølgelig tilføje alle de typer man vil. Det er nok meget nærliggende også at tilføje text/plain, text/xml og application/javascript.
(Bemærk at javascript ikke er text/javascript)
Hvis man er på jagt efter en udelukkende php-løsning kan man tilføje
1 | ob_start("ob_gzhandler"); |
I toppen af sine php-filer (eller i en include-fil der inkluderes på alle siderne), men mod_deflate har samme virkning og er nemmere at implementere. Det kræver dog, som med alt andet, at serveren understøtter det. Hvis man laver en
1 | phpinfo(); |
Vil man under “Loaded Modules” kunne se om modulet er tilgængeligt.
For flere hastighedstips se mit indlæg om mod_expires.
Sprog i Smarty
I forlængelse at mit forrige indlæg vil jeg komme med nogle idéer til hvordan man kan tilføje sprogsupport i Smarty.
Det jeg gerne vil ende med er en løsning hvor jeg i mine templates kan skrive noget lignende:
1 | <h1>{translate}Welcome to my site{/translate}</h1> |
Men også
1 | <title>{$title|translate}</title> |
Jeg vil også kunne angive sproget direkte, fx:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <html> <head> <title>{$title|translate:"fr"}</title> </head> <body> <h1>{translate lang="fr"}Welcome to my site{/translate}</h1> </body> </html> |
Jeg vil aldrig kalde Smarty direkte, men altid via min egen klasse som extend’er Smarty. Det gør det bl.a. nemmere at sætte diverse indstillinger op. I min egen klasse vil jeg, for at understøtte sprog, tilføje en block og en modifier. Det gør jeg på følgende måde:
1 2 3 4 5 6 7 8 | class mbnSmarty extends Smarty { public function __construct() { $this->register_modifier('translate', 'smarty_m_translate'); $this->register_block('translate', 'smarty_b_translate'); } } |
Bagefter skal jeg skrive de to funktioner hhv. smarty_b_translate og smarty_m_translate.
1 2 3 4 5 6 7 8 9 10 11 12 13 | function smarty_b_translate($args, $str, &$smarty, &$repeat) { if (!$repeat) { $text = new Text(); $text->setText($content); return $text->translate($args['lang'] ? $args['lang'] : LANG); } } function smarty_m_translate($str, $lang = LANG) { $text = new Text(); $text->setText($str); return $text->translate($lang); } |
Jeg har i mine eksempler gået ud fra, at der er defineret en LANG-konstant på følgende måde:
1 | define("LANG", "da"); |
Jeg bruger også en Text-klasse, som I selv må lave :)
Sprogstyring via mod_rewrite
En lille idé til hvordan man kan styre sprog via Apaches mod_rewrite.
Normalt definerer jeg en konstant ved navn URL der indeholder den absolutte adresse til forsiden af mit website. Fx:
1 | define("URL", "http://www.example.com"); |
Hver gang jeg skal linke i HTML/Smarty eller sende videre via PHP bruger jeg konstanten.
Nu vil jeg tilføje sprog som et ekstra “lag” på mine adresser, så jeg fremover kalder example.com/en/about.php for den engelske udgave, og example.com/fr/about.php for den fanske udgave af about.php; men jeg vil ikke have to versioner af about.php liggende.
Derfor laver jeg en lille rewrite-regl der omskriver førnævnte adresser til example.com/about.php?lang=en og example.com/about.php?lang=fr.
Reglen kan ses herunder. Bemærk at jeg kun checker på to a-z og ikke på de tilladte sprog. Det burde man måske gøre.
1 2 | RewriteEngine On RewriteRule ^([a-z]{2})/(.*)$ $2?lang=$1 [QSA] |
Jeg tilføjer QSA som betyder query string append, hvilket gør at eventuelle andre parametre til siden stadig kommer med.
Næste trin bliver at lave noget PHP der behandler ovenstående. Jeg vil typisk sætte min URL-konstant på i en header-fil der bliver inkluderet på alle sider. Før jeg definerer min konstant, laver jeg et check på $_GET['lang'] for at se om den er på listen over tilladte sprog. Er den det liver den tilføjet til konstanten, og på den måde vil alle mine linke indeholde sprogangivelsen.
1 2 3 4 5 6 7 8 9 | $valid_languages = array("en", "de", "fr", "da"); if (in_array($_GET['lang'], $valid_languages)) { define("URL", "http://www.example.com/".$_GET['lang']); } else { define("URL", "http://www.example.com"); } |
Det kan måske være meget smart at have to konstanter til URL’en. Én med sprog, og en uden, da det ikke er alle steder man vil angive sprog – fx billeder, stylesheets og javascript m.m.
E_NOTICE i PHP
Jeg ender altid med at kommer i slåskamp med andre php-udviklere omkring notices i koden, fordi min kode laver typisk mange. Hvis man lige har lært at slå notices til i php, vil man måske opfatte en notice som en fejl, men det er det ikke. Hvilket man også kan aflede af navnet – fejl plejer at hedde noget med error.
Jeg gør det typisk altid i if-sætninger for at gøre koden mere overskuelig og letlæselig. Nedenstående to eksempler viser først hvordan jeg plejer at lave mine if-sætninger, og sidste viser hvordan man skal skrive samme kode for at undgå en E_NOTICE.
1 2 | if ($id = (int) $_GET['id']) { } |
1 2 3 | if (isset($_GET['id']) && is_numeric($_GET['id'])) { $id = (int) $_GET['id']; } |
Man opnår det samme med de to stykker kode, men jeg vil mene at første er noget nemmere at læse.
Kommentarer i MySQL-forespørgsler
En lille idé man kan vælge at bruge eller ej :)
Har man et site der køre nogle store og tunge forespørgsler, og hvis man er bange for at nogle af dem løber løbsk eller ophober sig, kan man fx i starten af sin SQL indsætte en lille kommentar.
Tricket er nemlig at den kommentar kommer med når man laver en:
SHOW PROCESSLISTEfterfølgende sætter man et cron-job op der hvert minut kigger efter forespørgsler, og hvis scriptet opdager en forespørgsel der indeholder en bestemt kommentar kan man enten automatisk dræbe den, eller sende en mail om at en forespørgsel er løbet løbsk.
Kommentaren kunne fx indeholder en maks. kørselstid.
Et eksempel på en forespørgsel hvor man kun vil lave den køre i 120 sek.:
1 2 3 | <?php mysql_query("/* MAX_120 */ SELECT big_stuff FROM big_table"); ?> |
PHP-scriptet kunne se sådan ud:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?php mysql_connect("localhost", "benny", "pa55word"); mysql_select_db("benny_db"); $result = mysql_query("SHOW PROCESSLIST"); while($row = mysql_fetch_assoc($result)) { // Matcher /* MAX_120 */ if (preg_match("/\/\*\s*max_([0-9]+)\s*\*\//i", $row['Info'], $regs)) { // Kontrollerer om forespørgslen har kørt længe end den må if ($row['Time'] > $regs[1]) { mysql_query("KILL ".(int) $row['Id']); } } } ?> |
Underdomæner på en smart måde
En nem måde at lave underdomæner på er ved at have wildcard-DNS. Dette opnår man typisk ved at lave et CNAME hvor *.foo.dk peget på hoveddomænet foo.dk.
Når folk ser foo.dk eller www.foo.dk leverer serveren typisk index.php; men forestil jer, at man vil have serveren til at vise domain.php med ‘bar‘ som argument, hvis brugeren fx kalder bar.foo.dk.
Det kan forholdsvist nemt løses vha. følgende i ens .htaccess.
1 2 3 4 | RewriteEngine On RewriteCond %{HTTP_HOST} !^(www\.)?foo\.dk$ RewriteRule .* %{HTTP_HOST} [C] RewriteRule ^([^.]+) domain.php?host=$1 [L] |
Jeg omskriver kun hvis HTTP_HOST ikke er foo.dk eller www.foo.dk, og jeg omskriver http://bar.foo.dk/ til http://bar.foo.dk/bar.foo.dk for derefter at pille underdomænet ud, og vise domain.php?host=bar.
Bemærk at omskrivningen sker internt, så adresselinien ændrer sig ikke.
Det burde derefter være trivielt at lave nogle checks i domain.php på $_GET['host'].
Tricket i ovenstående ligger i at kæde de to rewrite-regler sammen vha. [C] der står for chain.
En måde at bruge VIEWs på
Jeg har lavet en simpel klasse der repræsenterer en MySQL-tabel. Den er meget simpel, og i constructor’en angiver man tabelnavn og primærnøgle, og så laver den bare en
1 | SELECT * FROM tabelnavn WHERE id = $id; |
Hvor tabelnavn og id er variable. Når man laver en instans af klassen udfra en user-tabel med id 5 laver man en
1 | $user = new User(5); |
Det oversætter min klasse så til:
1 | SELECT * FROM user WHERE userid = 5; |
Hvis jeg vel at mærke har kaldt min tabel user og primærnøglen userid i min klasse. Det der efterfølgende sker er, at klassen hælder alle felterne over i et array, og jeg kan efterfølgende hive feltet "name" ud som $user->getName(), eller gemme via $user->setName('Morten') der udfører en
1 | UPDATE tabel SET name = 'Morten' WHERE userid = 5; |
For at spare nogle databaseopslag kan man som andet argument selv sende felterne med. Fx.
1 2 | $row = mysql_fetch_assoc($result); $user = new User($row['userid'], $row); |
På den måde behøver min klasse ikke lave noget opslag. Det var egentlig ikke klassen jeg ville snakke om. Skriv hvis I vil se noget kode eller høre mere :)
Det jeg ville nævne var en smart finte (synes jeg selv :)). Først laver jeg en tabel til mine brugere:
1 2 3 4 5 6 | CREATE TABLE user ( userid INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, newsletter TINYINT(1) UNSIGNED NOT NULL, PRIMARY KEY (userid) ); |
Så fylder jeg lidt folk i:
1 2 3 4 5 6 | INSERT INTO user (name, gender, newsletter) VALUES ('Alfred', 1), ('Bent', 0), ('Conni', 1), ('Dorthe', 1), ('Else', 0); |
Nu laver jeg et VIEW til folk der abonnerer på nyhedsbrevet:
1 2 | CREATE VIEW newsletter AS SELECT * FROM user WHERE newsletter = 1; |
Det resultat som min VIEW returnerer kan jeg umiddelbart smide direkte over i min MySQL-klasse som:
1 2 3 4 | $result = mysql_query("SELECT * FROM newsletter"); while($row = mysql_fetch_assoc($result)) { $user = new User($row['userid'], $row); } |
Min klasse laver aldrig noget opslag selv, og da den ved hvilken tabel det stammer fra kan jeg også fint gemme. Det er måske nemmere at se det anvendelige hvis mit VIEW var mere kompliceret. Men forestil jer at jeg fx vil sende en e-mail til alle brugere at BedsteVen.dk der har oprettet en annonce, men ikke tilføjet noget billede, og deres annonce udløber om 14 dage, og der er mindre end 10 personer der har set den. Så begynder det at være meget rart at have det som et VIEW.
Om at sende nyhedsbreve
Jeg synes det er problematisk at sende mange nyhedsbreve ud. Især hvis alle nyhedsbrevene skal være unikke.
Jeg står tit i en situation hvor jeg har et site som fx BedsteVen.dk der er kodet i PHP op mod en MySQL database. For at kunne bruge al den logik der i forvejen findes på sitet, vælger jeg typisk at udsende de enkelte e-mails gennem PHP ved at kalde et script via wget.
Det er dejlig nemt at kode, men det er utrolig svært hvis man via et script skal sende mange mails (mange er mere end 300 mails).
PHP har en max_execution_time der afgør levetiden for scriptet. Typisk er den på 90 sekunder, og hvis man er længere tid om at sende end det, dør scriptet.
Jeg lavede nogle test på BedsteVen.dk, og via PHP’s mail() funktion kunne jeg over en periode på 90 sekunder udsende ca. 300 mails. Det svarer til 3,33 mails i sekundet.
Min løsning blev to-trins.
Først valgte jeg at begrænse mængden af mails til hvad serveren sikkert kunne klare:
1 2 3 4 | $count = 3 * ini_get(max_execution_time); foreach(getUsers($count) as $user) { $user->sendNewsletter(); } |
Ovenstående skulle gerne illustrere at jeg henter 3×90=270 brugere ud, og sender dem et nyhedsbrev. Det burde gøre at scriptet ikke dør undervejs.
Næste trin var at jeg i bunden af scriptet indsatte et link til scriptet igen, og kalder wget rekursivt så den blev ved med at hente siden — indtil der ikke længere er noget link. Ovenstående eksempel skal derfor udvides til at checke for at jeg ikke sender samme mail to gange.
For at få wget til at opføre sig som jeg vil, kalder jeg den på følgende måde:
1 | wget -q -erobots=off -w 5 -l 100 -r --delete-after -nd [URL] |
-q gør at der ikke kommer noget output
-erobots=off gør at den ikke henter robots.txt
-w 5 gør at wget venter 5 sek. mellem hver download
-l 100 sætter en grænse for hvor mange niveauer wget må følge
-r er rekursiv
--delete-after og -nd gør at wget rydder op efter sig selv
Om det er den smarteste måde at gøre det på ved jeg ikke rigtig. Det er én måde :)
Excel m.m. i PHP
I forlængelse af mit forrige indlæg med Zip-filer i PHP kan jeg nævne at de nye Office-produkter faktisk gemmer zip-filer med xml-dokumenter. Prøv fx at omdøbe en .xlsx-fil til .zip og pak den ud.
Jeg vil ikke lige gennemgå hvordan det er bygget op, men jeg har en fil ved navn xl\worksheets\sheet1.xml liggende, og den indeholder bl.a.:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <sheetData> <row r="1" spans="1:2"> <c r="A1"> <v>100</v> </c> <c r="B1"> <v>400</v> </c> </row> <row r="2" spans="1:2"> <c r="A2"> <v>200</v> </c> <c r="B2"> <v>800</v> </c> </row> <row r="3" spans="1:2"> <c r="A3"> <f>SUM(A1:A2)</f> <v>300</v> </c> <c r="B3"> <f>SUM(B1:B2)</f> <v>1200</v> </c> </row> </sheetData> |
Det er en xml-repræsentation af følgende rækker:
1 2 3 | 100 400 200 800 300 1200 |
Hvor den sidste række er en sum af de to ovenstående.
Har man først gennemskuet strukturen er det forholdsvist nemt at lave et PHP-script der kan skrive og læse Excel2007-dokumenter.
Word2007-dokumenter er opbygget på ca. samme måde.
Zip-filer i PHP
At kunne håndtere zip-filer er meget anvendeligt. Hvis man fx vil gi’ ens brugerne mulighed for at downloade deres data. Eller hvis man gerne vil give de samme brugere mulighed for at uploade flere filer uden at skulle uploade dem enkeltvis (og uden at bruge et flash-plugin).
I PHP er det forbavsende let. Det eneste det kræver er at zip-modulet er indlæst.
Har i eksemplet opretter jeg en lipsum.zip og tilføjer filen lipsum.txt til den. Bemærk at jeg kalder filen for zlipsum.txt i zip-filen:
1 2 3 4 5 | $zip = new ZipArchive(); if ($zip->open("lipsum.zip", ZipArchive::OVERWRITE)) { $zip->addFile("lipsum.txt", "zlipsum.txt"); $zip->close(); } |
Her ta’r jeg den samme lipsum.zip og pakker zlipsum.txt ud i samme bibliotek:
6 7 8 9 10 | $zip = new ZipArchive(); if ($zip->open("lipsum.zip")) { $zip->extractTo("./", "zlipsum.txt"); $zip->close(); } |
For at checke om zip-funktionerne er tilgængelige kan man lave en:
1 2 3 | if (class_exists("ZipArchive")) { // ... } |

Jeg hedder Morten, og jeg har udviklet webapplikationer siden slutningen af 90'erne.