(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