lundi 30 janvier 2012

Zend Framework : librairie de gestion de données, à partir d'un formulaire vers une base de données, avec traitement des fichiers

Voici un ensemble de classes permettant de gérer facilement l'insertion et la modification, en base de données et en répertoires, les valeurs d'un formulaire contenant n'importe quel type d'éléments, y compris des fichiers, et d'appliquer des traitements sur les fichiers uploadés.

Cette petite librairie permet de :
  • Insérer ou modifier des éléments divers (varchar, text...) en les filtrant éventuellement,
  • Insérer ou supprimer des fichiers dans des répertoires choisis,
  • Renommer éventuellement les fichiers uploadés, aléatoirement ou selon un nom défini,
  • Redimensionner les images en plusieurs tailles (par exemple vignettes, grandes, plein écran...),
  • Rogner éventuellement des images,
  • Changer éventuellement les clés des valeurs du formulaire, si elles ne correspondent pas aux noms de lignes de la table de db.
Nous utiliseront la librairie PHP Thumbnailer adaptée pour le Zend Framework, que vous pouvez trouver dans un petit tuto ici

Le code source de cette librairie est disponible sur Github.

Commençons par les éléments que nous utliserons :

Une petite classe permettant de générer un nom aléatoire, à placer dans library/Web82/Generate/Random.php :
class Web82_Generate_Random
{

        /**
         * 
         * @param integer $length
         */
        public function generate ( $length )
        {

                $random = '' ;
                $chaine = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ0123456789' ;

                srand ( ( double ) microtime () * 1000000 ) ;

                for ( $i = 0 ; $i < $length ; $i ++  ) {

                        $random .= $chaine [ rand () % strlen ( $chaine ) ] ;
                }
                return $random ;

        }

}
Une classe qui étend Zend_Form_Element_File en y ajoutant deux petites méthodes. Les éléments File du formulaire traité devront être des instances de cette classe. À placer dans library/Web82/Form/Element/File.php.
class Web82_Form_Element_File
    extends Zend_Form_Element_File
{

        /**
         * renomme le fichier
         * $name : le nom que l'on veut donner au fichier. S'il n'est pas spécifié, un nom aléatoire sera donné
         * $forceExtension : si l'on veut forcer l'extension du fichier (peu conseillé !)
         *
         * @param string|null $name
         * @param string|null $forceExtension
         * @return string
         */
        public function setNewName ( $name = NULL , $forceExtension = NULL )
        {
                /**
                 * on récupère l'extension du fichier, ou on lui assigne l'extension forcée
                 */
                if ( is_null ( $forceExtension ) ) {

                        $fileName = $this -> getFileName ( NULL , FALSE ) ;

                        $explodedName = explode ( '.' , $fileName ) ;

                        $extension = $explodedName [ count ( $explodedName ) - 1 ] ;
                } else {

                        $extension = $forceExtension ;
                }

                /**
                 * on génère le nom, ou on prend celui donné
                 */
                if ( is_null ( $name ) ) {

                        /**
                         * ici on utilise la classe précédente
                         */
                        $makeName = new Web82_Generate_Random ( ) ;

                        $newName = $makeName -> generate ( 10 ) . '.' . $extension ;
                } else {

                        $newName = $name . '.' . $extension ;
                }

                return $newName ;

        }

        /**
         *
         * @return string
         */
        public function getExtension ()
        {
                $fileName = $this -> getFileName ( NULL , FALSE ) ;

                $explodedName = explode ( '.' , $fileName ) ;

                $extension = $explodedName [ count ( $explodedName ) - 1 ] ;

                return $extension ;

        }

}
Une classe avec une méthode statique permettant de rogner une image suivant une proportion largeur/hauteur. Cela permet, par exemple, d'avoir des vignettes toutes de la même taille sur un catalogue, en supprimant des parties en haut et en bas, ou à gauche et à droite. Elle est placée dans library/Web/Image/Tools.php (Je posterai plus tard une librairie plus complète de traitements d'images).
class Web82_Image_Tools
{

