(El presente es un ensayo de como ire descubriendo como hacer cada cosa, por lo que tendría modificaciones sucesivas, sin embargo la idea es que lo que leas sirva y funcione)
Es quizás muy petulante el título, pero esa es la idea. Osea Generar el Modelo. 🙂
Como seguro ya se darían cuenta, con anteriores artículos y traducciones, estoy estudiando symfony 1.2 para mi proyecto.
Una de las cosas que hice primero fue hacer un ambiente para mis pruebas, con el fin de tener un ámbito para jugar a lo que se me ocurra. Osea que mi schema tiene muchas cosas, pero como es de pruebas … que va!
Una de las cosas que siempre me gusto es lo del método mágico __toString() de las clases del modelo generado. El poder luego redefinirlo en la clase personalizada, solicitando que devuelva un getXXX() y poder ver el dato de la base en los ABM / CRUD del administrador en lugar de los PrimaryKey, es realmente mas lindo.
Pero tener que redefinir en cada clase, puede ser molesto …
Vamos, soy de los que se pierden haciendos “xxxxxx_id” por todos lados en el modelo … 🙂 Por lo que tengo que redefinir muchos __toString() .
Entonces porque no poder en el schema, definir un atributo tostring a true o especificar el nombre del campo a utilizar por el método magico, para que el generador/builder/constructor del modelo lo haga por mi ya desde la BaseClase.php ?
Mi instalacion de Symfony 1.2 esta en /Symfony1.2.
Digamos que en otro equipo distinto del que estamos trabajando, hice una descarga por SVN del la versión 1.2, y luego la copie a este DIR.
Imagino que esto no tiene ningun tipo de incidencia.
Veamos un poco el schema:
propel: category: _attributes: { phpName: Category, phpToString: Name } name: varchar(255)
La idea central, es definir un atributo phpToString que nos permita definir un texto, en este caso Name, y que en este caso tambien hace referencia al campo name declarado en la linea debajo.
La finalidad es lograr que se genere un código / método toString() para esta clase, como el siguiente :
/** * Magic Method __toString will return the value of some field * if you setup the attributes of your schema for this object/table */ public function __toString() { return $this->getName(); }
Note que por esta razón, puse Name, con la N en mayúsculas. Ya vere despues como hacer la transformación camelcase…
El generador al $this->get le agregará a continuación lo que hallamos definido
Entiendo que iniciamos el script por generar de cada BaseClase.php, desde SfObjectBuilder. A continuación un extracto de como las clases se van heredando, según su definición:
class SfObjectBuilder extends PHP5ObjectBuilder { class PHP5ObjectBuilder extends ObjectBuilder { abstract class ObjectBuilder extends OMBuilder { abstract class OMBuilder extends DataModelBuilder {
Hagamos un salto al objeto OMBuilder (/Symfony1.2/lib/plugins/sfPropelPlugin/lib/vendor/propel-generator/classes/propel/engine/builder/om/OMBuilder.php)
/** * Builds the PHP source for current class and returns it as a string. * * This is the main entry point and defines a basic structure that classes should follow. * In most cases this method will not need to be overridden by subclasses. This method * does assume that the output language is PHP code, so it will need to be overridden if * this is not the case. * * @return string The resulting PHP sourcecode. */ public function build() { $this->validateModel(); $script = "<" . "?php\n"; // intentional concatenation $this->addIncludes($script); $this->addClassOpen($script); $this->addClassBody($script); $this->addClassClose($script); return $script; }
Estos metodo tan interesantes los podemos encontrar implementado en (/Symfony1.2\lib\plugins\sfPropelPlugin\lib\vendor\propel-generator\classes\propel\engine\builder\om\php5\PHP5ObjectBuilder.php) el objeto PHP5ObjectBuilder (use la flechita que esta a la derecha de la barra siguiente para ver los metodos)
/** * Adds the include() statements for files that this class depends on or utilizes. * @param string &$script The script will be modified in this method. */ protected function addIncludes(&$script) { } // addIncludes() /** * Adds class phpdoc comment and openning of class. * @param string &$script The script will be modified in this method. */ protected function addClassOpen(&$script) { $table = $this->getTable(); $tableName = $table->getName(); $tableDesc = $table->getDescription(); $interface = $this->getInterface(); $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 .= " * @package ".$this->getPackage()." */ abstract class ".$this->getClassname()." extends ".ClassTools::classname($this->getBaseClass())." "; $interface = ClassTools::getInterface($table); if ($interface) { $script .= " implements " . ClassTools::classname($interface); } $script .= " { "; } /** * 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) { $script = "\n" . $script; $table = $this->getTable(); if (!$table->isAlias()) { $this->addConstants($script); $this->addAttributes($script); } $this->addConstructor($script); $this->addApplyDefaultValues($script); // NUEVO CODIGO // NEW CODE if ($this->getPhpToString() != false) { $this->addToString($script); } $this->addColumnAccessorMethods($script); $this->addColumnMutatorMethods($script); $this->addHasOnlyDefaultValues($script); $this->addHydrate($script); $this->addEnsureConsistency($script); $this->addManipulationMethods($script); $this->addValidationMethods($script); if ($this->isAddGenericAccessors()) { $this->addGetByName($script); $this->addGetByPosition($script); $this->addToArray($script); } if ($this->isAddGenericMutators()) { $this->addSetByName($script); $this->addSetByPosition($script); $this->addFromArray($script); } $this->addBuildCriteria($script); $this->addBuildPkeyCriteria($script); $this->addGetPrimaryKey($script); $this->addSetPrimaryKey($script); $this->addCopy($script); if (!$table->isAlias()) { $this->addGetPeer($script); } $this->addFKMethods($script); $this->addRefFKMethods($script); $this->addClearAllReferences($script); } /** * Closes class. * @param string &$script The script will be modified in this method. */ protected function addClassClose(&$script) { $script .= " } // " . $this->getClassname() . " "; }
Note la nueva porción de código
// NUEVO CODIGO // NEW CODE if ($this->getPhpToString() != false) { $this->addToString($script); }
Quien se encarga de analizar el schema sea YML o XML, es la clase sfPropelDatabaseSchema (E:\wamp\apps\symfony\1.2\lib\plugins\sfPropelPlugin\lib\addon\sfPropelDatabaseSchema.class.php)
La Clase Abstracta XMLElement (/Symfony1.2/lib/plugins/sfPropelPlugin/lib/vendor/propel-generator/classes/propel/engine/database/model/XMLElement.php) representa a los elementos de un archivo XML. Es el método loadFromXML() quien toma esos atributos
/** * This is the entry point method for loading data from XML. * It calls a setupObject() method that must be implemented by the child class. * @param array $attributes The attributes for the XML tag. */ public function loadFromXML($attributes) { $this->attributes = array_change_key_case($attributes, CASE_LOWER); $this->setupObject(); }
La misma es heredada por otras como Table y Database.
A la clase Table, debemos agregarle estas modificaciones. Es quien hereda de XMLElement e implementa IDMethod.
(/Symfony1.2/lib/plugins/sfPropelPlugin/lib/vendor/propel-generator/classes/propel/engine/database/model/Table.php
)
Un atributo $phpToString
/** * The field name for the magic method __toString(). * * @var string */ private $phpToString;
Cargamos los valores al objeto, entre ellos $this->phpToString
/** * Sets up the Rule object based on the attributes that were passed to loadFromXML(). * @see parent::loadFromXML() */ public function setupObject() { $this->name = $this->getAttribute("name"); $this->phpName = $this->getAttribute("phpName"); $this->idMethod = $this->getAttribute("idMethod", $this->getDatabase()->getDefaultIdMethod()); $this->allowPkInsert = $this->booleanValue($this->getAttribute("allowPkInsert")); // retrieves the method for converting from specified name to a PHP name. $this->phpNamingMethod = $this->getAttribute("phpNamingMethod", $this->getDatabase()->getDefaultPhpNamingMethod()); $this->skipSql = $this->booleanValue($this->getAttribute("skipSql")); $this->readOnly = $this->booleanValue($this->getAttribute("readOnly")); $this->phpToString = $this->getAttribute("phpToString"); $this->pkg = $this->getAttribute("package"); $this->abstractValue = $this->booleanValue($this->getAttribute("abstract")); $this->baseClass = $this->getAttribute("baseClass"); $this->basePeer = $this->getAttribute("basePeer"); $this->alias = $this->getAttribute("alias"); $this->heavyIndexing = ( $this->booleanValue($this->getAttribute("heavyIndexing")) || ("false" !== $this->getAttribute("heavyIndexing") && $this->getDatabase()->isHeavyIndexing() ) ); $this->description = $this->getAttribute("description"); $this->enterface = $this->getAttribute("interface"); // sic ('interface' is reserved word) $this->treeMode = $this->getAttribute("treeMode"); $this->reloadOnInsert = $this->booleanValue($this->getAttribute("reloadOnInsert")); $this->reloadOnUpdate = $this->booleanValue($this->getAttribute("reloadOnUpdate")); }
Los Getter y Setter
/** * Get the value of phpToString. * @return value of phpToString. */ public function getPhpToString() { return $this->phpToString; } /** * Set the value of phpToString. * @param v Value to assign to phpToString. */ public function setPhpToString($v) { $this->phpToString = $v; }
Y tambien debemos adaptar la clase Database
(/Symfony1.2/lib/plugins/sfPropelPlugin/lib/vendor/propel-generator/classes/propel/engine/database/model/Database.php)
Un atributo $phpToString
private $phpToString;
Cargamos los valores al objeto, entre ellos $this->phpToString
/** * Sets up the Database object based on the attributes that were passed to loadFromXML(). * @see parent::loadFromXML() */ protected function setupObject() { $this->name = $this->getAttribute("name"); $this->phpToString = $this->getAttribute("phpToString"); $this->pkg = $this->getAttribute("package"); $this->baseClass = $this->getAttribute("baseClass"); $this->basePeer = $this->getAttribute("basePeer"); $this->defaultIdMethod = $this->getAttribute("defaultIdMethod", IDMethod::NATIVE); $this->defaultPhpNamingMethod = $this->getAttribute("defaultPhpNamingMethod", NameGenerator::CONV_METHOD_UNDERSCORE); $this->defaultTranslateMethod = $this->getAttribute("defaultTranslateMethod", Validator::TRANSLATE_NONE); $this->heavyIndexing = $this->booleanValue($this->getAttribute("heavyIndexing")); }
Los Getter y Setter
/** * Get the value of phpToString. * @return value of phpToString. */ public function getPhpToString() { return $this->phpToString; } /** * Set the value of phpToString. * @param v Value to assign to phpToString. */ public function setPhpToString($v) { $this->phpToString = $v; }
Ahora el XML
/** * @see XMLElement::appendXml(DOMNode) */ public function appendXml(DOMNode $node) { $doc = ($node instanceof DOMDocument) ? $node : $node->ownerDocument; $dbNode = $node->appendChild($doc->createElement('database')); $dbNode->setAttribute('name', $this->name); if ($this->phpToString) { $dbNode->setAttribute('phpToString', $this->phpToString); } if ($this->pkg) { $dbNode->setAttribute('package', $this->pkg); } if ($this->defaultIdMethod) { $dbNode->setAttribute('defaultIdMethod', $this->defaultIdMethod); } if ($this->baseClass) { $dbNode->setAttribute('baseClass', $this->baseClass); } if ($this->basePeer) { $dbNode->setAttribute('basePeer', $this->basePeer); } if ($this->defaultPhpNamingMethod) { $dbNode->setAttribute('defaultPhpNamingMethod', $this->defaultPhpNamingMethod); } if ($this->defaultTranslateMethod) { $dbNode->setAttribute('defaultTranslateMethod', $this->defaultTranslateMethod); } /* FIXME - Before we can add support for domains in the schema, we need to have a method of the Column that indicates whether the column was mapped to a SPECIFIC domain (since Column->getDomain() will always return a Domain object) foreach ($this->domainMap as $domain) { $domain->appendXml($dbNode); } */ foreach ($this->vendorInfos as $vi) { $vi->appendXml($dbNode); } foreach ($this->tableList as $table) { $table->appendXml($dbNode); } }
Con estas dos clases modificadas, estamos en condiciones de usar estos cambios para la clase OMBuilder que hereda de DataModelBuilder. Pero esta ultima clase no sera necesario modificarla. (/Symfony1.2/lib/plugins/sfPropelPlugin/lib/vendor/propel-generator/classes/propel/engine/builder/om/OMBuilder.php). Solo hay que :
/** * Gets the field name for the magic method __toString. * @return string */ public function getPhpToString() { $phpToString = ($this->getTable()->getPhpToString() ? $this->getTable()->getPhpToString() : $this->getDatabase()->getPhpToString()); if (!$phpToString) { $phpToString = false; } return $phpToString; }
Como vimos la clase hija de la anterior OMBuilder, es PHP5ObjectBuilder.
(/Symfony1.2/lib/plugins/sfPropelPlugin/lib/vendor/propel-generator/classes/propel/engine/builder/om/php5/PHP5ObjectBuilder.php)
2 respuestas en “Generando el Modelo, a mi manera”
Muy interesante, aunque yo lo resolveria con un behavior, es un ejemplo perfecto de pizarron para comenzar a desarrollar un framework de generación de código que no permita no solo mayor personalización de nuestro código, sino tambien una mejor calidad de codigo y mayor riquesa de opciones.
Pues con un behavior, ni se me ocurre como 🙂 , eso se los dejo a los expertos.
Faltaria de extender quizas un poco las clases que gestionan las tareas
estsa pueden ser algunas…
/lib/plugins/sfPropelPlugin/lib/task/sfPropelBaseTask.class.php
/lib/plugins/sfPropelPlugin/lib/task/sfPropelBuildModelTask.class.php
/lib/plugins/sfPropelPlugin/lib/task/sfPropelGenerateAdminTask.class.php