<?php // ?>

praktyczne porady dotyczące programowania aplikacji internetowych

Jak napisać własny parser tagów korzystając z Web Service

Często przy pisaniu kodu i jeszcze częściej przy poprawianiu kodu napisanego przez inne osoby stajemy na głowie, jak opracować lub zrozumieć logikę działającej aplikacji. Ten artykuł ma za zadanie przybliżyć kwestie jak programować nie tworząc plików index.php, w których mieszane są wszystkie możliwe języki HTML, PHP, Javascript, SQL, CSS co utrudnia ich analizę. Oczywiście istnieją pewne zależnosci jak na przykład SQL, który siłą rzeczy musi być wywoływany z poziomu PHP oraz inne. Niemniej jednak przy odpowiednim podejściu można sobie ułatwić życie opracowując kod w sposób, który umożliwi nam jego późniejszą aktualizację, a innym osobom, które może kiedyś będą musiały coś do niego dopisać oszczędzi trochę i tak już siwych włosów na głowie :)

Najważniejszż zasadą w pisaniu jakiejkolwiek aplikcji i nie tyczy się to tylko i wyłacznie tych webowych wydaje się być oddzielenie logiki od procedur wizualizujacych, czyli w przypadku www HTML'a, który w powiązaniu ze stylami kaskadowymi CSS oraz z Javascript jest odpowiedzialny za stronę wizualizacyjną projektu. No tak, ale HTML sam z siebie może jedynie wyświetlać dane, celowo pomijam tutaj XSLT lub ESI, języki, które nadają się owszem do wizualizowania pobieranych z plików typu XML, mają i one jednak pewne ograniczenia. XSLT operuje na pojedynczej stronie, natomiast ESI jakkolwiek ciekawy w założeniu jest słabo udokumentowany a jeszcze ciężej znaleźć providera, który zainstaluje nam go na serwerze.

Idealnym wyjściem jest zatem stworzenie własnego parsera, który oprócz standardowych tagów HTML przetwarzałby własne dane, które to z kolei byłyby powiązane z określonymi funkcjami, zatem do dzieła!

Założenia

Kod strony bedzie stworzony w oparciu o jezyk XML, którego elementy będą nastepnie w odpowiedni sposób parsowane. W moim przykładzie będzie to prosta strona wyświetlająca tytuł, nagłówek i osadzony dokument, który z kolei będzie generowany automatycznie na podstawie innego template. Komunikacja z bazą danych będzie natomiast zrealizowana za pomocą własnego Web Service. Dla uproszczenia wszystkie pliki włącznie z Web Service będą umieszczone w jednym katalogu.

Kod XML strony

Zgodnie z wcześniejszymi założeniami poniżej przedstawiam kod strony zapisany jako xml/index.xml, który nastepnie będzie parsowany.

<?xml version="1.0" encoding="UTF-8"?>
<template>
     <head>
         <title>Parser</title>
     </head>
     <body>
         <div id="div-container">
             <div id="div-header">
                 <h1>Head</h1>
                 <h2>Subhead</h2>
             </div>
             <div id="div-content">
                 <!-- docs tag -->
                 <docs template="xml/doc.xml" id="1" />
             </div>
         </div>
     </body>
</template>

oraz szablon dokumentu zapisany pod nazwą xml/doc.xml

<?xml version="1.0" encoding="UTF-8"?>
<template>
    <!-- vars tag -->
    <h3><var name="title" /></h3>
    <p><var name="content" /></p>
</template>

Zwróć uwagę na konstrukcję XML’a atrybuty muszą być umieszczone w cudzysłowach niedozwolone jest używanie apostrofów. Tagi pojedyncze muszą być zamknięte znakiem / (slash).

Baza danych MySQL

Dla potrzeb projektu należy stworzyć przykładową bazę danych, która w moim przypadku ograniczy się do jednej tabeli docs o następującej konstrukcji:

