Zend FR

Consultez la FAQ sur le ZF avant de poster une question

Vous n'êtes pas identifié.

#1 05-11-2009 11:28:53

rebolon
Membre
Date d'inscription: 25-05-2009
Messages: 13

[ZF1.9][PHP5.3] Générateur de modèle et de formulaire

Bonjour,

si ça intéresse quelqu'un je travaille rapidement sur une application de génération de code pour les Zend_Form et les Models.
Dans les 2 cas, on vérifie si le fichier existe et on bloque la génération. Il est toutefois possible de forcer la génération (on écrase alors le fichier).
Actuellement l'application est testable en via un controller en spécifiant la table à partir de laquelle on va générer le code.

La génération des models est très simple puisque l'on génère un fichier avec la classe Model_NomDeMaTable qui étend une classe XXX_Db_Table_Abstract où XXX peut être Zend ou tout autre namespace.
Je ne génère pas les propriétés table ou primary qui seront de toutes façon récupérables automatiquement.
Au développeur de surcharger cette classe s'il veut générer d'autres choses.

La génération des formulaires est plus complète :
on se base sur un Model.
on créé la classe Form_NomDeMaTable qui étend unc classe XXX_Form où XXX peut être Zend ou tout autre namespace.
La méthode init est ensuite générée. Cette méthode va créer le formulaire puis ajouter tous les champs correspondants aux champs du Model. Les champs qui compose la clé primaire sont forcément hidden et non obligatoire (sinon problème pour les ajouts, le create du CRUD en fait). Les autres champs ont des validateurs ajoutés selon qu'ils sont Digits, Float, NULLABLE. Une valeur par défaut peut être ajoutée si le champ en question en dispose.

Attention, php5.3 est obligatoire car pour reconnaitre le type des champs, j'utilise la réflection sur la classe Zend_Db_Adpater_PDO_XXX afin de récupérer le tableau des types numériques de l'adapter sélectionné. Certains trouveront ça sympa, d'autres pas. Si vous avez des îdées là-dessus, je suis preneur.
Evidemment vous aurez devinez que ça ne fonctionne qu'avec les adapter PDO, tant pis pour les autres ;-)

Tout n'est pas encore testé, mais ça semble fonctionnel. Des test unitaires devraient être réalisés dans le futur, mais pour ça il faut que je vois comment sont intégrés ces tests avec Zend.

Objectif futur : se passer du controller et faire ça de manière automatique via Zend_Tool, mais là... il faut tester qu'il y a bien un adpater de configuré. Il faut ensuite permettre de sélectionner un adapter s'il en existe plusieurs. Du coup on aurait en paramètre : le nom de la table à partir de laquelle baser la génération + (facultatif) le nom d'un adapter

Mon factory et ma classe abstraite :

class Rebolon_Generator
{   
   const CLASS_NAME = 'Rebolon_Generator' ;
   
   protected function __construct(){}
   
   static public function build( $type, $params = null )
   {
       if( $params instanceof Zend_Config )
         $params = $params->toArray() ;
       
       switch( $type = strtolower( $type ) )
       {
          case 'form':
          case 'model':
             break ;
          default :
             throw new Rebolon_Generator_Exception( 'Type unknown' ) ;
       }
       
       $className = self::CLASS_NAME.'_'.ucfirst( $type ) ;
       return  new $className( $params ) ;
   }
}

