Wer kennt es nicht – das PDF?

Wir haben es alle bestimmt schon mal benutzt. Doch wie erstellen wir ein solches Dokument? Ich möchte euch in diesem kleinem Tutorial zeigen, wie ihr mit der Zend-PDF-Komponente eure eigenen PDF-Dateien erstellen könnt. Der Clou dabei: wir verwenden Zeilenumbrüche (mehrzeilige Texte).

Zum Schluss möchten wir in unserem Controller nur wenig Code sehen. Dieser könnte so aussehen:

<?php
class PdfController
    extends Zend_Controller_Action
{
    public function init()
    {
        $this->_helper
             ->viewRenderer
             ->setNoRender(true);
        $this->_helper
             ->layout()
             ->disableLayout();
    }

    public function createPdfAction()
    {
        // @todo hier evtl. Daten aus einem Model/Service holen und an My_Example_Pdf übergeben
        // My_Example_Pdf könnte genauso gut auch gleich ein Service sein.
        $pdf = new My_Example_Pdf();
        $pdf->renderToOutput();
    }
}

Hierzu benötigen wir jedoch zunächst die Klasse My_Example_Pdf.

<?php
class My_Example_Pdf
    extends Zend_Pdf
{
    public function renderToOutput()
    {
    }
}

So kommen wir unserem Ziel schon näher – langsam ;D Fangen wir hier also mit der Ausgabe der PDF-Datei an. Dafür müssen bestimmte Header gesetzt werden. Darunter zB. das PDF als Body und die Content-Length.

$response->setHeader('Content-Disposition', 'attachment; filename="My_Example_PDF_' . date('d-m-Y') . '.pdf"');
$response->setHeader('Content-Type', 'application/pdf', true);
$response->setHeader('Content-length', strlen($binaryPdf));
$response->setBody($binaryPdf);

$binaryPdf entspricht hier dem Inhalt des gerenderten PDF-Dokumentes, den wir durch Aufruf der render-Methode der Zend_Pdf-Klasse erhalten.

$binaryPdf = $pdf->render();

Wir können ein PDF natürlich komplett ohne irgendwelche Templates erstellen. Ich für meinen Fall, habe jedoch eine Example.pdf-Datei, welche bereits feste Formularfeldpositionen hat, die später ausgefüllt werden sollen. Es gäbe nun verschiedene Ansätze unsere PDF-Datei mit Daten zu füttern. Ich entscheide mich hier für den Weg, bereits vorab eine Instanz von Zend_Pdf zu erzeugen und diese an meine My_Example_Pdf-Klasse zu übergeben. Je nachdem in welchem Kontext wir uns gerade befinden und wie spezifisch das PDF aufgebaut werden muss, könnten sich auch andere Ansätze erschließen.

Erzeugen wir also eine Instanz von Zend_PDF und laden unser Tempalte Example.pdf, welches wir nun auch mit Daten füllen werden:

$pdf = Zend_Pdf::load(APPLICATION_PATH . '/../data/Example.pdf');
$firstPage = $pdf->pages[0];
$firstPage->setFont(Zend_Pdf_Font::fontWithName(Zend_Pdf_Font::FONT_HELVETICA_BOLD), 10);
$firstPage->drawText('foo', 105, 764.3, 'UTF-8');
$firstPage->drawText('bar', 181.8, 740.3, 'UTF-8');
$firstPage->drawText('baz', 365, 740.3, 'UTF-8');
$firstPage->setFont(Zend_Pdf_Font::fontWithName(Zend_Pdf_Font::FONT_HELVETICA), 9);
$this->drawMultilineText($firstPage, $something, 75, 685);

Huch, was haben wir denn hier? Die Funktion drawMultilineText haben wir bisher noch gar nicht erwähnt. Das holen wir nun nach. Diese Funktion erwartet als ersten Parameter eine Pdf-Seite, auf welcher mehrzeiliger Text eingefügt werden soll. Als zweiten Parameter übergeben wir den Text, als dritten und vierten Parameter die x- und y-Startpositionen von unten links ausgehend, als 5ten Parameter können wir die Zeilenhöhe und als 6ten das Charset angeben. Durch Angabe der Zeilenhöhe können wir also – aha das ist unser Geheimnis – mehrere Zeilen berechnen und entsprechend den Text hinein zeichnen. Die Zeilenhöhe sollte daher entsprechend der Schritgröße oder etwas größer gewählt werden.

