Zenedith's dev blog

Dałem się namówić.., szatanowi chyba..

Generowanie klasy Zend_Db_Table_Abstract z uzyciem Propel’a.

Zend Framework jest naprawdę fajną biblioteką do tworzenia w technologii PHP.
Tematem tego wpisu będzie sposób na wygenerowanie klasy dziedziczącej po Zend_Db_Table_Abstract
na podstawie schematu xml dla generatora Propel.

Zainspirował mnie do tego wpis ze strony bloga amnuts’a.

Przed przystąpieniem do pracy, wypadałoby zainstalować ten pakiet oraz dodatkowe wymagane – jest to opisane dokładnie tutaj.

Propel to ORM dla PHP, jeden z dwóch najbardziej rozpoznawalnych z Doctrine.
Do wygenerowania użyję Propela z tej prostej przyczyny, że miałem z nim już do czynienia oraz jego „obsługa” jest banalnie prosta.

Dlaczego wogóle używać Propela żeby wygenerować klasę Zend_Db_Table_Abstract skoro Propel załatwia i tak wszystko za nas bez żadnej ingerencji (dostajemy gotowe klasy z mapowaniem do tabel)?

M.in. dlatego, żeby nie dokładać kolejnych zależności do projektu, szczególnie gdy w jego skład wchodzi już bogata biblioteka Zend.


Za przykład użycia posłuży nam dostępny w dokumentacji schemat 3 tabel: Book, Author i Publisher, który wygląda następująco:

<?xml version=“1.0″ encoding=“UTF-8″?>
<database name=“nazwa_bazy” defaultIdMethod=“native”>
<table name=“book”>
<column name=“book_id” type=“integer” required=“true” primaryKey=“true” autoIncrement=“true” />
<column name=“title” type=“varchar” size=“255″ required=“true”/>
<column name=“author_id” type=“integer” required=“true” />
<column name=“publisher_id” type=“integer” required=“true” />

<foreign-key foreignTable=“author”>
<reference local=“author_id” foreign=“author_id” />
</foreign-key>
<foreign-key foreignTable=“publisher”>
<reference local=“publisher_id” foreign=“publisher_id”/>
</foreign-key>
</table>
<table name=“author”>
<column name=“author_id” type=“integer” required=“true” primaryKey=“true” autoIncrement=“true”/>
<column name=“first_name” type=“varchar” size=“128″ required=“true” />
<column name=“last_name” type=“varchar” size=“128″ required=“true” />
</table>
<table name=“publisher” description=“the publisher table”>
<column name=“publisher_id” type=“integer” required=“true” primaryKey=“true” autoIncrement=“true” />
<column name=“name” type=“varchar” size=“128″ required=“true”/>
</table>
</database>

Jest to bardzo prosty model, posiadający po jednym kluczu głównym (primary key), co będzie miało dla nas takie znaczenie, że zaprezentowany generator działa (chwilowo) tylko dla takich kluczy głównych (jedno-kolumnowych).

Do wygenerowania modelu potrzebna jest jeszcze konfiguracja pliku build.properties, w którym definiujemy:
propel.project = nazwa_projektu
propel.database = mysql
propel.database.url = mysql:host=localhost;dbname=nazwa_bazy
propel.database.user=root
propel.database.password=root

Ustawienia te zostaną przekazane do pliku ustawień generatora (dostęp przez zapis ${propel.project}), który znajduje się w następującej lokalizacji:
PEAR\data\propel_generator\default.properties.

Standardowo wywołanie generatora to polecenie:
propel-gen.bat nazwa_projektu

Generator utworzy wówczas pełną strukturę ORM, gotową do pracy z PHP w momencie zdefiniowania danych projektu w pliku runtime-conf.xml,  oraz skrypt SQL do utworzenie tabel zdefiniowanych w schemacie, w odpowiednim dla wybranego tyou bazy danych formacie.

Na tym kończy się krótkie streszczenie tutorial’a ze strony Propel’a – dla nieobeznanych polecam zajrzeć i zapoznać się z tą bardzo przejrzystą dokumentacją.

Przejdźmy do meritum.

Wprowadzimy zmiany w 3 plikach:

  1. PEAR\propel\engine\builder\om\php5\PHP5ZendObjectBuilder.php – stworzymy własną klasę do generowania „naszego” kodu klasy dziedziczącej po Zend_Db_Table_Abstract
  2. PEAR\data\propel_generator\default.properties – zmodyfikujemy dodając parę opcji oraz zmienimy domyślną klasę generatora klasy PHP na wspomnianą w punkcie 1.
  3. PEAR\propel\engine\phing\PropelOMTask.php – zmienimy sposób generowania plików na podstawie dodanych opcji w pliku z punktu 2.

Nasza klasa do generowania klasy Zend’a PHP5ZendObjectBuilder.php wygląda następująco:

