Mit PHP einen Monat mit verlinkbaren Daten anzeigen

CalendarFür eine Applikation, in der man Ressourcen reservieren kann, wollte ich ein Kalender mit verlinkbaren Tagen anzeigen (grad wie im Meeting Room Booking System). Da ich aus dem Alter raus bin, in dem man alles selber machen muss und ausserdem eine PHP-Datumsfunktionsphobie (PHPYpiresiaChronophobia) entwickelt habe, wollte ich eigentlich etwas Fertiges verwenden. Es gibt vorallem Kalender in JavaScript, ein Tutorial das den Kommentaren nach nicht funktioniert (und das mittlerweile vom Web verschwunden ist), Scripts ohne Ausgabebeispiele, Riesenmonster (deren Anwendungsbeispiele mehr Code haben als eine Eigenimplementation) oder Kalender, die zwar gut sind, aber irgendwie nicht gepasst haben (I18n, …). Ich möchte auch nix mit Ajax, Sigolin und Meister Propper, denn es soll eine Enterprise-Applikation geben, und da ist JavaScript halt nicht so opportun (und erspart mir wieder ein paar graue Haare mehr). Aber vor allem (aber das verschweige ich hier) hat das wie eine spannende Aufgabe ausgesehen, und als Kontrollfreak will ich es eben doch selber gemacht haben 😀 .

Ziel

Das Ziel ist es einen Kalender zu erhalten, der die Wochen anzeigt, bei dem ein Tag (Event) markiert werden kann und das heutige Datum speziell ausgezeichnet ist. Er sollte also in etwa so aussehen (die Monatsnamen und Namen der Wochentage sollten sich auch noch der Sprache anpassen):

PHP-Kalender

Man sollte entweder ein Monat und ein Jahr übergeben können oder einfach nichts, wobei dann der aktuelle Monat und das aktuelle Jahr verwendet werden.

Vorgehen und Implementation

Ich habe mich entschlossen, dem Enterprisefassadefrontsidedecontrollerkurtmitgurtgimmeabreak-Pattern zu folgen… Quatsch, meine Gewohnheit und die Stimmen in meinem Kopf haben mich dazu gezwungen, den Kalender in zwei verschiedenen Funktionen zu implementieren. PHP hat ein absolut geniales Array-Handling, und wenn ich an solchen datenmanipulierenden Aufgaben arbeite habe ich mir angewöhnt, zuerst die reinen Daten in einem (mehrdimensionalen, ev. assoziativen) Array abzubilden. Dies hat den grossen Vorteil, dass ich die aufbereiteten Daten nicht nur für die HTML-Ausgabe, sondern auch von anderen Funktionen aus benutzen kann. Das Ziel wäre ein Array, dass auf der einen Dimension die Wochen und auf der Anderen die Wochentage hat. Man nehme den ASCIIstift hervor und zeichne ein Beispiel für Juni 2008:

[22] => {   '',   '',   '',   '',   '',   '',  '1'}
[23] => {  '2',  '3',  '4',  '5',  '6',  '7',  '8'}
[24] => {  '8',  '9', '10', '11', '12', '13', '14'}
...
[27] => { '31',   '',   '',   '',   '',   '',   ''}

Die Benamsung ist wie folgt:
PHP-Kalender benamst

Den Monat in ein Array quetschen