Nun zu unserer Funktion:

    public function drawMultilineText(Zend_Pdf_Page $pdfPage, $text, $xStart, $yStart, $lineHeight=10, $charset='UTF-8')
    {
        $i = 0;

        $string_a = explode("n", $text);
        $outputString = '';
        foreach ($string_a as $part) {
            $temp = wordwrap($part, $this->_maxLen, "n", true);
            $temp_a = explode("n", $temp);

            foreach ($temp_a as $line) {
                $i++;
                if ($i > $this->maxLines) {
                    break;
                }

                // Nun Zeichnen wir eine Zeile von unserem Text.
                $pdfPage->drawText($this->_spaces . $line, $xStart, $yStart, $charset);

                $yStart -= $lineHeight;
            }
        }
    }

Wir teilen unseren Text anhand der bereits vorhandenen Zeilenumbrüche, iterieren über den Text, brechen die Zeilen nochmals anhand der maximalen Zeichenlänge um und iterieren weiter – nur diesmal über die zerstückelten Satzteile bis wir endlich eine neue Zeile in unsere PDF-Seite dazu zeichen. Wir haben also nachstehende Klasse erzeugt:

class My_Example_Pdf
    extends Zend_Pdf
{
    /**
     * You can add space before every line for advanced usage.
     *
     * @var string
     */
    private $_spaces = '';

    /**
     * The maximum char count per line.
     *
     * @var integer
     */
    private $_maxLen = 110;

    /**
     * The maximum of rows to draw.
     *
     * @var integer
     */
    private $_maxLines = 40;

    public function __construct($options = null)
    {
        // Nothing to do here for now.
    }

    public function renderToOutput()
    {
        $binaryPdf = $pdf->render();

        $response->setHeader('Content-Disposition', 'attachment; filename="My_Example_PDF_' . date('d-m-Y') . '.pdf"');
        $response->setHeader('Content-Type', 'application/pdf', true);
        $response->setHeader('Content-length', strlen($binaryPdf));
        $response->setBody($binaryPdf);
        $response->sendResponse();
    }

    public function drawMultilineText(Zend_Pdf_Page $pdfPage, $text, $xStart, $yStart, $lineHeight=10, $charset='UTF-8')
    {
        $i = 0;

        $string_a = explode("n", $text);
        $outputString = '';
        foreach ($string_a as $part) {
            $temp = wordwrap($part, $this->_maxLen, "n", true);
            $temp_a = explode("n", $temp);

            foreach ($temp_a as $line) {
                $i++;
                if ($i > $this->maxLines) {
                    break;
                }

                // Nun Zeichnen wir eine Zeile von unserem Text.
                $pdfPage->drawText($this->_spaces . $line, $xStart, $yStart, $charset);

                $yStart -= $lineHeight;
            }
        }
    }
}