<?php

require_once ‘propel/engine/builder/om/php5/PHP5ObjectBuilder.php’;

class PHP5ZendObjectBuilder extends PHP5ObjectBuilder {

 public $refmap_outer = ”;
 public $refmap_inner = ”;
 public $FK_NAMES = array();
 public $COLS_NAMES = array();

 public function __construct(Table $table) {
   parent::__construct($table);

   $this->refmap_outer = <<<EOT
 protected \$_referenceMap = array(
%s
 );
EOT;

   $this->refmap_inner = <<<EOT
‘%s’ => array(
‘columns’     => array(%s),
‘refTableClass’ => %s,
‘refColumns’     => array(%s),
   ‘refClass’   => %s
   )
EOT;
 }

 public function addRequired(&$script)
 {
   $script .= “
require_once ‘phpinc/Db/Table.php’;
“;
 }

 /**
  * Gets the package for the [base] object classes.
  * @return     string
  */
 public function getPackage()
 {
   return “ZendClasses”;
 }

 /**
  * Adds class phpdoc comment and openning of class.
  * @param      string &$script The script will be modified in this method.
  */
 protected function addClassOpen(&$script)
 {
   $this->addRequired($script);

   $table = $this->getTable();
   $tableName = $table->getName();
   $tableDesc = $table->getDescription();
   $interface = $this->getInterface();
   $extending_class = $this->getBuildProperty(‘builderObjectClassExtending’);

   $script .= “
/**
* Base class that represents a row from the ‘$tableName’ table.
*
* $tableDesc
*”;
   if ($this->getBuildProperty(‘addTimeStamp’)) {
     $now = strftime(‘%c’);
     $script .= “
* This class was autogenerated by Propel “ . $this->getBuildProperty(‘version’) . ” on:
*
* $now
*”;
   }
   $script .= “
*/
class “.$this->getClassname().” extends “.$extending_class.“
{
“;
 }

 /**
  * Specifies the methods that are added as part of the basic OM class.
  * This can be overridden by subclasses that wish to add more methods.
  * @see        ObjectBuilder::addClassBody()
  */
 protected function addClassBody(&$script)
 {
   $table = $this->getTable();
   if (!$table->isAlias()) {
     $this->addConstants($script);
     $this->addAttributes($script);
   }

   $this->addCollsDefinition($script);

   // $this->addConstructor($script);
 }

 /**
  * Closes class.
  * @param      string &$script The script will be modified in this method.
  */
 protected function addClassClose(&$script)
 {
   $script .= “
} // “ . $this->getClassname() . “
“;
 }

 /**
  * Adds any constants to the class.
  * @param      string &$script The script will be modified in this method.
  */
 protected function addConstants(&$script)
 {
   $tableName = $this->prefixTableName($this->getTable()->getName());
   $dbName = $this->getDatabase()->getName();
   $script .= “
 /** the table name for this class */
 const TABLE_NAME = ‘$tableName’;
 protected \$_name = ‘$tableName’;
  “;
 }

 /**
  * Adds class attributes.
  * @param      string &$script The script will be modified in this method.
  */
 protected function addAttributes(&$script)
 {
   $table = $this->getTable();

   if (!$table->isAlias()) {
     $this->addColumnAttributes($script);
   }

   $this->addFK($table, $script);
 }

 public function addFK($table, &$script)
 {
   $refs = array();
   $r = 0;

   foreach ($table->getForeignKeys() as $fk) {

     $table_name = $fk->getForeignTableName();
     $COLS = $fk->getLocalColumns();

     foreach ($COLS as $lp => $cols_name) {
       if (array_key_exists($cols_name, $this->FK_NAMES)) {
         $this->FK_NAMES[$cols_name] = $table_name;
       }
     }

     $FK_FLIP = array_flip($this->FK_NAMES);

     $column_name = strtoupper($FK_FLIP[$table_name]);
     $class_ref_name = $this->getBuildProperty(‘basePrefix’) . $this->getForeignTable($fk)->getPhpName();
     $referenced_column_name = ‘ID’;

     $refs[] = sprintf($this->refmap_inner,
    ‘ref’ . ++$r,
    ’self::’.$column_name,
     $class_ref_name.‘::TABLE_NAME’,
     $class_ref_name.‘::’.$referenced_column_name,
    “‘$class_ref_name’”
     );
   }

   if (!empty($refs)) {
    $script .= “
“;
    $script .= sprintf($this->refmap_outer, join(“,\n“, $refs));
  }
 }