abstract class Rebolon_Generator_Abstract

   const P_SAVEPATH = 'path' ;
   const P_NAMESPACE = 'namespace' ;
   const P_FORCE = 'force' ;
   
   protected $savePath ;
   protected $namespace = 'Zend' ;
   protected $forceCompile = false ;
   protected $className ;
   protected $name ;
   
   public function __construct( $config /* $savePath, $namespace = 'Zend', $forceCompile = false */ )
   {
      if (!is_array($config))
      {
         $config = array( self::P_SAVEPATH=>$config );
      }

      $this->setOptions( $config ) ;
   }
   
   protected function setOptions( $options )
   {
      foreach( $options as $key => $value )
      {
         switch( $key )
         {
            case 0 :
            case self::P_SAVEPATH :
               $this->setSavePath( $value ) ;
               break;
            case 1 :
            case self::P_NAMESPACE :
                 $this->setNamespace( $value ) ;
                 break;
            case 2 :
            case self::P_FORCE:
                 $this->setForceCompile( $value ) ;
                 break;
            default:
                 // ignore unrecognized configuration directive
                 break;
            }
        }
   }
   
   static protected function getClassName( $name, $prefix = null )
   {
      return strpos( $name, '_' ) === (strlen( $name )-1) ? $name : $prefix .'_'.$name ;
   }
   
   protected function setClassName( $name, $prefix = null )
   {
      $className = self::getClassName( $name, $prefix ) ;
      $this->className = $className ;
      $this->setName( $className ) ;
      return $this ;
   }
   
   private function setName( $name )
   {
      if( $name == self::getClassName( $name, '-' ) )
         $name = ucfirst( $name ) ;
      else
      {
         $a = explode( '_', $name ) ;
         
         if( count($a) > 1 )
            array_shift( $a ) ;
           
         foreach( $a as & $v )
            $v = ucfirst( $v ) ;
         $name = implode( '_', $a ) ;
      }
     
      $this->name = $name ;
     
      return $this ;
   }
   
   protected function getFullPath( $className )
   {
      // Supprime le prefix Form ou Model avant de construire le chemin
      $class = explode( '_', $className ) ;
      array_shift( $class ) ;
      $className = implode( '_', $class ) ;
     
      return $this->savePath.DIRECTORY_SEPARATOR.$className.'.php' ;
   }
   
   protected function canSave()
   {
      if( $this->className == null )
         throw Rebolon_Generator_Exception( 'The class name has not been initialized' ) ;

      if( !$this->forceCompile && is_file( $this->getFullPath( $this->className ) ) )
         return false ;
         
      return true ;
   }
   
   protected function save()
   {
      if( $this->canSave() )
         return file_put_contents( $this->getFullPath( $this->className ), '<?php'.chr(10).$this->code ) ;

      return false ;
   }
   
   protected function setSavePath( $savePath )
   {
      if( !is_dir( $savePath ) )
         throw new Rebolon_Generator_Exception( 'Directory does not exists ('.$savePath.')' ) ;
      if( !is_writeable( $savePath ) )
         throw new Rebolon_Generator_Exception( 'Directory is not writeable ('.$savePath.')' ) ;
     
      $this->savePath = $savePath ;
     
      return $this ;
   }
   
   protected function setNamespace( $namespace )
   {
      $namespace = strpos( $namespace, '_' ) === (strlen( $namespace )-1) ? $namespace : $namespace.'_' ;
      if( !in_array( $namespace, Zend_Loader_Autoloader::getInstance()->getRegisteredNamespaces() ) )
         throw new Rebolon_Generator_Exception( 'Namespace is not registered ('.substr($namespace, 0, (strlen( $namespace )-1) ).')' ) ;
     
      $this->namespace = $namespace ;
     
      return $this ;
   }
   
   protected function setForceCompile( $force )
   {
      $this->forceCompile = (bool) $force ;
     
      return $this ;
   }
   
   public function __get( $propName )
   {
      if( isset( $this->$propName ) )
         return $this->$propName ;
         
      throw new Rebolon_Generator_Exception( 'Unkown property' ) ;
   }
}

Le générateur de Formulaire :