Die Funktionsdeklaration und das Defaulthandling sollten eigentlich einleuchtend sein:

    public function getMonthArray($month=null, $year=null) {
        // Take today if no month or year is given
        $now=time();
        if($month==null || $year==null) {
            $month=date("n", $now);
            $year=date("Y", $now);
        }

date ist eine wichtige Funktion. Ohne Argument gibt sie verschiedene Dinge des aktuellen Datums zurück, warum denn dieses komische $now? Es könnte ja sein, dass mitten in der Skriptausführung der Monat oder das Jahr wechselt. Dann wollen wir wenigstens konsequent den letzten Monat anzeigen und nicht ein Chaos.

Leider fangen die Monate nicht mit Montagen an und die Wochen hören nach 7 Tagen spontan auf. Nicht mal auf eine gemeinsame Anzahl Tagen konnten sich die Monate einigen. Um diesem Chaos zu umgehen, arbeite ich mit time(), das die Anzahl Sekunden seit 1.1.1970 zurück gibt. Die sind zum Glück dem Gesetz des Dezimalsystems unterworfen und verändern sich nicht nach 7, 12 oder 42 Tagen.

Jetzt suchen wir die erste und die letzte Woche. Das wäre nicht so schwierig, wäre die erste Woche nicht nach ISO-8601 definiert:

Als erste Woche im Jahr wird die Woche mit dem ersten Januar-Donnerstag definiert. Dadurch fällt der 4. Januar immer in die erste Woche.

Was haben denn die geraucht? Dem Gregor sein Bart? Also der 1. Januar kann in der 53. Woche liegen. Gut, gell?

Ausserdem berechnen wir noch, wieviele Leertage es zu Beginn gibt (Padding). Ist der erste Tag ein Mittwoch, müssen 2 Tage (Mo, Di) leer gelassen werden. Ebenso am Schluss, einfach andersherum verkehrt 🙂 .

        // Get timestamps for the first and the last day of the month  
        $firstdayunixstamp=mktime(0,0,0,$month,1,$year);
        $lastdayunixstamp=mktime(0,0,0,$month,cal_days_in_month(CAL_GREGORIAN,$month,$year),$year);

        // Get the weeks numbers
        $firstweek=intval(date("W",$firstdayunixstamp));
        $lastweek=intval(date("W",$lastdayunixstamp));

        // We want to have a rectangular display. This is the padding at the beginning and the end
        $firstweekday=intval(date("N",$firstdayunixstamp));
        $lastweekday=intval(date("N",$lastdayunixstamp));

Da wir leider nicht über die Wochen loopen können berechnen wir die Anzahl Tage (Anzahl Tage pro Monat + Leerstellen zu Beginn + Leerstellen am Schluss) und die Anzahl Wochen.

        // Numweeks is not linear at the start or end of a year!
        // Calculate days in month plus paddings
        $daystoshow=cal_days_in_month(CAL_GREGORIAN,$month,$year)+($firstweekday-1)+(7-$lastweekday);
       
        // Calculate how much weeks we display
        $numweeks=$daystoshow/7;

Jetzt geht es los! Wir loopen durch die Wochen und schauen jeweils, ob wir ein Padding zu Beginn, ein Monatstag oder ein Schlusspadding einfügen müssen:

        $montharray=array();
        $dayofmonth=1;
        // Now loop through every week
        for($iweek=0;$iweek<$numweeks;$iweek++) {
            $week=intval(date("W",mktime(0,0,0,$month,$dayofmonth,$year)));
            for($dayofweek=0; $dayofweek<7; $dayofweek++) {
                // First week and month has not yet started or last week and month has ended?
                if(($week==$firstweek && $dayofweek<$firstweekday-1)
                      || ($week==$lastweek && $dayofweek>$lastweekday-1)) {
                    $day="";               
                } else {
                    $day=$dayofmonth++;
                }
                $montharray[$week][$dayofweek]=$day;
            }  
        }
        return $montharray;
    }

Das Array mit HTML verziehren

An sich müssen wir nun nur noch durch dieses Array hindurch foreachen und jedes Datum mit der vom Entwickler gewünschten URL plus eventuellen Parametern versehen. Klar machen wir auch noch Titelchen und so…