Wir können nun das $firstPage-Objekt an My_Example_Pdf->drawMultilineText($firstPage, …. übergeben und danach unser PDF rendern und zB. zum Download anbieten.

Jan 272011
 

Cronjobs sind eine praktische Möglichkeit, zeitgesteuert und ohne weiteres zu tun PHP-Code im Hintergrund ausführen lassen zu können.

Das Zend Framework bietet generell sehr nützliche Komponenten an, die man natürlich auch in einem Cronjob verwenden kann. Möchten wir mehr als einen Cronjob für unser Projekt erstellen, bietet es sich an eine Cronjob-Boostrap.php-Datei zu erzeugen. Das ist der wohl auch komplizierteste Teil, da wir hier bestimmen müssen, welche Komponenten/Module/… wir überhaupt benutzen möchten.

Eine solche Datei könnte wie die nachstehende aussehen:

<?php
// Define path to application directory
define('APPLICATION_PATH',
realpath(dirname(__FILE__) . '/../../application'));

// Define application environment
define('APPLICATION_ENV', 'cronjob');

// Ensure library/ is on include_path
set_include_path(implode(PATH_SEPARATOR,
array(realpath(APPLICATION_PATH . '/../library'),
realpath('/usr/share/include/'),
get_include_path())));

require_once 'Zend/Application.php';

// Create application, bootstrap, and run
$application = new Zend_Application(APPLICATION_ENV,
APPLICATION_PATH . '/configs/application.ini');

// Bootstrap needed application resources.
$application->bootstrap('Autoload')
->bootstrap('Config')
->bootstrap('Db')
->bootstrap('Cache')
->bootstrap('Locale');

In der dritten Zeile geben wir den Pfad zum Application-Ordner an. Gefolgt von der Angabe unseres Environments. Das ist insbesondere deswegen hilfreich, weil wir unsere application.ini zusätzlich unter Angabe des neuen Bereichs cronjob nutzen können und nicht die komplette Seitenkonfiguration nur für den Cronjob neu erstellen müssen.

Den Eintrag in der application.ini könnten wir wie folgt vornehmen:

[cronjob : production]

resources.frontController.controllerDirectory = ""
resources.layout.layoutPath = ""

phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1

Wir benötigen in einem Cronjob weder unsere Controller-Struktur, noch ein Layout. die Fehlerausgabe hingegen hilft beim auffinden von Problemen. Diese können entweder in stdout oder in eines der zahlreichen Fehlerlogs umgeleitet werden.

Der nächste Schritt ist das hinzufügen der Library-Pfade. Unter ../Library lassen sich Applikationsbezogene Libraries einbinden und unter /usr/share/include/ könnte zB. der Ordner Zend mit dem Zend Framework liegen.

Last but not least müssen wir wie in einer ZendApplication-Anwendung üblich ZendApplication starten und den Speicherort unserer Konfigurationsdatei (idR. application.ini) bekannt geben. Wenn wir nun die benötigten Module bootstrappen, können wir mit dem eigentlichem Cronjob fort fahren. Hier können dann die benötigten Aufgaben eingestellt und ausgeführt werden. So zB. ein Backup von Dateien/Datenbanken anlegen, eMails versenden oder vieles vieles mehr.

 

Ein rutschig-kaltes-verschneites Hallo Euch allen.

Es freut mich, Euch heute den dritten Teil der NestedSets-Implementierung für Zend_Db_Table vorweisen zu können.

Beginnen wir zunächst mit verschiedenen Möglichkeiten, Daten unter bestimmten Bedingungen zu erhalten.

Wollen wir wissen, wie viele Kinder jedes Element hat, können wir das mit etwas Geschick wie folgt lösen:

    public function fetchWithAggregateCount()
    {
        $sql = "SELECT `parent`.*, COUNT(`product`.{$this->_primary}) AS `childElements`
                FROM `{$this->_name}` AS `node`,
                     `{$this->_name}` AS `parent`,
                     `{$this->_name}` AS `product`
                WHERE `node`.`{$this->_leftColumn}` BETWEEN `parent`.`{$this->_leftColumn}` AND `parent`.`{$this->_rightColumn}`
                    AND `node`.`{$this->_primary}`=`product`.{$this->_primary}
                GROUP BY `parent`.{$this->_primary}
                ORDER BY `node`.`{$this->_leftColumn}`;";

        return $this->getAdapter()->fetchAll($sql);
    }

In childElements ist die Anzahl der Kinder enthalten. Ein Beispieldatensatz könnte so aussehen:

array
  0 =>
    array
      'test_id' => string '1' (length=1)
      'test_name' => string 'Tierreich' (length=9)
      'test_left' => string '1' (length=1)
      'test_right' => string '32' (length=2)
      'childElements' => string '16' (length=2)
  1 =>
    array
      'test_id' => string '3' (length=1)
      'test_name' => string 'Fische' (length=6)
      'test_left' => string '2' (length=1)
      'test_right' => string '13' (length=2)
      'childElements' => string '6' (length=1)
  2 =>
    array
      'test_id' => string '21' (length=2)
      'test_name' => string 'Stör' (length=5)
      'test_left' => string '3' (length=1)
      'test_right' => string '4' (length=1)
      'childElements' => string '1' (length=1)
// ...

Hier wird deutlich, dass inklusive dem Rootelement 15 weitere Elemente vorhanden sind. Die Kategorie Fische beinhaltet 5 Datensätze, welche Fische repräsentieren und Stör ist das einzige Element ohne weitere Unterelemente.

Kommen wir zu einem recht praktischen Beispiel. Für Breadcrumbs bzw. zur Erstellung einer Sitemap benötigen wir zB. die Tiefe eines Elementes um eventuell eine Einrückung vornehmen zu können. Die Tiefe aller Elemente erhalten wir, indem wir eine solche Abfrage durchführen:

    public function fetchDepthOfNodes()
    {
        $sql = "SELECT `node`.*, (COUNT(`parent`.`{$this->_primary}`) - 1) AS `depth`
                FROM `{$this->_name}` AS `node`,
                     `{$this->_name}` AS `parent`
                WHERE `node`.`{$this->_leftColumn}` BETWEEN `parent`.`{$this->_leftColumn}` AND `parent`.`{$this->_rightColumn}`
                GROUP BY `node`.`{$this->_primary}`
                ORDER BY `node`.`{$this->_leftColumn}`;";

         return $this->getAdapter()->fetchAll($sql);
    }

Diese Abfrage liefert uns ein Array aller Datensätze zurück und fügt als zusätzlichen Key depth an, welcher die Tiefe des Elementes widerspiegelt. depth = 0 bedeutet, dass wir ein Rootelement haben. depth = 1 wäre das erste Kindelement des Rootelements.

Nun möchten wir jedoch nicht bei jeder Abfrage die Tiefe aller Datensätze in Erfahrung bringen, da dies bei großen Datenbank zu viel Overhead darstellen kann. Der Gedanke, nur die Tiefe der Elemente unter einem bestimmten Node abzufragen liegt nahe. Wie gedacht, so getan:

    public function fetchDepthOfSubtreeWithId($id)
    {
        $sql = "SELECT `node`.*, (COUNT(`parent`.`{$this->_primary}`) - (sub_tree.`depth` + 1)) AS `depth`
                FROM `{$this->_name}` AS `node`,
                     `{$this->_name}` AS `parent`,
                     `{$this->_name}` AS `sub_parent`,
                (
                    SELECT `node`.`{$this->_primary}`, (COUNT(`parent`.`{$this->_primary}`) - 1) AS `depth`
                    FROM `{$this->_name}` AS `node`,
                         `{$this->_name}` AS `parent`
                    WHERE `node`.`{$this->_leftColumn}` BETWEEN `parent`.`{$this->_leftColumn}` AND `parent`.`{$this->_rightColumn}`
                        AND `node`.`{$this->_primary}`=:id
                    GROUP BY `node`.`{$this->_primary}`
                    ORDER BY `node`.`{$this->_leftColumn}`
                ) AS `sub_tree`
                WHERE `node`.`{$this->_leftColumn}` BETWEEN `parent`.`{$this->_leftColumn}` AND `parent`.`{$this->_rightColumn}`
                    AND `node`.`{$this->_leftColumn}` BETWEEN `sub_parent`.`{$this->_leftColumn}` AND `sub_parent`.`{$this->_rightColumn}`
                    AND `sub_parent`.`{$this->_primary}` = `sub_tree`.`{$this->_primary}`
                GROUP BY `node`.`{$this->_primary}`
                ORDER BY `node`.`{$this->_leftColumn}`;";

         $bind = array(
             ':id' => $id,
         );

         return $this->getAdapter()->fetchAll($sql, $bind);
    }

Als Ergebnis erhalten wir für die ID = 3 (Kategorie Fische) das nachstehende Array:

array
  0 =>
    array
      'test_id' => string '3' (length=1)
      'test_name' => string 'Fische' (length=6)
      'test_left' => string '2' (length=1)
      'test_right' => string '13' (length=2)
      'depth' => string '0' (length=1)
  1 =>
    array
      'test_id' => string '21' (length=2)
      'test_name' => string 'Stör' (length=5)
      'test_left' => string '3' (length=1)
      'test_right' => string '4' (length=1)
      'depth' => string '1' (length=1)
  2 =>
    array
      'test_id' => string '20' (length=2)
      'test_name' => string 'Sardine' (length=7)
      'test_left' => string '5' (length=1)
      'test_right' => string '6' (length=1)
      'depth' => string '1' (length=1)
// ...

Da wir test_id = 3 als Rootelement für unsere aktuelle Abfrage angegeben haben, ist die erste Zeile mit einer Tiefe von 0 versehen.

Wie sieht es jedoch aus, wenn wir die Tiefe und die Anzahl der Kinder gleichzeitig benötigen? Auch dieser Fall soll nicht vernachlässigt werden ;)

    public function fetchWithDepthAndOffspring()
    {
        $sql = "SELECT `node`.*, COUNT(*)-1 AS `depth`,
                    ROUND ((`node`.`{$this->_rightColumn}` - `node`.`{$this->_leftColumn}` - 1) / 2) AS `offspring`
                FROM `{$this->_name}` AS `node`,
                     `{$this->_name}` AS `parent`
                WHERE `node`.`{$this->_leftColumn}` BETWEEN `parent`.`{$this->_leftColumn}` AND `parent`.`{$this->_rightColumn}`
                GROUP BY `node`.`{$this->_leftColumn}`
                ORDER BY `node`.`{$this->_leftColumn}`;";

        return $this->getAdapter()->fetchAll($sql);
    }