class Rebolon_Generator_Form extends Rebolon_Generator_Abstract

   const CLASS_PREFIX = 'Form' ;
   const CLASS_EXTENDS = 'Form' ;
   const INIT_METHOD = 'init' ;
   
   protected $code ;
   
   public function __construct( $config )
   {
      parent::__construct( $config ) ;
   }
   
   public function getForm( $form )
   {
      try
      {
         return new $form ;
      }
      catch( Rebolon_Generator_Exception $rge )
      {
         $rge->setMessage( $rge->getMessage().' (Impossible to instanciate the form '.$form.') ' ) ;
         throw $rge ;
      }
   }
   
   public function build( $modelName )
   {
      $m = Rebolon_Generator_Model::getModel( $modelName ) ;
      $t = Rebolon_Generator_Model::describeTableFromModel( $m ) ;
     
      $this ->setClassName( $m->info( Zend_Db_Table_Abstract::NAME ), self::CLASS_PREFIX )
            ->createClass( $m )
            ->addSingleton()
            ->addInit( $t )
            ->save() ;
   }
   
   /*
    * @TODO : gérer la possibilité de forcer la compilation du script => écrasement de fichier
    */
   protected function createClass( Zend_Db_Table_Abstract $m )
   {
      if( $this->className == null )
         throw new Rebolon_Generator_Exception( 'The class name has not been initialized' ) ;
     
      if( !$this->canSave() )
         throw new Rebolon_Generator_Exception( 'Class already exists in specified folder('.$this->className.')' ) ;

      $c = new Zend_CodeGenerator_Php_Class();
      $c->setName( $this->className ) ;
      $c->setExtendedClass( $this->namespace.self::CLASS_EXTENDS ) ;
     
      $this->code = $c ;
     
      return $this ;
   }
   
   protected function addInit( $fields = null )
   {
      $this->code->setMethods(
            array(
        // Method passed as array
              array(
                  'name'       => self::INIT_METHOD,
                  'parameters' => array(),
                  'body'       => ( $this->namespace != 'Zend' ? 'parent::Init();'.chr(10) : null ) ,
                  'docblock'   => new Zend_CodeGenerator_Php_Docblock(
                                    array (
                                       'longDescription' => 'init() is the initialization routine called when Zend_Form objects are
created. In most cases, it make alot of sense to put definitions in this
method, as you can see below.  This is not required, but suggested. 
There might exist other application scenarios where one might want to
configure their form objects in a different way, those are best
described in the manual:

@see    http://framework.zend.com/manual/en/zend.form.html
@return void',
                                    )
                                 ),
              ),
           )
        )  ;
           
      // ajout des champs si besoin
      if( !is_null( $fields ) )
         $this->completeInit( $fields ) ;
         
      // création du formulaire de base (après la création des champs car permet d'avoir le submit à la fin
      $this->addForm() ;
     
      return $this ; 
   }
   
   protected function getFormName()
   {
      return self::CLASS_PREFIX.$this->name ;
   }
   
   protected function getItemName( $name )
   {
      return $this->getFormName().'_'.$name ;
   }
   
   protected function addForm()
   {
      $b = $this->code->getMethod( self::INIT_METHOD )->getBody() ;
      $b .= '$this->setMethod("post")'.chr(10)
            .chr(9).'->setAttrib("id", "'.$this->getFormName().'")'.chr(10)
            .chr(9).'->addElement( "submit", "submit", array( "label"=>"Envoyer", ) ) ; '.chr(10) ;
      $this->code->getMethod( self::INIT_METHOD )->setBody( $b ) ; 
     
      return $this ;
   }
   
   protected function completeInit( $fields )
   {
      $b = $this->code->getMethod( self::INIT_METHOD )->getBody() ;
     
      foreach( $fields as $key=>$field )
      {
         // Création de l'élément selon un check sur Primaire
         // permet de passer à hidden ou à text (avec label dans ce cas)
         if( $field['PRIMARY'] === true )
         {
            $b .= '$item'.$field['COLUMN_NAME'].' = new Zend_Form_Element_Hidden( "'.$this->getItemName( $field['COLUMN_NAME'] ).'" ) ;' ;
         }
         else
         {
            $b .= '$item'.$field['COLUMN_NAME'].' = new Zend_Form_Element_Text( "'.$this->getItemName( $field['COLUMN_NAME'] ).'" ) ;' ;
            $b .= chr(9).'$item'.$field['COLUMN_NAME'].'->setLabel("'.ucfirst(strtolower($field['COLUMN_NAME'])).'") ; ' ;
         }
         $b.= chr(10);
                             
         // Faire un check de type isNumeric depuis le tableau Zend_Db_Adapter_Pdo_XXX->$_numericDataTypes
         // permet de mettre un validateur de type numérique
         if( Rebolon_Db_Adapter_Abstract::isNumeric( Zend_Db_Table_Abstract::getDefaultAdapter(), $field ) )
         {
            $b .= chr(9).'$item'.$field['COLUMN_NAME'].'->addValidator( "Digits" ) ; ' ;
         }
         elseif( Rebolon_Db_Adapter_Abstract::isDecimal( Zend_Db_Table_Abstract::getDefaultAdapter(), $field ) )
         {
            $b .= chr(9).'$item'.$field['COLUMN_NAME'].'->addValidator( "Float" ) ; ' ;
         }
         $b.= chr(10);
         
         // Positionner la valeur par défaut si DEFAULT !== NULLABLE
         if( $field['DEFAULT'] !== null )
         {
            $b .= chr(9).'$item'.$field['COLUMN_NAME'].'->setDefault( "'.$field['DEFAULT'].'" ) ; ' ;
         }
         $b.= chr(10);
         
         // Mettre une limite de chaine via LENGTH
         if( $field['NULLABLE'] === false )
         {
            // On ne met à required que pour les champs != PRIMARY => sinon problème lors de l'ajout
            if( $field['PRIMARY'] === false )
               $b .= chr(9).'$item'.$field['COLUMN_NAME'].'->setRequired( true ) ; ' ;
           
            // permet de mettre un validateur sur stringLenght > 0
            if( $field['LENGTH'] === null )
            {
               $b .= chr(9).'$item'.$field['COLUMN_NAME'].'->addValidator( "StringLength", false, array( 1, ) ) ; ' ;
            }
         }
         // permet de mettre un validateur sur stringlenght > LENGTH
         elseif( $field['LENGTH'] !== null )
         {
            $b .= chr(9).'$item'.$field['COLUMN_NAME'].'->addValidator( "StringLength", false, array( '.$field['LENGTH'].', ) ) ; ' ;
         }
         
         // That's all
         $b .= chr(10).chr(9).'$this->addElement( $item'.$field['COLUMN_NAME'].' ) ; '.chr(10) ;
      }
           
      $this->code->getMethod( self::INIT_METHOD )->setBody( $b ) ;
     
      return $this ;
   }
   
   protected function addSingleton()
   {
      $this->code->setProperties(
            array
            (
               array(
                  'name'         => '_instance',
                  'visibility'   => 'protected',
               ),
            )
         )
                ->setMethods(
            array(
        // Method passed as array
              array(
                  'name'       => 'getInstance',
                  'parameters' => array(),
                  'body'       => 'if( self::$_instance instanceof Form_Collection )'.chr(10).
                                  chr(9).'return new self() ;'. chr(10).   
                                  chr(10).
                                  'self::$_instance = new self() ;'.chr(10).
                                  'return self::$_instance ;' ,
                  'docblock'   => new Zend_CodeGenerator_Php_Docblock(
                                    array (
                                       'shortDescription' => 'Only for chaining',
                                    )
                                 ),
              ),
           )
                        )  ;
     
      return $this ; 
   }
   
}