Zuerst die obligatorische initialisierung:

    public function getMonthHTML($month=null, $year=null, $day=null, $url="", $parameters="", $marktoday=true) {
        // Take today if no month or year is given
        $now=time();
        if($month==null || $year==null) {
            $month=date("n", $now);
            $year=date("Y", $now);
            $day=date("j", $now);
        }

        if($marktoday) {
            $thismonth=date("n", $now);
            $thisyear=date("Y", $now);
            $thisday=date("j", $now);
        }
       
        // Get the dates in an array
        $montharray=$this->getMonthArray($month, $year);
        $htmlarray=array();

Und nun der Hauptloop der jedes Datum mit Link versieht:

        // Spice up the array with links and marks for today/event
        // Loop through weeks
        foreach($montharray as $week=>$days) {
            // Loop through days
            for($dayofweek=0; $dayofweek<7; $dayofweek++) {
                // Just
                if($montharray[$week][$dayofweek]) {
                    // Mark today
                    $currentday=$montharray[$week][$dayofweek];
                    if($year==$thisyear&&$month==$thismonth&&$montharray[$week][$dayofweek]==$thisday&&$marktoday) {
                        $currentday='<span class="today">'.$currentday."</span>";
                    }              
                    // The event
                    if($day && $montharray[$week][$dayofweek]==$day) {
                        $currentday='<span class="marked">'.$currentday."</span>";
                    }                  
                   
                    // Create the link
                    if($url) {
                        $showurl = $url."?".($parameters?$parameters."&amp;":"")."day=".urlencode($montharray[$week][$dayofweek])."&amp;month=".urlencode($month)."&amp;year=".urlencode($year);
                        $currentday='<a href="'.$showurl.'">'.$currentday.'</a>';
                    }
                    $htmlarray[$week][$dayofweek]=$currentday;
                } else {
                    $htmlarray[$week][$dayofweek]='';
                }
            }
        }

In einem zweiten Loop passen wir dann alles in eine schöne Tabelle. Das join Kommando ist echt Zucker! Elegant kann man damit zwischen Array-Inhalten etwas einfüllen, sei es ein Komma oder eben die Zellentrenner. Ein kleiner Hack ermöglicht uns die Beschriftung der Wochentage.

Die entsprechenden Klassen werden vorgegeben und mit CSS kann der heutige Tag/das markierte Event noch schön gestaltet werden….

Wieso gebe ich HTML zurück und nicht direkt aus? Weil Gott jedesmal ein kleines Hundebaby tötet wenn man HTML in einer Unterfunktion ausgibt!

        //Create HTML table
        $firstdayunixstamp=mktime(0,0,0,$month,1,$year);
        $html = '<table class="month">'."\n";
        // Header
        $html .= '  <thead><tr><th colspan="8">'.strftime("%B",$firstdayunixstamp).' '.$year.'</th></tr></thead>'."\n";
        $html .= '  <tr><th></th>';
        // Names of the weekdays
        for($i=0;$i<7;$i++) {
            //Ugly Hack, September 2008 starts with a monday.
            $html.='<th class="weekday">'.strftime("%a", mktime(0,0,0,9,$i+1,2008))."</th>";
        }
        $html .= "  </tr>\n";
   
        // Loop through weeks
        foreach($htmlarray as $week=>$days) {
                  $html .= '    <tr><th class="monthweek">'.$week.'</th><td class="monthday">'.join('</td><td class="monthday">',$days).'</td></tr>'."\n";
        }
        // We are done
        $html .= "</table>\n";
        return $html;
    }

Gibt es Verbesserungsvorschläge?

Hier der komplette Code zum Download: MonthDisplay in PHP

Nachtrag 12.2008: Vielen Dank an J.B. und Deed. Ich habe den Kalender gemäss ihren guten Tipps angepasst.

3 Gedanken zu “Mit PHP einen Monat mit verlinkbaren Daten anzeigen

  1. also erstmal: krasse Sache!
    braucht zwar etwas Einarbeitungszeit, aber gut…

    nur ne kleine sache (der Schönheit wegen):

    // Header
            $html .= '  <thead><tr><th colspan="8">'.date("F",$firstdayunixstamp).' '.$year.'</th></tr></thead>'."n";

    und dann noch was für Validitätsfetischsten:

    // Loop through weeks
            foreach($htmlarray as $week=>$days) {
              $html .= '    <tr><th class="monthweek">'.$week.'</th><td class="monthday">'.join('</td><td class="monthday">',$days).'</td></tr>'."n";
            }

    da fehlte das

    </tr>
    mfg aus berlin… auus BERLIIIN!

  2. ‚moin!
    wenn zum Schluss die anstatt der date-Funktion die Funktion strftime-Funktion verwendet wird (und die Parameter entsprechend angepasst werden: %B statt F und %a statt D), dann werden die Monate und Wochentage in deutsch (oder einer beliebigen anderen Sprache) angezeigt (sofern diese natürlich per setlocale festgelegt wurde).

    mfg

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.