CREATE TABLE IF NOT EXISTS `docs` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `title` varchar(255) NOT NULL,
  `content` text NOT NULL,
  PRIMARY KEY  (`id`)
);

gdzie zostanie zapisany przykładowy rekord:

INSERT INTO `docs` (`id`, `title`, `content`) VALUES
(1, 'My Title', 'Example of content');

bazę danych należy utworzyć samodzielnie zmieniając odpowiednie dane potrzebne do zalogowania się do niej umieszczone w dalszej części artykułu.

WSDL

Do komunikacji z bazą danych jak już wcześniej napisałem użyję Web Service, z plikiem WSDL wygenerowanym za pomocą rozszerzenia WSF / WSO2. Omijam tutaj proces tworzenia samego pliku, odsyłając zainteresowanych do innego artykułu na tej stronie. Wygenerowany plik WSDL zapisany pod nazwą doc.xml ma następującą postać:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://www.wso2.org/php"
xmlns:tnx="http://www.wso2.org/php/xsd"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:http="http://www.w3.org/2003/05/soap/bindings/HTTP/"
xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl"
targetNamespace="http://www.wso2.org/php">
    <types>
        <xsd:schema elementFormDefault="qualified" targetNamespace="http://www.wso2.org/php/xsd" xmlns:ns0="{WEBSERVICE}">
            <xsd:import namespace="{WEBSERVICE}"/>
            <xsd:element name="getDoc">
                <xsd:complexType>
                    <xsd:sequence>
                        <xsd:element name="id" type="xsd:nonNegativeInteger"/>
                    </xsd:sequence>
                </xsd:complexType>
            </xsd:element>
            <xsd:element name="getDocResponse">
                <xsd:complexType>
                    <xsd:sequence>
                        <xsd:element name="properties" maxOccurs="unbounded" type="ns0:getDocResponse"/>
                    </xsd:sequence>
                </xsd:complexType>
            </xsd:element>
        </xsd:schema>
        <xsd:schema elementFormDefault="qualified" targetNamespace="{WEBSERVICE}">
            <xsd:complexType name="getDocResponse">
                <xsd:sequence>
                    <xsd:element name="id" type="xsd:nonNegativeInteger"/>
                    <xsd:element name="title" type="xsd:string"/>
                    <xsd:element name="content" type="xsd:string"/>
                </xsd:sequence>
            </xsd:complexType>
        </xsd:schema>
    </types>
    <message name="getDoc">
        <part name="parameters" element="tnx:getDoc"/>
    </message>
    <message name="getDocResponse">
        <part name="parameters" element="tnx:getDocResponse"/>
    </message>
    <portType name="ws.doc.phpPortType">
        <operation name="getDoc">
            <input message="tns:getDoc"/>
            <output message="tns:getDocResponse"/>
        </operation>
    </portType>
    <binding name="ws.doc.phpSOAPBinding" type="tns:ws.doc.phpPortType">
        <soap:binding xmlns="http://schemas.xmlsoap.org/wsdl/soap/" transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
        <operation xmlns:default="http://schemas.xmlsoap.org/wsdl/soap/" name="getDoc">
            <soap:operation xmlns="http://schemas.xmlsoap.org/wsdl/soap/" soapAction="INSERT_PATH_HERE/ws.doc.php/getDoc" style="document"/>
            <input xmlns:default="http://schemas.xmlsoap.org/wsdl/soap/">
                <soap:body xmlns="http://schemas.xmlsoap.org/wsdl/soap/" use="literal"/>
            </input>
            <output xmlns:default="http://schemas.xmlsoap.org/wsdl/soap/">
                <soap:body xmlns="http://schemas.xmlsoap.org/wsdl/soap/" use="literal"/>
            </output>
        </operation>
    </binding>
    <service name="ws.doc.php">
        <port xmlns:default="http://schemas.xmlsoap.org/wsdl/soap/" name="ws.doc.phpSOAPPort_Http" binding="tns:ws.doc.phpSOAPBinding">
            <soap:address xmlns="http://schemas.xmlsoap.org/wsdl/soap/" location="INSERT_PATH_HERE/ws.doc.php"/>
        </port>
    </service>