        /**
         * $source : l'image source
         * $destination : la destination désirée
         * $proportionWidthOnHeight : la proportion largeur/hauteur souhaitée
         * $extension :  l'extension du fichier
         * 
         * @param resource $source
         * @param string $destination
         * @param integer $proportionWidthOnHeight
         * @param string $extension
         */
        static public function cropImage ( $source , $destination , $proportionWidthOnHeight , $extension )
        {
                /**
                 * on récupère la taille de l'image source
                 */
                $size = getimagesize ( $source ) ;

                $actualWidth  = $size[ 0 ] ;
                $actualHeight = $size[ 1 ] ;

                /**
                 * on détermine ensuite la taille de l'image de destination, 
                 */
                /**
                 * cas d'une image source en paysage
                 */
                if ( $actualWidth >= $actualHeight * $proportionWidthOnHeight ) {

                        $newWidth  = $actualHeight * $proportionWidthOnHeight ;
                        $newHeight = $actualHeight ;

                        $startX = ($actualWidth - $newWidth) / 2 ;
                        $startY = 0 ;
                }

                /**
                 * cas d'une image source en portrait
                 */ else {

                        $newWidth  = $actualWidth ;
                        $newHeight = $actualWidth / $proportionWidthOnHeight ;

                        $startX = 0 ;
                        $startY = ($actualHeight - $newHeight) / 2 ;
                }

                /**
                 * on crée l'image de destination
                 */
                $newImage = imagecreatetruecolor ( $newWidth , $newHeight ) ;

                /**
                 * puis la resource image à partir de la source
                 */
                switch ( $extension ) {
                        case 'jpg':
                        case 'jpeg':
                        case 'JPG':
                        case 'JPEG':
                                $sourceImage = imagecreatefromjpeg ( $source ) ;
                                break ;
                        case 'gif':
                        case 'GIF':
                                $sourceImage = imagecreatefromgif ( $source ) ;
                                break ;
                        case 'png':
                        case 'PNG':
                                $sourceImage = imagecreatefrompng ( $source ) ;
                                break ;
                }

                /**
                 * puis on copie la partie de l'image source dans celle de destination
                 */
                imagecopy ( $newImage , $sourceImage , 0 , 0 , $startX , $startY , $newWidth , $newHeight ) ;

                /**
                 * et on enregistre l'image
                 */
                switch ( $extension ) {
                        case 'jpg':
                        case 'jpeg':
                        case 'JPG':
                        case 'JPEG':
                                imagejpeg ( $newImage , $destination ) ;
                                break ;
                        case 'gif':
                        case 'GIF':
                                imagegif ( $newImage , $destination ) ;
                                break ;
                        case 'png':
                        case 'PNG':
                                imagepng ( $newImage , $destination ) ;
                                break ;
                }

                /**
                 * on détruit les deux resources
                 */
                imagedestroy ( $newImage ) ;
                imagedestroy ( $sourceImage ) ;

        }

}
Avant les classes de traitement proprement dites, voici le nerf de la guerre, le fichier que vous aurez à adapter suivant vos besoins, un fichier INI qui va gérer les traitements des fichiers de notre formulaire. Suivez bien les commentaires.
; fichier "files.ini" à placer dans application/configs

; supposons que le nom de l'élément image soit "image" (ce qui est bien parlant, ma foi...)

; ici le répertoire temporaire d'upload
file.image.tmpPath = APPLICATION_PATH "/../public/images/upload/tmp"

; veut-on renommer le fichier ? oui: 1, non: 0
file.image.rename.ok = 1
; si l'on veut donner un nom précis au fichier, le spécifier ici
; si l'on veut donner un nom aléatoire, ne pas mettre cette ligne
file.image.rename.name = "nomQueLonVeutDonner"
; veut-on en forcer l'extension (fort peu conseillé !) ? oui: 1, non: 0
file.image.rename.forceExtension = 0

; on a ici le choix de directement enregistrer le fichier, sans lui appliquer aucun traitement
; dans ce cas, décommenter cette ligne et donner le répertoire désiré :

; file.image.moveToPath = APPLICATION_PATH "/../public/repertoire/ou/on/place/le/fichier"

; veut-on redimensionner ? oui: 1, non: 0
file.image.resize = 1

; on peut opérer plusieurs redimensionnements (clé "version"), aboutissant à plusieurs images
; à placer dans des répertoires différents, car elles auront le même nom

; le répertoire de destination
file.image.versions.thumb.path = APPLICATION_PATH "/../public/images/upload/thumb"
; la largeur désirée
file.image.versions.thumb.size.width = 120
; la hauteur
file.image.versions.thumb.size.height = 60
; veut-on rogner l'image ? oui: 1, non, 0
file.image.versions.thumb.crop.ok = 1
; dans ce cas, on spécifie la proportion largeur/hauteur
file.image.versions.thumb.crop.proportionWidthOnHeight = 2

; un autre version, sans rognage...
file.image.versions.diapo.path = APPLICATION_PATH "/../public/images/upload/diapo"
file.image.versions.diapo.size.width = 1900
file.image.versions.diapo.size.height = 369
file.image.versions.diapo.crop.ok = 0

; et une dernière, pour laquelle on va récupérer les dimensions pour les ajouter en base de donnée
file.image.versions.fullScreen.path = APPLICATION_PATH "/../public/images/upload/fullScreen"
file.image.versions.fullScreen.size.width = 1900
file.image.versions.fullScreen.size.height = 1000
file.image.versions.fullScreen.crop.ok = 0
; 1 pour récupérer les dimensions
file.image.versions.fullScreen.getSize.ok = 1
; ici le nom du champ de la db pour la largeur...
file.image.versions.fullScreen.getSize.widthField = "width"
; et celui pour la hauteur
file.image.versions.fullScreen.getSize.heightField = "height"


; une vidéo uploadée, dont le champ s'appelle "vidéo" (original...)
file.video.tmpPath = APPLICATION_PATH "/../public/images/upload/tmp"

; on la renomme...
file.video.rename.ok = 1
file.video.rename.forceExtension = 0

; mais on ne la redimensionne pas, bien sûr ! Donc, directement  moveToPath
file.video.moveToPath = APPLICATION_PATH "/../public/images/upload/video"


; et hop, un PDF
file.pdf.tmpPath = APPLICATION_PATH "/../public/pdf/tmp"

file.pdf.rename.ok = 1
file.pdf.rename.name = "paf"
file.pdf.rename.forceExtension = 0

file.pdf.moveToPath = APPLICATION_PATH "/../public/pdf"

Voici enfin les deux classes qui gèrent tous les traitements : D'abord, la classe que l'on appellera dans le contrôleur : library/Web82/Content/Insert.php
class Web82_Content_Insert
{

        /**
         *
         * @var Zend_Form
         */
        public $form ;

        /**
         *
         * @var Zend_Db_Table_Abstract
         */
        public $table ;

        /**
         *
         * @var array
         */
        public $values ;

        /**
         *
         * @var Zend_Filter
         */
        public $filter ;

        /**
         *
         * @var Zend_Config
         */
        public $config ;

        /**
         * 
         * $form : le formulaire peuplé des valeurs
         * $table : la table de la base, instanciée de Zend_Db_Table_Abstract
         * $filtre : l'éventuel filtre
         * $config : la config
         * 
         * @param Zend_Form $form
         * @param Zend_Db_Table_Abstract $table
         * @param Zend_Filter|null $filter
         * @param Zend_Config|null $config // en cas de files
         */
        public function __construct ( Zend_Form $form , Zend_Db_Table_Abstract $table , $filter = NULL , Zend_Config $config = NULL )
        {
                /**
                 * voir plus bas tous les setters
                 */
                $this -> setForm ( $form ) ;
                $this -> setTable ( $table ) ;
                $this -> setValues () ;

                if ( ! is_null ( $filter ) ) {

                        $this -> setFilter ( $filter ) ;
                }

                if ( ! is_null ( $config ) ) {

                        $this -> setConfig ( $config ) ;
                }

        }

        /**
         * $action : l'action désirée, "insert" ou "update"
         * $id : le nom du champ clé primaire en db
         * $valueId : le nom du champ clé primaire dans le formulaire
         * $keyModifier : le tableau de changement des clés
         * 
         * @param string $action
         * @param string $id
         * @param string $valueId
         * @param array $keyModifier
         * @return boolean 
         */
        public function insert ( $action , $id = 'id' , $valueId = 'id' , $keyModifier = NULL )
        {
                /**
                 * le form n'est pas valide
                 */
                if ( ! $this -> form -> isValid ( $this -> values ) ) {

                        return FALSE ;
                }

                /**
                 * il l'est
                 */ else {

                        /**
                         * on vérifie s'il y a des files
                         */
                        $fileElements = array ( ) ;

                        foreach ( $this -> form -> getElements () as $formElement ) {

                                if ( $formElement instanceof Zend_Form_Element_File && ! is_null ( $formElement -> getValue () ) ) {

                                        $fileElements[ ] = $formElement ;
                                }
                        }

                        /**
                         * si le tableau est rempli, c'est qu'il y a des files
                         */
                        if ( count ( $fileElements ) > 0 ) {

                                /**
                                 * alors on appelle la classe Upload
                                 */
                                $upload = new Web82_Content_File_Upload ( $this -> form , $this -> table , $this -> filter , $this -> config ) ;
                                $this -> values = $upload -> upload () ;
                        }

                        /**
                         * on change évetuellement les clés des valeurs
                         * avec un tableau ayant pour clés les anciennes clés
                         * et pour valeurs les clés désirées
                         */
                        if ( ! is_null ( $keyModifier ) ) {

                                $valuesArray = $this -> values ;

                                $this -> values = array ( ) ;

                                foreach ( $keyModifier as $oldKey => $newKey ) {

                                        $this -> values[ $newKey ] = $valuesArray[ $oldKey ] ;
                                }
                        }

                        /**
                         * on filtre éventuellement les données
                         * avec un filtre implémentant Zend_Filter_Interface
                         */
                        if ( ! is_null ( $this -> filter ) ) {

                                $this -> values = $this -> filter -> filter ( $this -> values ) ;
                        }

                        /**
                         * on balance en db
                         */
                        if ( $action == 'insert' ) {

                                $rowId = $this -> table -> insert ( $this -> values ) ;

                                $this -> values[ $id ] = $rowId ;
                        } elseif ( $action == 'update' ) {

                                $this -> table -> update ( $this -> values , $this -> table -> getAdapter () -> quoteInto ( $id . '=?' , $this -> values[ $valueId ] ) ) ;
                        }

                        /**
                         * et on retourne les valeurs finales
                         */
                        return $this -> values ;
                }

        }

        /**
         *
         * @param Zend_Form $form 
         */
        public function setForm ( Zend_Form $form )
        {

                try {

                        if ( $form instanceof Zend_Form ) {

                                $this -> form = $form ;
                        }
                }
                catch ( Zend_Exception $exc ) {

                        $log = Zend_Registry::get ( 'logger' ) ;
                        $log -> err ( $exc -> getTraceAsString () ) ;
                }

        }

        /**
         *
         * @param Zend_Form $form 
         */
        public function setTable ( Zend_Db_Table_Abstract $table )
        {

                try {

                        if ( $table instanceof Zend_Db_Table_Abstract ) {

                                $this -> table = $table ;
                        }
                }
                catch ( Zend_Exception $exc ) {

                        $log = Zend_Registry::get ( 'logger' ) ;
                        $log -> err ( $exc -> getTraceAsString () ) ;
                }

        }

        /**
         *
         * @param array
         */
        public function setValues ()
        {

                $this -> values = $this -> form -> getValues () ;

        }

        /**
         *
         * @param Zend_Filter $form 
         */
        public function setFilter ( $filter )
        {

                $this -> filter = $filter ;

        }

        /**
         *
         * @param Zend_Config $config 
         */
        public function setConfig ( Zend_Config $config )
        {
                try {

                        if ( $config instanceof Zend_Config ) {

                                $this -> config = $config ;
                        }
                }
                catch ( Zend_Exception $exc ) {

                        $log = Zend_Registry::get ( 'logger' ) ;
                        $log -> err ( $exc -> getTraceAsString () ) ;
                }

        }

}
Puis la classe qui va traiter les fichiers : library/Web82/Content/Files/Upload.php :
class Web82_Content_File_Upload
    extends Web82_Content_Insert
{

        /**
         *
         * @return array
         */
        public function upload ()
        {
                $values = $this -> form -> getValues () ;

                /**
                 * on itère les files
                 */
                foreach ( $this -> config -> file as $element => $file ) {

                        /**
                         * le champ du formulaire n'a pas été rempli, on ne le traite pas
                         */
                        if ( ! $this -> form -> {$element} -> isUploaded () ) {

                                unset ( $values[ $element ] ) ;
                        }

                        /**
                         * il l'a été
                         */ else {

                                /**
                                 * le fichier n'a pas été correctement reçu
                                 */
                                if ( ! $this -> form -> {$element} -> receive () ) {

                                        throw new Zend_Exception ( 'Le fichier n\'a pas été reçu' ) ;
                                }

                                /**
                                 * il l'a été
                                 */ else {

                                        /**
                                         * on choppe le nom et l'extension de l'image
                                         */
                                        $tmpName   = $this -> form -> {$element} -> getFileName ( NULL , FALSE ) ;
                                        $extension = $this -> form -> {$element} -> getExtension () ;

                                        /**
                                         * on renomme ?
                                         */
                                        if ( $file -> rename -> ok == 1 ) {

                                                if ( ! is_string ( $file -> rename -> name ) ) {

                                                        $renameName = NULL ;
                                                } else {

                                                        $renameName = $file -> rename -> name ;
                                                }

                                                if ( $file -> rename -> forceExtension == 0 ) {

                                                        $renameForceExtension = NULL ;
                                                } else {

                                                        $renameForceExtension = $file -> rename -> forceExtension ;
                                                }

                                                $newName = $this -> form -> {$element} -> setNewName ( $renameName , $renameForceExtension ) ;
                                        } else {

                                                $newName = $tmpName ;
                                        }

                                        /**
                                         * le path tmp
                                         */
                                        $tmpPath = $file -> tmpPath ;

                                        /**
                                         * on redimensionne ?
                                         */
                                        if ( $file -> resize == 1 ) {

                                                /**
                                                 * on itère les versions
                                                 */
                                                foreach ( $file -> versions as $version ) {

                                                        /**
                                                         * on rogne ?
                                                         */
                                                        if ( $version -> crop -> ok == 1 ) {

                                                                $croppedName = 'crop_' . $tmpName ;

                                                                try {

                                                                        Web82_Image_Tools::cropImage (
                                                                            $tmpPath . '/' . $tmpName , $tmpPath . '/' . $croppedName , $version -> crop -> proportionWidthOnHeight , $extension ) ;
                                                                }
                                                                catch ( Zend_Exception $exc ) {

                                                                        /**
                                                                         * si on a défini un logger dans le Bootstrap, sinon, on fait ce qu'on veut...
                                                                         */
                                                                        $log = Zend_Registry::get ( 'logger' ) ;
                                                                        $log -> err ( $exc -> getTraceAsString () ) ;
                                                                }

                                                                $tmpNameToInsert = $croppedName ;
                                                        } else {

                                                                $tmpNameToInsert = $tmpName ;
                                                        }

                                                        /**
                                                         * on resize avec Thumbnailer, et on enregistre
                                                         */
                                                        try {

                                                                Thumbnailer_CreateThumb::createThumbnail (
                                                                    $tmpPath . '/' . $tmpNameToInsert , $version -> path . '/' . $newName , $version -> size -> width , $version -> size -> height ) ;
                                                        }
                                                        catch ( Zend_Exception $exc ) {

                                                                /**
                                                                 * si on a défini un logger dans le Bootstrap, sinon, on fait ce qu'on veut...
                                                                 */
                                                                $log = Zend_Registry::get ( 'logger' ) ;
                                                                $log -> err ( $exc -> getTraceAsString () ) ;
                                                        }

                                                        /**
                                                         * doit-on récupérer les dimensions pour les insérer en db ?
                                                         */
                                                        if ( $version -> getSize -> ok == 1 ) {

                                                                $dimensions = getimagesize ( $version -> path . '/' . $newName ) ;

                                                                $values[ $version -> getSize -> widthField ]  = $dimensions[ 0 ] ;
                                                                $values[ $version -> getSize -> heightField ] = $dimensions[ 1 ] ;
                                                        }
                                                }
                                        }

                                        /**
                                         * on ne redimensionne pas, on copie juste
                                         */ else {

                                                copy ( $tmpPath . '/' . $tmpName , $file -> moveToPath . '/' . $newName ) ;
                                        }

                                        /**
                                         * on supprime les images temporaires
                                         */
                                        if ( is_file ( $tmpPath . '/' . $tmpName ) ) {

                                                unlink ( $tmpPath . '/' . $tmpName ) ;
                                        }

                                        if ( is_file ( $tmpPath . '/' . $croppedName ) ) {

                                                unlink ( $tmpPath . '/' . $croppedName ) ;
                                        }

                                        /**
                                         * on assigne le nom de l'élément aux valeurs à insérer en db
                                         */
                                        $values[ $element ] = $newName ;
                                }
                        }
                }

                /**
                 * et on retourne les valeurs
                 */
                return $values ;

        }

}
Et voilà ! On va pouvoir appeler la classe Insert de notre contrôleur de cette manière :
/**
 * on instancie la config
 */
$config   = new Zend_Config_Ini ( APPLICATION_PATH . '/configs/files.ini' ) ;

/**
 * le formulaire
 */
$form     = new My_Form ( $_POST ) ;

/**
 * et le modèle de la table
 */
$myModel = new My_Model() ;

/**
 * on appelle la classe en lui passant les arguments (ici sans filtre)
 */
$insert = new Web82_Content_Insert ( $form , $myModel , NULL , $config ) ;

/**
 * et on traite les données (ici en insertion)
 */
$result = $insert -> insert ( 'insert' ) ;
Voilà, c'est terminé ! N'hésitez pas à me laisser vos commentaires, notamment si vous trouvez des bugs !

Aucun commentaire:

Enregistrer un commentaire