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:
- PEAR\propel\engine\builder\om\php5\PHP5ZendObjectBuilder.php – stworzymy własną klasę do generowania “naszego” kodu klasy dziedziczącej po Zend_Db_Table_Abstract
- PEAR\data\propel_generator\default.properties – zmodyfikujemy dodając parę opcji oraz zmienimy domyślną klasę generatora klasy PHP na wspomnianą w punkcie 1.
- 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.
Brak komentarzy.