Wollen wir es noch etwas komplizierter machen, als es eh schon ist? Natürlich – sonst würde es ja keinen Spaß machen ..

Holen wir uns also alle Datensätze und geben zusätzlich noch an, ob übergeordnete oder untergeordnete Zeilen vorhanden sind oder nicht:

    public function fetchWithChildAndParentCount()
    {
        $sql = "SELECT `node`.*,
                    round((`node`.`{$this->_rightColumn}`-`node`.`{$this->_leftColumn}`-1)/2,0) AS `offspring`,
                    count(*)-1+(`node`.test_left>1) AS `depth`,
                    ((min(`parent`.`{$this->_rightColumn}`)-`node`.`{$this->_rightColumn}`-(`node`.`{$this->_leftColumn}`>1))/2) > 0 AS `lower`,
                    (((`node`.`{$this->_leftColumn}`-max(`parent`.`{$this->_leftColumn}`)>1))) AS `upper`
                FROM `{$this->_name}` AS `node`,
                     `{$this->_name}` AS `parent`
                WHERE `node`.`{$this->_leftColumn}` BETWEEN `parent`.`{$this->_leftColumn}` AND `parent`.`{$this->_rightColumn}`
                    AND (`parent`.`{$this->_primary}` != `node`.`{$this->_primary}` OR `node`.`{$this->_leftColumn}` = 1)
                GROUP BY `node`.`{$this->_primary}`
                ORDER BY `node`.`{$this->_leftColumn}`;";

        return $this->getAdapter()->fetchAll($sql);
    }

