Je vous propose de voir une gestion de galerie photo pour l’administration, telle que je l’ai abordée dans un de mes derniers projets.
Pour vous situer, il s’agit d’un site de vente en ligne.
Qui dit vente dit produits, et qui dit produits dit photos ![]()
L’idée est de gérer les photos d’un produit directement depuis sa page d’édition de l’admin générator.



On a choisi de gérer l’upload avec swfUpload qui gére un liste d’attente pour l’upload de plusieurs fichiers. Et l’upload en background qui ne gèle pas la page, ca fait plus web2.0 :p
(http://code.google.com/p/swfupload/)
J’ai téléchargé le SWFUpload v2.2.0.1 Samples.zip et j’ai utilisé les fichiers du répertoire « simpledemo » que j’ai réparties comme suis.
(Le fichier css d’origine s’appelait default.css, je l’ai renommé pour des raisons évidentes.)
- /web/swf/swfupload.swf
- /web/css/swfUpload.css
- /web/js/swfupload.js
- /web/js/swfupload.queue.js
- /web/js/fileprogress.js
- /web/js/handlers.js
- /web/images/XPButtonUploadText_61x22.png
Un coup d’oeil rapide à la partie du schema.yml qui nous intéresse. Rien de transcendant, un objet Produit avec quelques champs types et l’objet ProduitPhoto qui lui est lié. Sur l’objet ProduitPhoto, un filename et un booléen pour définir l’image par défaut du produit.
...
Produit:
tableName: monprojet_produit
actAs:
Timestampable: ~
columns:
id: { type: integer(4), unsigned: true, primary: true, autoincrement: true }
nom: { type: string(255), notnull: true }
description: { type: clob }
prix: { type: decimal, scale: 2 }
ProduitPhoto:
tableName: monprojet_produit_photo
columns:
id: { type: integer(4), unsigned: true, primary: true, autoincrement: true }
produit_id: { type: integer(4), unsigned: true }
filename: { type: string(255), notnull: true }
is_default: { type: boolean, default: false }
relations:
Produit:
local: produit_id
foreign: id
foreignAlias: Photos
onDelete: CASCADE
... |
On génére l’admin-generator pour l’objet produit.
./symfony doctrine:generate-admin backend Produit |
Et c’est partie !
Pour rajouter cette gestion des images dans l’ecran d’édition des produits il faut rajouter un partial à la vue edit.
(On la rajoute uniquement sur l’edit, car sur l’écran create, l’objet n’est pas encore persisté en base et donc on ne peut pas lui rattacher des objets ProduitPhoto)
Le fonctionnement est le suivant. Le partial général où est inclus et configuré swfUpload (_photoUpload.php). Un parial _photoListe.php inclus dans _photoUpload.php qui servira pour raffraichir la liste en ajax (avec jQuery). Et un dernier partial _ajaxPhotoDelete.php qui ne sert qu’a retourner l’animation à faire lors de la suppression d’une photo.
On commence par positionner le partial _photoUpload.php dans le generator.yml en contexte edit.
generator:
class: sfDoctrineGenerator
param:
model_class: Produit
theme: admin
non_verbose_templates: true
with_show: false
singular: ~
plural: ~
route_prefix: produit_produit
with_doctrine_route: 1
config:
actions: ~
fields:
created_at: { label: Date de création, date_format: dd/MM/yyyy }
updated_at: { label: Date de modification, date_format: dd/MM/yyyy }
nom: { label: Nom }
description: { label: Déscription }
list:
title: Liste des produits
display: [=id, =nom]
filter: ~
form: ~
edit:
title: Modifier un produit
display:
"Produit": [id, nom, description]
"Photos": [_photoUpload]
"Dates": [created_at, updated_at]
new:
title: Ajouter un produit
display:
"Produit": [id, nom, description]
"Dates": [created_at, updated_at] |
Et on va se pencher plus en détail sur ce partial _photoUpload.php.
<?php use_stylesheet('swfUpload.css') ?> <?php use_javascript('jquery.js') ?> <?php use_javascript('jquery-ui.js') ?> <?php use_javascript('swfupload.js') ?> <?php use_javascript('swfupload.queue.js') ?> <?php use_javascript('fileprogress.js') ?> <?php use_javascript('handlers.js') ?> <div id="pictures_list" class="sf_admin_form_row"> <?php include_partial('produit/photoListe', array('photos' => $form->getObject()->getPhotos())) ?> </div> <div id="picture_upload" class="sf_admin_form_row"> <div class="fieldset flash" id="fsUploadProgress"><span class="legend">File d'attente.</span></div> <div id="divStatus">0 fichier uploadé.</div> <div> <span id="spanButtonPlaceHolder"></span> <input id="btnCancel" type="button" value="Cancel All Uploads" onclick="$.swfUpload.cancelQueue();" disabled="disabled" style="margin-left: 2px; font-size: 8pt; height: 29px;" /> </div> </div> <script type="text/javascript"> (function($) { $('div.actions a.default').live('click', function(event) { event.preventDefault(); $.post( $(this).attr('href'), { }, function(data) { $('#pictures_list').html(data); } ); }); $('div.actions a.delete').live('click', function(event) { event.preventDefault(); if(confirm('Etes vous sur de vouloir supprimer cette photo ?')) { $.post( $(this).attr('href'), { }, function(data) { eval(data); } ); } }); myQueueComplete = function(numFilesUploaded) { var status = document.getElementById("divStatus"); status.innerHTML = numFilesUploaded + " fichier" + (numFilesUploaded === 1 ? "" : "s") + " uploadé" + (numFilesUploaded === 1 ? "" : "s") + "."; $.post( '<?php echo url_for('produit_ajax_photo_liste', $form->getObject()) ?>', { }, function(data) { $('#pictures_list').html(data); } ); }; jQuery.swfUpload = new SWFUpload({ flash_url: "/swf/swfupload.swf", upload_url: "<?php echo url_for('produit_photo', $form->getObject()) ?>", post_params: {"<?php echo ini_get('session.name') ?>" : "<?php echo session_id(); ?>"}, file_size_limit: "100 MB", file_types: "*.jpg;*.gif;*.png", file_types_description: "Fichiers image", file_upload_limit: 10, file_queue_limit: 0, file_post_name: 'photo', custom_settings: { progressTarget: "fsUploadProgress", cancelButtonId: "btnCancel" }, debug: false, // Button settings button_image_url: "/images/XPButtonUploadText_61x22.png", button_width: "61", button_height: "22", button_placeholder_id: "spanButtonPlaceHolder", // The event handler functions are defined in handlers.js file_queued_handler: fileQueued, file_queue_error_handler: fileQueueError, file_dialog_complete_handler: fileDialogComplete, upload_start_handler: uploadStart, upload_progress_handler: uploadProgress, upload_error_handler: uploadError, upload_success_handler: uploadSuccess, upload_complete_handler: uploadComplete, queue_complete_handler: myQueueComplete }); })(jQuery); </script> |
Il faut penser à inclure les différents éléments nécessaires le css et les tout les js. Je les ai inclus ici pour bien les mettre en évidence.
Après il suffit de positionner les différents éléments html nécessaires au fonctionnement de swfUpload.
Et enfin le javascript pour initialiser swfUpload. J’utilise jQuery pour gérer l’ajax, j’ai donc adapté l’initialisation de swfUpload pour la rendre « jQuery friendly » :p
L’initialisation de swfUpload est assez standard, avec toute une tripoté de paramètres pour définir : le type de fichier accepté, configurer l’apparence du bouton et configurer tous les retour d’événements qu’il est possible d’utiliser.
J’ai laissé les gestionnaires d’événements par défaut à l’exception de l’événement de fin d’upload (myQueueComplete) que j’ai redéfini pour faire un appel ajax et recharger le partial _photoListe.php.
J’attire votre attention sur le parametre post_params auquel on passe le session name et le session_id, on verra un peu plus bas le pourquoi du comment
Le partial _photoListe.php, rien de spécial. Un listing des photos du produit avec 2 liens pour supprimer ou définir comme image par défaut, les 2 faisant appel à une action en ajax.
(NB: On retrouve le Helper Thumb de thomas pour les miniatures des photos)
<?php use_helper('Thumb') ?> <?php if($photos->count() > 0): ?> <?php foreach( $photos as $photo ): ?> <div id="photo-<?php echo $photo->getId()?>" class="picture"> <?php echo showThumb( $photo->getFilename(), 'produits', array( 'height' => 120, 'width' => 120, 'alt' => $photo->getProduit()->getNom(), 'title' => $photo->getProduit()->getNom(), 'border' => 0 ), 'center', 'produit-defaut.jpg') ?> <div class="actions"> <?php if($photo->isDefault()): ?> <strong><img src="/images/backend/star.png" align="left" /> Par défaut</strong> <?php else: ?> <ul> <li><a href="<?php echo url_for('produit_photo_ajax_default', $photo) ?>" class="default"><img src="/images/backend/star.png" align="left" /> Par défaut</a></li> <li><a href="<?php echo url_for('produit_photo_ajax_delete', $photo) ?>" class="delete"><img src="/images/backend/cross.png" align="left" /> Supprimer</a></li> </ul> <?php endif; ?> </div> </div> <?php endforeach; ?> <div class="clear"></div> <?php else: ?> <p>Aucune photo pour l'instant.</p> <?php endif; ?> |
Le partial _ajaxPhotoDelete.php, c’est juste l’effet jQuery-ui pour faire disparaitre la div de la photo qui viens d’être supprimée.
$('#photo-<?php echo $photo_id ?>').effect('drop'); |
Un petit tour sur le routing.yml pour défnir les différentes routes utilisées.
...
produit_photo:
url: /produit/:id/upload
class: sfDoctrineRoute
options: { model: Produit, type: object }
param: { module: produit, action: upload }
requirements:
sf_method: [get, post]
produit_photo_ajax_default:
url: /produit_photo/:id/default
class: sfDoctrineRoute
options: { model: ProduitPhoto, type: object }
param: { module: produit, action: ajaxPhotoDefault }
requirements:
sf_method: [post]
produit_photo_ajax_delete:
url: /produit_photo/:id/delete
class: sfDoctrineRoute
options: { model: ProduitPhoto, type: object }
param: { module: produit, action: ajaxPhotoDelete }
requirements:
sf_method: [post]
produit_ajax_photo_liste:
url: /produit_photo/:id/list
class: sfDoctrineRoute
options: { model: Produit, type: object }
param: { module: produit, action: ajaxPhotoListe }
requirements:
sf_method: [post]
... |
On utilise la classe formulaire qui va gérer l’upload.
Il faut désactiver le csrf car l’envoie n’est pas effectué par le formulaire mais par l’animation flash.
<?php class ProduitPhotoForm extends BaseProduitPhotoForm { public function configure() { $this->widgetSchema['produit_id'] = new sfWidgetFormInputHidden(); $this->widgetSchema['is_default'] = new sfWidgetFormInputHidden(); $this->widgetSchema['filename'] = new sfWidgetFormInputFile(array()); $this->validatorSchema['filename'] = new sfValidatorFile(array( 'required' => true, 'path' => sfConfig::get('sf_upload_dir').'/produits/source', 'mime_types' => 'web_images', 'max_size' => 10485760 ), array( 'max_size' => 'Fichier trop gros (10Mo maximum).' )); $this->disableCSRFProtection(); } } |
Et enfin l’action.class.php qui orchestre tout ca.
Au niveau de l’action executeUpload il faut créer « à la main » le tableau des paramètres à passer au bind.
<?php require_once dirname(__FILE__).'/../lib/produitGeneratorConfiguration.class.php'; require_once dirname(__FILE__).'/../lib/produitGeneratorHelper.class.php'; /** * produit actions. * * @package * @subpackage produit * @author * @version SVN: $Id: actions.class.php 12474 2008-10-31 10:41:27Z fabien $ */ class produitActions extends autoProduitActions { public function executeUpload(sfWebRequest $request) { $this->produit = $this->getRoute()->getObject(); $this->forward404unless($this->produit); $this->form = new ProduitPhotoForm(); $values = array( 'produit_id' => $this->produit->getId(), 'is_default' => ($this->produit->getPhotos()->count() > 0) ? false : true, 'filename' => $request->getFiles('photo') ); $this->form->bind($values, $values); if ($this->form->isValid()) { $photo = $this->form->save(); $return = 'ok'; } else { $return = 'ko'; } return $this->renderText($return); } /** * ajax pour definir une photo par defaut * * @param sfWebRequest $request */ public function executeAjaxPhotoDefault(sfWebRequest $request) { if($request->isXmlHttpRequest()) { $photo = $this->getRoute()->getObject(); $produit = $photo->getProduit(); $old_default = $produit->getPhotoDefault(); $produit->setPhotoDefaut($photo->getId()); return $this->renderPartial('produit/photoListe', array('photos' => $produit->getPhotos())); } else { $this->redirect404(); } } /** * ajax pour effacer une photo * * @param sfWebRequest $request */ public function executeAjaxPhotoDelete(sfWebRequest $request) { if($request->isXmlHttpRequest()) { $photo = $this->getRoute()->getObject(); $photo_id = $photo->getId(); $photo->delete(); return $this->renderPartial('produit/ajaxPhotoDelete', array('photo_id' => $photo_id)); } else { $this->redirect404(); } } /** * ajax pour avoir la liste des photos * * @param sfWebRequest $request */ public function executeAjaxPhotoListe(sfWebRequest $request) { if($request->isXmlHttpRequest()) { $produit = $this->getRoute()->getObject(); return $this->renderPartial('produit/photoListe', array('photos' => $produit->getPhotos())); } else { $this->redirect404(); } } } |
Produit.class.php
<?php class Produit extends BaseProduit { public function setPhotoDefaut($photoId) { Doctrine_Query::create() ->update('ProduitPhoto p') ->set('p.is_default', '?', false) ->where('p.produit_id = ?', $this->getId()) ->execute(); Doctrine_Query::create() ->update('ProduitPhoto p') ->set('p.is_default', '?', true) ->andWhere('p.id = ?', $photoId) ->execute(); return true; } public function getPhotoDefault() { return Doctrine::getTable('ProduitPhoto')->getDefault($this->getId()); } } |
ProduitPhotoTable.class.php
<?php class ProduitPhotoTable extends Doctrine_Table { public function getDefault($produit_id) { return $this->createQuery('c') ->andWhere('c.produit_id = ?', $produit_id) ->andWhere('c.is_default = ?', true) ->fetchOne(); } } |
ProduitPhoto.class.php
<?php class ProduitPhoto extends BaseProduitPhoto { public function isDefault() { return (bool) $this->getIsDefault(); } } |
Le problème !
A ce stade, tout est fini et en place pour que ca marche. Hors ca ne marche pas… Pourquoi ? Je rapelle le contexte, on est dans l’admin donc on est identifié et c’est là le problème. En effet swfUpload est une animation flash contenu dans le page mais qui va initialiser des requêtes http parallèles à celle utilisée par le navigateur. Et donc lorsque swfUpload se pointe à l’url « produit_photo » pour initialisé son upload, il se prend une belle erreur 403…
Le problème est connu des concepteurs de swfUpload et la solutions qu’ils préconisent est de forcé l’identifiant de session dans le script d’upload en faisant un session_id($session_id). C’est pourquoi on le fait passer dans la variable « post_params » à l’initialisation de swfUpload.
Pour faire la même chose dans symfony on ne peut pas le faire dans une action, car la session est déjà initialisée à ce niveau. On est obligé d’aller modifié le factories.yml et la calsse la classe sfSessionStorage pour aller forcer l’identifiant de session dans le cas ou on fait appel à l’action upload.
C’est un tip que j’ai trouvé sur le forum symfony qui était pour la version 1.0, et qui fonctionne toujours à la différence que le contexte n’est plus passé en paramètre dans la 1.2 et donc il faut aller le charger.
factories.yml
...
all:
...
storage:
class: mySessionStorage
param:
session_name: weezobackend
... |
mySessionStorage.calss.php
<?php class mySessionStorage extends sfSessionStorage { public function initialize($options = null) { $context = sfContext::getInstance(); //Shitty work-around for swfuploader if( $context->getActionName() == "upload") { $sessionName = $parameters["session_name"]; if($value = $context->getRequest()->getParameter($sessionName)) { session_name($sessionName); session_id($value); } } parent::initialize($options); } } |
Ça y est, cette fois ça marche !
Et vous avez une gestion d’images directement dans la fiche du produit qui est assez user-friendly
Merci d’avoir lu jusqu’au bout :p
Je reste à votre disposition pour toute questions ou suggestions ![]()
@bientôt !

Bonjour,
J’ai eu une petite erreur d’installation, je ne sais pas si c’est du à mes config serverus en local mais il a fallut que j’enleve ‘mime_types’ => ‘web_images’, dans la classe de mon formulaire.
En faisant afficher les erreus il me disait invalide mime_type (‘application/octet-stream’).
j’ai enlevé, ca marche nickel!
de toute façon la vérification est déja faite via le JS.
Bonjour,
J\’ai bien appliqué ce tuto sympatique. J\’ai rencontré des problèmes avec le generator.yml. Le parser ne comprenait pas les display dans le contexte \"edit\" et \"new\" (si on supprimait celui dans \"edit\"). J\’ai donc du les rappatrier dans le form général.
Ensuite, je me retrouve maintenant devant ces logs :
\"[27-Sep-2009 18:28:12] Action \"produit/1\" does not exist.
[27-Sep-2009 18:28:12] Action \"produit_photo/1\" does not exist.\"
Cela se produit lorsque j\’essai d\’uploadé un fichier. Evidemment erreur 500 apparait dans le swf.
J\’ai vraiment suivi le tuto avec le mm schema, la seule différence est que j\’utilise propel plutot que doctrine. (j\’ai apporté les modifs).
Avez-vous une idée pour ces comportement étranges ?
Merci,
neothone
Mea culpa,
Il y avait en effet une petite coquille dans le generator.yml
Le display n’est pas à placer dans « generator/param/config/edit/form/display » mais dans « generator/param/config/edit/display ».
J’ai apporté la modification au post.
Pour ce qui est de l’erreur 500. Est ce qu’elle apparait tout le temps ? ou seulement sur un nouveau produit ?
Merci beaucoup, cela fonctionne a merveille.
Bonjour!
Merci pour ce tuto qui m\’a permis de mettre en place dans mon site un uploader pour mes galeries photos.
Il y a cependant un bug, si je snif le réseau pour connaitre le résultat d\’execution de l\’upload, j\’ai ce message d\’erreur:
Auriez vous une idées sur la maniere de supprimer ces erreurs ?
Apparement cela se declenche sur le bind du form.
Merci beaucoup
Warning: array_keys() [function.array-keys]: The first argument should be an array in /Users/jeremy/Sites/sfLaura/lib/vendor/symfony/lib/form/sfForm.class.php on line 1114
Warning: sort() expects parameter 1 to be array, null given in /Users/jeremy/Sites/sfLaura/lib/vendor/symfony/lib/form/sfForm.class.php on line 1115
Warning: array_keys() [function.array-keys]: The first argument should be an array in /Users/jeremy/Sites/sfLaura/lib/vendor/symfony/lib/form/sfForm.class.php on line 1114
Warning: sort() expects parameter 1 to be array, null given in /Users/jeremy/Sites/sfLaura/lib/vendor/symfony/lib/form/sfForm.class.php on line 1115
Warning: array_keys() [function.array-keys]: The first argument should be an array in /Users/jeremy/Sites/sfLaura/lib/vendor/symfony/lib/form/sfForm.class.php on line 1114
Warning: sort() expects parameter 1 to be array, null given in /Users/jeremy/Sites/sfLaura/lib/vendor/symfony/lib/form/sfForm.class.php on line 1115
Warning: array_keys() [function.array-keys]: The first argument should be an array in /Users/jeremy/Sites/sfLaura/lib/vendor/symfony/lib/form/sfForm.class.php on line 1114
Warning: sort() expects parameter 1 to be array, null given in /Users/jeremy/Sites/sfLaura/lib/vendor/symfony/lib/form/sfForm.class.php on line 1115
Warning: Cannot modify header information – headers already sent by (output started at /Users/jeremy/Sites/sfLaura/lib/vendor/symfony/lib/form/sfForm.class.php:1114) in /Users/jeremy/Sites/sfLaura/lib/vendor/symfony/lib/response/sfWebResponse.class.php on line 335
Warning: Cannot modify header information – headers already sent by (output started at /Users/jeremy/Sites/sfLaura/lib/vendor/symfony/lib/form/sfForm.class.php:1114) in /Users/jeremy/Sites/sfLaura/lib/vendor/symfony/lib/response/sfWebResponse.class.php on line 349
ok
Bonjour,
J’ai suivi toute la démarche et cela fonctionne.
Toutefois, l’upload créé sur mon serveur des fichiers .bin inexploitable et comme toi, j’ai du retirer le mime_types dans le validateur de mon champs d’upload.
Je ne sais pas quoi faire. J’ai vérifié que les images étaient bien dans les mime_types de sfCompat10Plugin, j’ai activé ce plugin, mais rien n’y fait.
Une idée ? svp
Mathieu
J’ai trouvé une issue en créant une méthode generate%nameColumn%Filename($file) dans mon Object.php qui permet de sauvegarde le fichier sous le nom que l’on souhaite.
Pas très propre… mais ça marche.
j’ai une question au sujet des méthodes $photo->isDefault(), $produit->getPhotoDefault() et $produit->setPhotoDefaut().
elles ne sont pas définies et je m’étonne que personne n’en fasse la remarque. il faut bien les ajouter aux classes ProduitPhoto.class.php et Produit.class.php n’est-ce-pas?
Salut yanice,
En effet, il manquait quelques éléments qui ne concernaient pas directement l’explication du fonctionnement de swfUpload mais qui étaient succeptibles d’entraver la compréhension.
J’ai rajouté les méthodes manquantes.
Merci de ta participation.
Bonjour,
J\’ai suivi le tuto à la lettre et j\’ai quelques soucis.
Comme matthieu, j\’ai les messaged d\’erreurs en backend_dev.php mais pas en backend.php.
Les images sont sauvegardés mas avec une extension .bin donc inexploitables.
Quelqu\’un a t-il une idée?
Merci
Salut Olivier et tout le monde,
Merci pour ce tutoriel, c\’est très sympa de faire profiter les autres de ton expérience.
J\’ai suivi ton tuto et après quelques petites erreurs de copie et quelques modifications pour coller à mon schema de données, ca marche !
Un super effet, un grand MERCI.
Leny
Salut!
Merci beaucoup pour ce tuto très clair. Je rencontre néanmoins un petit souci au niveau de l’upload. Je teste en local avec Wamp.
Dans le validateur du formulaire :
- si je laisse ‘mime_types’ => ‘web_images’, aucune image n’est uploadée et la variable $return retourne ‘ko’
- si je mets ‘mime_types’ => ‘image/jpeg’, je me prends une erreur 500 par swfupload
- si je commente la ligne ‘mime_types’, l’upload marche mais l’image a une extension .bin
Avez vous une idée pour régler ce problème? La réponse de Mathieu (7) a l’air intéressante mais il ne laisse pas sa fonction…
De plus, lorsque je clique sur supprimer, la photo est supprimée de la base mais pas du répertoire et la page ne s’actualise pas (la photo reste jusqu’à un refresh manuel)
Merci d’avance!
Thank you Olivier for the detailed tutorial.
I did all of the tutorial but nothing happened, I run Swfupload+Symfony1.4+sfGuard4.0
I think that the problem is with sfGuard,that it doesn’t pass the session.
Any help, Please ASAP?????
Hi tamer,
Thank you for reading us, even if we do not translate our posts.
You said you are using sfGuard it’s looks like you are using Propel. This post was made to work with the Doctrine ORM, there must be some changes to convert it work to Propel.
If you are not using Propel the problem is maybe that you do not use the right plugin, you should use sfDoctrineGuardPlugin.
Plus this post is a bit old right now, it was initialy for symfony 1.2 i’ll try to re-write it soon for 1.4
Best regards.
Olivier, Thank you for instant response.
I took in mind differences between Propel and Doctrine,
I believe that the problem is with sessions and sfGuard.because in the log file it really go to create action but there’s signout/signin sfGuard log, maybe it doesn’t pass the session of flash.
Please if you have any idea , it’s urgent to me to solve this problem.