Le générateur de modèle

class Rebolon_Generator_Model extends Rebolon_Generator_Abstract

   const CLASS_PREFIX = 'Model' ;
   const CLASS_EXTENDS = 'Db_Table_Abstract' ;
   
   protected $code ;
   
   public function __construct( $config )
   {
      parent::__construct( $config ) ;
   }
   
   /*
    * @name : getModel
    * @params : string modelName (name of a model which extends Zend_Db_Table_Abstract) ex: Model_Person or Person
    * @return : Zend_Db_Table_Abstract
    */
   static public function getModel( $modelName )
   {
      try
      {
         $className = self::getClassName( $modelName, self::CLASS_PREFIX ) ;
         $o = new $className ;
         
         if( !($o instanceof Zend_Db_Table_Abstract ) )
            throw new Rebolon_Generator_Exception( 'Model specified is not the name of a Zend_Db_Table_Abstract class' ) ;

         if( !( $o->getAdapter() instanceof Zend_Db_Adapter_Pdo_Abstract ) )
            throw new Rebolon_Generator_Exception( 'This generator works only with PDO adapter' ) ;
           
         return $o ;
      }
      catch( Rebolon_Generator_Exception $rge )
      {
         $rge->setMessage( $rge->getMessage().' (Impossible to instanciate the model '.$modelName.') ' ) ;
         throw $rge ;
      }
   }
   
   /*
    * @name : describeTable
    * @params : string modelName (name of a model which extends Zend_Db_Table_Abstract)
    * @return : array
    */
   static public function describeTable( $modelName )
   {
      $m = self::getModel( $modelName ) ;
      return self::describeTableFromModel( $m ) ;
   }
   
   /*
    * @name : describeTableFromModel
    * @params : Zend_Db_Table_Abstract m
    * @return : array
    */
   static public function describeTableFromModel( Zend_Db_Table_Abstract $m )
   {
      return $m->getAdapter()->describeTable( $m->info( Zend_Db_Table_Abstract::NAME ) ) ;
   }
   
   public function build( $tableName )
   { 
      $this ->setClassName( $tableName, self::CLASS_PREFIX )
            ->createClass()
            ->save() ;
   }
   
   /*
    * @TODO : gérer la possibilité de forcer la compilation du script => écrasement de fichier
    */
   protected function createClass()
   { 
      if( $this->className == null )
         throw new Rebolon_Generator_Exception( 'The class name has not been initialized' ) ;
     
      if( !$this->canSave() )
         throw new Rebolon_Generator_Exception( 'Class already exists in specified folder('.$this->className.')' ) ;

      $c = new Zend_CodeGenerator_Php_Class();
      $c->setName( $this->className ) ;
      $c->setExtendedClass( $this->namespace.self::CLASS_EXTENDS ) ;
     
      $this->code = $c ;
     
      return $this ;
   }
}