</definitions>

Plik zawiera definicję funkcji wejściowej getDoc, która oczekuje parametru id, czyli numeru rekordu do pobrania z bazy danych i wyjściowej struktury getDocResponse, która zawiera pełny opis wyniku w moim przypadku jest to bezpośrednie odzwierciedlenie rekordu z trzema właściwościami odpowiadającymi polom w tabeli, czyli: id, title oraz content. Pamiętaj o podmianie INSERT_PATH_HERE na odpowiednią wartość.

Web Service

Kolejny etap mojego projektu, to stworzenie Web Service wykonującego operację odczytu rekordu z bazy danych oto kod.

/**
  * Doc response class
  */
class getDocResponse
{
    public $id;
    public $title;
    public $content;
}

/**
  * Doc request class
  */
class Doc {
    /**
      * __construct
      */
    function __construct () {}

    /**
      * getDoc
      *
      * get doc example
      *
      * @param int $id node's id
      * (maps to the xs:nonNegativeInteger XML schema type)
      * @return array of object getDocResponse $properties nodes' list
      * (maps to the xs:anyType XML schema type)
      */
     public function getDoc ($ob)
    {
        // db operation
        $id_mysql = @ mysql_connect ("LOCALHOST", "USER", "PASSWORD");
        $id_db = @ mysql_select_db ("DBNAME", $id_mysql);
        $id_query = mysql_query ("
            select
                *
            from
                `docs`
            where 1 = 1
                and id = " . $ob -> id . "
        ");
        $ar_row = mysql_fetch_row ($id_query);
        mysql_close ($id_mysql);
       
        // get result
        $ob_result = new stdClass ();
        $ob_result -> id      = $ar_row["id"];
        $ob_result -> title   = $ar_row["title"];
        $ob_result -> content = $ar_row["content"];
       
        // return
        return array ("properties" => $ob_result);
    }
}

ini_set ("soap.wsdl_cache_enabled", 0);
$ob_server = new SoapServer ("INSERT_PATH_HERE/doc.xml?wsdl");
$ob_server -> setClass ("Object");
$ob_server -> handle();

Pamiętaj o podmianie wytłuszczonych LOCALHOST, USER, PASSWORD, DBNAME na odpowienie dane oraz INSERT_PATH_HERE na ścieżkę, gdzie będzie umieszczony ten przykład.

Polecam aplikację soapUI do sprawdzania działania Web Service, dzięki której można zaimportować plik WSDL, sprawdzając poprawność jego konstrukcji i użyć bezpośrednio do testów.

index.php

Właściwa strona index.php, która wyświetli wynik będzie miała za zadanie wykonanie następujących operacji:

  1. Odczyt pliku.
  2. Konwersja pliku do obiektu DOM.
  3. Rekurencyjne parsowanie danych. Zwróć uwagę, że w moim przykładzie funkcja _parser jest wywoływana kilkukrotnie. Na przykład dane wejściowe do własnego tagu docs są również parsowane wykorzystując informacje przekazane w atrybutach.

oto kod:

$st_xml = _get_file ("xml/index.xml");
$ob_dom = _get_dom ($st_xml);
print "" . _parser ($ob_dom) . "";

/**
 * _get_dom
 *
 * @access public
 * @param  string $st_xml
 * @return object $ob_dom
 */
function _get_dom ($st_xml)
{
    $ob_dom = new DOMDocument ("1.0", "UTF-8");
    $ob_dom -> preserveWhiteSpace = false;
    $ob_dom -> formatOutput = true;
    $ob_dom -> loadXML ($st_xml);
   
    return $ob_dom;
}

/**
 * _get_file
 *
 * @access public
 * @param  string $st_file file's path
 * @return mixed string content if success or false if error occured
 */
function _get_file ($st_file)
{
    $id_file = @ fopen ($st_file, "r");
    if (! $id_file) {
        return false;
    }
   
    $st_content = @ fread ($id_file, filesize ($st_file));
   
    if (! $st_content) {
        return false;
    }
   
    fclose ($id_file);
   
    return $st_content;
}

/**
 * _parser
 *
 * @access public
 * @param  object $ob_node
 * @param  integer $in_level
 * @return string generated HTML
 */
function _parser ($ob_node, $in_level = 0)
{
    $st = "";
    $st_name = $ob_node -> nodeName;
    $ar_attr = array ();
   
    // get attributes
    if ($ob_node -> attributes) {
        foreach ($ob_node -> attributes as $ob_attr) {
            $ar_attr[$ob_attr -> name] = $ob_attr -> value;
        }
    }
   
    if (is_object ($ob_node) && $ob_node -> hasChildNodes ()) {
        // double tag
        $ob_subnodes = $ob_node -> childNodes;
       
        $st_inner = "";
       
        switch ($st_name) {
            default:
                foreach ($ob_subnodes as $ob_subnode) {
                    $in_level++;
                    switch ($ob_subnode -> nodeName) {
                        case "#text": // text
                            $st_inner .= trim ($ob_subnode -> nodeValue);
                            break;
                           
                        case "#comment": // do nothing it's only comment
                            $st_inner .= "";
                            break;
                       
                        default: // get inner content
                            $st_inner .= _parser ($ob_subnode, $in_level);
                    }
                    $in_level--;
                }
        }
       
        if ($st_name != "#document" && $st_name != "template") {
            $st_attr = "";
            foreach ($ar_attr as $st_key => $st_value) {
                $st_attr .= $st_key . "=\"" . $st_value . "\"";
            }
            $st .= "<$st_name" .  ($st_attr ? " $st_attr" : "") . ">" . $st_inner . "";
        }
        else {
            $st .= $st_inner;
        }
       
    }
    else {
        // single tag
        switch ($st_name) {
            // render special tag docs
            case "docs":
                // get data from Web Service
                $ob_soap = new SoapClient ("INSERT_PATH_HERE/doc.xml?wsdl", array ("trace" => true));
                $ob_result = $ob_soap -> getDoc (array (
                    "id" => $ar_attr["id"]
                ));
               
                // set variables recieved from WS
                $_SESSION["id"]      = $ob_result -> properties -> id;
                $_SESSION["title"]   = $ob_result -> properties -> title;
                $_SESSION["content"] = $ob_result -> properties -> content;
               
                // docs template recursion parsing
                $st_xml = _get_file ($ar_attr["template"]);
                $ob_dom = _get_dom ($st_xml);
                $st    .= _parser ($ob_dom);
               
                break;
               
            // render special tag var
            case "var":
                $st .= $_SESSION[$ar_attr["name"]];
                break;
               
            // render standard tag
            default:
                $st_attr = "";
                foreach ($ar_attr as $st_key => $st_value) {
                    $st_attr .= $st_key . "=\"" . $st_value . "\"";
                }
                $st .= "<$st_name $st_attr />";
        }
    }
   
    return $st;
}

I to wszystko, teraz wystarczy wrzucić na serwer wszystkie pliki z powyższego artykułu, czyli:

  • xml/index.xml
  • xml/doc.xml
  • index.php
  • doc.xml
  • ws.doc.php
  • wsf.doc.php - plik dołączony do archiwum poniżej

utworzyć bazę danych wg wcześniejszych wskazówek i podmienić odpowiednie dane w plikach i uruchomić index.php z przeglądarki. Całość do ściągnięcia jest również tutaj. Dodatkowo umieszczam plik wsf.doc.php na podstawie którego rozszerzenie WSF / WSO2 tworzy automatycznie WSDL.

Wenecja 2009 opracowany przez Michał Luberda

Parser tagów w PHP z wykorzystaniem Web Service