Das Ergbenis dieses Methodenaufrufs könnte das nachstehende Array ergeben:

// ...
  1 =>
    array
      'test_id' => string '3' (length=1)
      'test_name' => string 'Fische' (length=6)
      'test_left' => string '2' (length=1)
      'test_right' => string '13' (length=2)
      'offspring' => string '5' (length=1)
      'depth' => string '1' (length=1)
      'lower' => string '1' (length=1)
      'upper' => string '0' (length=1)
  2 =>
    array
      'test_id' => string '21' (length=2)
      'test_name' => string 'Stör' (length=5)
      'test_left' => string '3' (length=1)
      'test_right' => string '4' (length=1)
      'offspring' => string '0' (length=1)
      'depth' => string '2' (length=1)
      'lower' => string '1' (length=1)
      'upper' => string '0' (length=1)
// ...

Viel Spaß beim auswerten ;)

Mit lower und upper erhalten wir die Tiefe nach unten/oben – sollten noch Elemente vorhanden sein. depth ist weiterhin die Tiefe ausgehend vom Rootelement.

Im vierten Teil der NestedSets-Reihe werde ich Euch endlich zeigen, wie ihr Subtrees tauscht und die Positionen von Subtrees ändert/verschiebt.

© 2010-2012 RenePardon BoonWeb Suffusion theme by Sayontan Sinha