Test dans votre controller :

$gm = Rebolon_Generator::build( 'Model', array( APPLICATION_PATH.'/models/', 'Rebolon' ) ) ;
$gm->build( 'Person' ) ;
$gf = Rebolon_Generator::build( 'Form', array( APPLICATION_PATH.'/forms/test/', 'Rebolon' ) ) ;
$gf->build( 'Person' ) ;

Controllez le code généré et donnez-moi votre avis.
Totue critique est acceptée :-)

Dernière modification par rebolon (05-11-2009 15:27:08)

Hors ligne

 

#2 09-11-2009 06:58:07

lesauf
Membre
Lieu: Yaoundé - Cameroun
Date d'inscription: 29-11-2007
Messages: 52
Site web

Re: [ZF1.9][PHP5.3] Générateur de modèle et de formulaire

Bonjour,

J'ai l'impression que tu ne respecte pas les coding standards de Zend.
Jette un oeil de ce coté de la doc. A mon avis ca devrait rendre ton code plus attrayant.

A plus,
Lesauf

Hors ligne

 

#3 09-11-2009 09:34:28

rebolon
Membre
Date d'inscription: 25-05-2009
Messages: 13

Re: [ZF1.9][PHP5.3] Générateur de modèle et de formulaire

Je vais y jeter un coup d'oeil de suite.
Thx

Hors ligne

 

#4 09-11-2009 09:57:17

rebolon
Membre
Date d'inscription: 25-05-2009
Messages: 13

Re: [ZF1.9][PHP5.3] Générateur de modèle et de formulaire

Je viens de me rendre compte qu'il existe un module 'Zenerator' développé par Gabriel Malkas qui permet déjà de générer ce type de code. Je vais regarder ça de plus prêt et voir ce que je fais. Mais comme j'aime pas réinventer la roue (même si apparemment c'est déjà fais en partie) je vais surtout voir ce que je peux apporter de plus à Zenerator.

Hors ligne

 

#5 06-01-2010 14:45:18

elkolonel
Administrateur
Lieu: Grasse
Date d'inscription: 18-12-2007
Messages: 299
Site web

Re: [ZF1.9][PHP5.3] Générateur de modèle et de formulaire

Hello,

finalement as tu pu contribuer au projet Zenerator ??

Cordialement,
Fred

Hors ligne

 

Pied de page des forums

Propulsé par PunBB
© Copyright 2002–2005 Rickard Andersson
Traduction par punbb.fr

Graphisme réalisé par l'agence Rodolphe Eveilleau
Développement par Kitpages