I PHP 5.4 har det kommet en ny SERVER-variabel som heter REQUEST_TIME_FLOAT og gir oss tiden da nettsiden begynner å laste (i millisekunder). Denne muliggjør at vi slipper å definere en konstant helt i begynnelsen av koden vår, som inneholder verdien av microtime(true). Nå kan vi i stedet for gjøre hele prosessen slik:
Koden kan ligge hvor som helst i koden din (men legg den helst litt langt oppe …). Poenget med å dra nytte av register_shutdown_function() er at metoden blir kalt selv om nettsiden avslutter uventet, for eksempel via exit og die.
Cron er et ganske nyttig verktøy som gjør deg istand til å utføre spesielle oppgaver på et gitt tidspunkt, men er uheldigvis kun tilgjengelig på UNIX-systemer. I Windows har man noe som heter planlagte oppgaver, som også går an å anvende.
Men hva er dette godt for, når mange webhotell-tilbydere ikke engang har funksjonen(e) aktivert?
Det eneste alternativ som står igjen, er å lage en cron-emulator med PHP (eller lignende). Vi ønsker altså å lage et system som selv kan utføre oppgaver på gitte tidspunkt.
Ved et par Google-søk er det mulig å finne utallige slike eksempler, men personlig har jeg alltid følt de har manglet noe. Jeg ønsker det skal være enkelt og lett forståelig, samt enkelt å integrere i andre systemer.
I stedet for å bruke mye tid på å finne «det perfekte scriptet», så satte jeg av en liten time til å utvikle noe slikt selv.
Jeg endte opp med noe jeg selv mener er ganske enkelt å forstår seg på, samtidig som det er ganske enkelt å utvide også.
Hvordan koden fungerer
Totalt inneholder applikasjonen tre klasser:
Scheduler — inneholder de ulike handlingene, og bestemmer hvilke som skal kjøres
SchedulerEvent — egen klasse for hver handling. Her lagres informasjon om hvilken kommando som skal kjøres, og hvor mange sekunder det går mellom hver gang
SchedulerInterval — en bitteliten klasse som bestemmer intervallet en handling skal kjøres i.
Scheduler tar i bruk Singleton-mønsteret, slik at du kan få tak i klasseinstansen hvor som helst, uten å miste data som objektet lagrer.
Jeg har benyttet meg av et par ukjente triks for å oppnå ønsket resultat:
For å sikre meg om at koden skal utføres, uansett hva brukeren velger å gjøre, har jeg tatt i bruk ignore_user_abort()
For at data er lagret til neste sidevisning, har jeg tatt i bruk register_shutdown_function. Der utføres det en kode som lagrer de ulike handlingene i en fil, som blir hentet opp ved neste sidevisning
Til slutt lar jeg SchedulerEvent-klassen implementere Serializable. På den måten kan jeg lagre hele objektet i eksempelvis en tekstfil, og så gjenopprette det igjen
Opprette handlinger
Koden under legger til en handling, som bestemmer at filen cron_scripts/my_file.php skal kjøres hver time. En gyldig URL må gis.
$scheduler = Scheduler::getInstance ();
$event = new SchedulerEvent ("http://example.com/cron_scripts/my_file.php",
new SchedulerInterval (SchedulerInterval::HOURLY));
$scheduler->addEvent ($event);
Kjøre handlinger
Koden for å kjøre handlinger er nødt til å ligge i en fil som blir kjørt på hver sidevisning, eksempelvis index.php.
Dette gjør vi for å være sikker på å utføre handlingene med best mulig presisjon.
// Sørger for at koden under blir utført uansett.
ignore_user_abort(true);
include_once 'class.scheduler.php';
include_once 'class.schedulerevent.php';
include_once 'class.schedulerinterval.php';
// sørger for at objektene blir lagret til neste sidevisning.
register_shutdown_function ('Scheduler::shutdown');
$scheduler = Scheduler::getInstance ();
foreach ($scheduler->getEvents() as $event)
{
// utfører handlinger som ennå ikke har blitt utført,
// eller som skal utføres basert på tiden gitt av metoden
if ($event->getNextRunTime() <= new DateTime() )
{
$event->run ();
}
}
Når en handling skal utføres, blir det sendt et separat POST-request til den valgte filen. Dette er for å unngå at handlingen ikke skal trekke ned hastigheten på siden.
Å jobbe med datoer kan være riktig slitsomt til tider. Hvor mange ganger har du ikke klødd deg litt i bakhodet, og tenkt hvordan i huleste du kan formatere datoer som enten er formatert forskjellig, er fra ulike tidssoner eller som du må trekke fra / legge til dager/uker/måneder … ?
Ja, det har vært et problem. Med det mener jeg at løsningen er på plass, og at du kan slappe ekstra godt av. Dette innebærer selvfølgelig at du har en PHP-installasjon tilsvarende 5.3.x.
Alle dager i mars måned
Tidligere har vi måttet brukt PHPs datofunksjoner (date/strtotime/strftime …), men siden PHP 5.3 kan vi nemlig dra nytte av de nye DateTime-klassene og godene som medfølger.
Se bare hvor enkelt det er å skrive ut alle dagene i Mars 2011:
$march = new DateTime ('March 2011');
$days = new DatePeriod (
$march,
new DateInterval ('P1D'),
$march->modify ('first day of next month')
);
foreach ($days as $day)
{
echo $day->format ('Y-m-d'), "\n";
}
Årsaken til at jeg valgte å skrive «first day of next month» fremfor «last day of» er på grunn av at siste datoen blir ekskludert, som hadde betydd at 31. mars ikke hadde blitt skrevet ut.
DateTime lager et objekt med 1. mars 2011 utifra «March 2011″
DatePeriod lager et objekt som inneholder alle datoer fra 1. mars til 1. april (eksklusivt) hvor det er 1 dag mellom hver instans (dette bestemmes av DateInterval)
Antall dager mellom to datoer
DateTime-klassen har et stort bruksområde, og kan for eksempel brukes til å finne differansen mellom to datoer:
$january = new Datetime ('January 2011');
$diff = $january->diff ( new DateTime ('April 2011') );
echo 'Difference between January and April: ',
$diff->format ('%R%a days'); // 90 days
%a fungerer slik at den skal gi meg det totale antallet dager mellom de to datoene. Denne funksjonaliteten fungerer dessverre ikke optimalt på Windows, og kan skape endel frustrasjoner. Men i lag med PHP 5.3 kan du også sammenligne to DateTime-instanser ved bruk av «comparison operators» (større/mindre enn, osv.) Dette gjør oss istand til å lage en «work-around» til Windows-problematikken:
$january = new DateTime ('January 2011');
$april = new DateTime ('April 2011');
// clone variable to keep $january clean..
$currdate = clone $january;
for ($days = 0; $currdate->modify ('+1 day') <= $april; ++$days);
echo "Difference between January and April: {$days} days";
Utvide DateTime med støtte for MySQL Datetime
Det er også ganske tilfredsstillende å benytte seg av DateTime ilag med MySQL DATETIME. I dette eksempelet har jeg valgt å utvide DateTime-klassen:
class MyDateTime extends DateTime
{
const MYSQL = 'Y-m-d H:i:s';
public static function createFromMySQL ($datetime)
{
return self::createFromFormat (self::MYSQL, $datetime);
}
}
// date from MySQL
$date = MyDateTime::createFromMySQL ($row['my_date']);
$timestamp = $date->getTimestamp ();
echo "Date: {$date->format ('d.m.Y H:i')}\n";
Poenget mitt var ikke å utvide klassen for å legge til en enkel funksjon, men heller illustrere at det kan være lurt å gjøre det (DateTime-klassen har tross alt ikke tatt høyde for absolutt alt).
Kanskje det kunne vært en idé å gjøre Windows work-arounden min til en funksjon som utvider DateInterval ?
Som regel er det kun DateTime du har behov for i det daglige, men du kan også få god nytte av både DateInterval og DatePeriod om du for eksempel skal jobbe med kalendere.
Når man skriver nettsider med PHP er det mye som kan være med på å dra opp lastetiden. For ikke å snakke om alle CSS- og JavaScript-filer som også må lastes ned. Hva kan man så gjøre?
Om du jobber med databaser kan du for eksempel skru på MySQL Query Cacher, samtidig som du kan mellomlagre resultatet i HTML-, JSON eller XML-format. Men alt dette krever igrunn litt arbeid, samt at noen koder her og der må endres.
$lastModified = filemtime (__FILE__);
$etagFile = md5_file (__FILE__);
$ifModifiedSince = isset ($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : FALSE;
$etagHeader = isset ($_SERVER['HTTP_IF_NONE_MATCH'] ? trim ($_SERVER['HTTP_IF_NONE_MATCH']) : FALSE;
header("Last-Modified: " . gmdate("D, d M Y H:i:s", $lastModified) . " GMT");
header("Etag: $etagFile");
header('Cache-Control: public');
//check if page has changed. If not, send 304 and exit
if (@strtotime ($ifModifiedSince) == $lastModified || $etagHeader == $etagFile)
{
header ("HTTP/1.1 304 Not Modified");
exit;
}
//your normal code below
Om du limer inn koden ovenfor i starten på de PHP-filene du ønsker å mellomlagre, merker du forskjellen med én gang. Jeg har selv testet koden i flere prosjekter, og den fungerer utmerket. Det som er verdt å merke seg, er at den ikke fanger opp endringer i dynamisk innhold med én gang.
Den merker så klart endringer på seg selv, men dersom du henter innhold fra en database så kan det ta noen minutter før det vises.
Denne prosessen kan, så vidt jeg vet, ikke fremskyndes siden mellomlageret ligger i nettleseren, og det er ikke mulig å fjerne det (dette må sluttbruker gjøre manuelt).
Er det noe jeg virkelig elsker med PHP, må det være Standard PHP Library, eller SPL som det også heter. Det er en samling av innebygde klasser som gjør deg istand til for eksempel å kjøre (iterere) gjennom arrays, filer, mappestrukturer (endimensjonalt) eller flerdimensjonalt (rekursiv). Alt dette ved en solid OOP-struktur!
Scenario: du skal iterere rekursivt gjennom en mappestruktur, og ønsker kun å hente ut filer med endelsen «txt».
Hva gjør du? Du kunne brukt en kombinasjon av scandir() og din egen rekursive funksjon, eller glob(), eller, eller…
Hva dersom du ønsker å bruke kodene senere, flere ganger, bare med små endringer? Det er virkelig en grense for hvor fleksibelt et system kan være når det er satt til å utføre én bestemt oppgave. Utnytter du OOP, kan du snu på dette.
I koden nedenfor henter jeg ut alle filer (uansett nivå) som befinner seg inni mappen filer/.
foreach (new RecursiveIteratorIterator(
new RecursiveDirectoryIterator('filer')
) as $file
)
{
echo $file->getFilename();
}
Men skulle ikke jeg filtrere bort enkelte filer? Hvordan gjør jeg det?
Her har jeg skrevet enda et enkelt eksempel, hvor jeg kun henter ut filer med endelsen «txt».
class RecursiveFilter extends FilterIterator
{
protected $_extensions = array ();
public function __construct ($path, array $extension = array ())
{
$this->_extensions = $extensions;
parent::__construct(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)));
}
/**
* Bestemmer hvilke filer som skal bli godtatt
* Metoden må returnere FALSE eller TRUE, alt ettersom
* om filen er godtatt eller ei.
*/
public function accept ()
{
// get the file that are being iterated
$item = $this->getInnerIterator ();
return in_array ( pathinfo ($item->getBasename(), PATHINFO_EXTENSION), $this->_extensions);
}
}
$iterator = new RecursiveFilter ('filer', array ('txt'));
foreach ($iterator as $item)
{
echo $item->getBasename();
}
Så enkelt kan det gjøres. I teorien har vi igrunn fjernet sjekken ut av foreach-løkken, og inn i egen klasse. Det virker unødvendig, men med tanke på gjenbruk og abstraksjon så er dette en meget god idé.
Om du ønsker alle filer som ikke har endelsen «txt», så snur du bare på sjekken som utføres i accept() til:
Du kan selvfølgelig utvide accept() til så mye du vil, og ta høyde for alt du ønsker. $item inneholder et FileInfo-objekt, så da kan du bare slå deg løs!