 public function addCollsDefinition(&$script)
 {
   if (!empty($this->COLS_NAMES)) {
     $script .= “
 protected \$_cols = array(
  ‘”.implode(“‘,\n    ’”, $this->COLS_NAMES).“‘
 );”;
   }

 }
 /**
  * Add comment about the attribute (variable) that stores column values
  * @param      string &$script The script will be modified in this method.
  * @param      Column $col
  **/
 protected function addColumnAttributeComment(&$script, Column $col) {
   $cptype = $col->getPhpType();
   $clo = strtolower($col->getName());

   $script .= “
 /**
  * The value for the $clo field.”;
   if ($col->getDefaultValue()) {
     if ($col->getDefaultValue()->isExpression()) {
       $script .= “
  * Note: this column has a database default value of: (expression) “.$col->getDefaultValue()->getValue();
     } else {
       $script .= “
  * Note: this column has a database default value of: “. $this->getDefaultValueString($col);
     }
   }

   if ($col->isPrimaryKey()) {
     $script .= “
  * This is primary key.
  “;
   }
   $script .= “
  * @var        $cptype
  */”;
 }

 /**
  * Adds the declaration of a column value storage attribute
  * @param      string &$script The script will be modified in this method.
  * @param      Column $col
  **/
 protected function addColumnAttributeDeclaration(&$script, Column $col) {
   $var = strtolower($col->getName());

   $this->COLS_NAMES[] = $col->getName();

   if ($col->isForeignKey()) {
     $const = strtoupper($col->getName());
     $this->FK_NAMES[$var] = ”;
   }
   elseif ($col->isPrimaryKey()) {
     $const = ‘ID’;
     $script .= “
 protected \$_primary = ‘$var’;
“;
   }
   else {
     $const = strtoupper($col->getName());
   }

   $script .= “
 const “.$const.” = ‘” . $var . “‘;
“;
 }

 /**
  * Adds the comment for the constructor
  * @param      string &$script The script will be modified in this method.
  **/
 protected function addConstructorComment(&$script) {
   $script .= “
 /**
  * Initializes internal state of “.$this->getClassname().” object.
  */”;
 }

 /**
  * Adds the function declaration for the constructor
  * @param      string &$script The script will be modified in this method.
  **/
 protected function addConstructorOpen(&$script) {
   $script .= “
 public function __construct()
 {”;
 }

 /**
  * Adds the function body for the constructor
  * @param      string &$script The script will be modified in this method.
  **/
 protected function addConstructorBody(&$script) {
   $script .= “
parent::__construct();”;
 }

 /**
  * Adds the function close for the constructor
  * @param      string &$script The script will be modified in this method.
  **/
 protected function addConstructorClose(&$script) {
   $script .= “
 }
“;
 }

} // PHP5ZendObjectBuilder

Dziedziczy ona po domyślnej klasie generowania kodu klasy ORM dla Propela. Nadpisujemy w niej interesujące nas metody i o resztę nie musimy się martwić – można by dziedziczyć bezpośrednio po klasie ObjectBuilder ale dzięki dziedziczneniu po klasie PHP5ObjectBuilder otrzymujemy prostsza klasę.

Kolejną rzeczą do zrobienia jest edycja pliku ustawień default.properties.

Dodamy do niego następujące opcje:

  • propel.builder.overvrite = peer object mapbuilder (jakiego typu pliki generować)
  • propel.builder.no_overwrite = peerstub objectstub (jakiego typu pliki generować)
  • propel.builder.object.class.extending = Zend_Db_Table_Abstract (nazwa klasy po której będzie dziedziczenie)

oraz zmienimy wartość opcji:

propel.builder.object.class = propel.engine.builder.om.php5.PHP5ObjectBuilder

na

propel.builder.object.class = propel.engine.builder.om.php5.PHP5ZendObjectBuilder

Wszystko to razem zbierze klasa PropelOMTask.php.
Zmianie uległa tylko część metody main() dlatego pokażę tu tylko jej treść:

public function main()
 {
   // check to make sure task received all correct params
   $this->validate();

   $generatorConfig = $this->getGeneratorConfig();

   foreach ($this->getDataModels() as $dataModel) {
     $this->log(“Processing Datamodel : “ . $dataModel->getName());

     foreach ($dataModel->getDatabases() as $database) {

       $this->log(”  - processing database : “ . $database->getName());

       foreach ($database->getTables() as $table) {

         if (!$table->isForReferenceOnly()) {

           $this->log(“\t+ “ . $table->getName());

           // —————————————————————————————–
           // Create Peer, Object, and MapBuilder classes
           // —————————————————————————————–

           $BUILD = array();

           $to_build = $generatorConfig->getBuildProperty(‘builderOvervrite’);

           if (is_null($to_build)) {
             $BUILD = array(‘peer’, ‘object’, ‘mapbuilder’);
           }
           else {
             if ($to_build) {
               $BUILD = explode(‘ ‘, $to_build);
             }
           }

           if (!empty($BUILD)) {
             foreach ($BUILD as $target) {
               $builder = $generatorConfig->getConfiguredBuilder($table, $target);
               $this->build($builder);
             }
           }

           // —————————————————————————————–
           // Create [empty] stub Peer and Object classes if they don’t exist
           // —————————————————————————————–

           $to_build = $generatorConfig->getBuildProperty(‘builderNooverwrite’);

           if (is_null($to_build)) {
             $BUILD = array(‘peerstub’, ‘objectstub’);
           }
           else {
             if ($to_build) {
               $BUILD = explode(‘ ‘, $to_build);
             }
           }

           // these classes are only generated if they don’t already exist
           if (!empty($BUILD)) {
             foreach ($BUILD as $target) {
               $builder = $generatorConfig->getConfiguredBuilder($table, $target);
               $this->build($builder, $overwrite=false);
             }
           }

           // —————————————————————————————–
           // Create [empty] stub child Object classes if they don’t exist
           // —————————————————————————————–

           // If table has enumerated children (uses inheritance) then create the empty child stub classes if they don’t already exist.
           if ($table->getChildrenColumn()) {
             $col = $table->getChildrenColumn();
             if ($col->isEnumeratedClasses()) {
               foreach ($col->getChildren() as $child) {
                 $builder = $generatorConfig->getConfiguredBuilder($table, ‘objectmultiextend’);
                 $builder->setChild($child);
                 $this->build($builder, $overwrite=false);
               } // foreach
             } // if col->is enumerated
           } // if tbl->getChildrenCol

           // —————————————————————————————–
           // Create [empty] Interface if it doesn’t exist
           // —————————————————————————————–

           // Create [empty] interface if it does not already exist
           if ($table->getInterface()) {
             $builder = $generatorConfig->getConfiguredBuilder($table, ‘interface’);
             $this->build($builder, $overwrite=false);
           }

           // —————————————————————————————–
           // Create tree Node classes
           // —————————————————————————————–

           if ($table->treeMode()) {
             switch($table->treeMode()) {
               case ‘NestedSet’:
                 foreach (array(‘nestedsetpeer’, ‘nestedset’) as $target) {
                   $builder = $generatorConfig->getConfiguredBuilder($table, $target);
                   $this->build($builder);
                 }
                 break;

               case ‘MaterializedPath’:
                 foreach (array(‘nodepeer’, ‘node’) as $target) {
                   $builder = $generatorConfig->getConfiguredBuilder($table, $target);
                   $this->build($builder);
                 }

                 foreach (array(‘nodepeerstub’, ‘nodestub’) as $target) {
                   $builder = $generatorConfig->getConfiguredBuilder($table, $target);
                   $this->build($builder, $overwrite=false);
                 }
                 break;

               case ‘AdjacencyList’:
                 // No implementation for this yet.
               default:
                 break;
             }

           } // if Table->treeMode()

         } // if !$table->isForReferenceOnly()

       } // foreach table

     } // foreach database

   } // foreach dataModel

 } // main()

To wszytko co trzeba zrobić, żeby wygenerować pliki zgodnie z własnym „szablonem”.
Ponieważ Propel generuje jeszcze parę dodatkowych plików dla każdej z tabeli, wprowadziłem opcje, które to wyłączą – uzyskujemy wtedy tylko klasy „naszego Zend’owego modelu”.
W tym celu należy zmienić ustawienia opcji w pliku default.properties na następujące:

  • propel.builder.overvrite = object
  • propel.builder.nooverwrite =

Dodatkowy parametr dodany w opcjach:

propel.builder.object.class.extending

powoduje wstawienie podanego tekstu jako nazwy klasy po której wygenerowana klasa będzie dziedziczyć.

W podanym zagadnieniu będzie to oczywiście klasa Zend_Db_Table_Abstract, lecz wcale tak być nie musi, zwłaszcza jeśli dysponujemy własną klasą dziedziczącą po Zend_Db_Table_Abstract i generowane klasy będą miały po niej dziedziczyć.

Na koniec udostępniam archiwum ze zmienionymi plikami oraz przykładowe dane i wygenerowane dla nich klasy.

Do pobrania tutaj (rzeczywiste rozszerzenie to zip).

Podane rozwiązanie można usprawnić, przede wszystkim wprowadzając obsługę wielo-kolumnowych kluczy głównych, lepszej parametryzacji kodu, opcji, etc.

Napewno jednak może stanowić bazę do własnych rozwiązań i pomysłów.

PHP5ObjectBuilder=
Reklamy

30 Czerwiec, 2009 - Posted by | scripts, web | , ,

Brak komentarzy.

Skomentuj

Proszę zalogować się jedną z tych metod aby dodawać swoje komentarze:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj / Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj / Zmień )

Zdjęcie na Google+

Komentujesz korzystając z konta Google+. Wyloguj / Zmień )

Connecting to %s

%d blogerów lubi to: