A while ago I’ve read this cool article by Will Linssen on sortable list in jQuery and this inspired me to take it to the next level which is Symfony. This is a tutorial how to make a for example product gallery in Symfony backend admin modules. For this I’m using Symfony 1.4, sfJqueryReloadedPlugin , sfThumbnailPlugin and Doctrine. The main goals we want to achieve here are:
- every product is able to have many images
- the images are sortable in desired order
- we want a drag’n'drop interface to change the order
- we want to get the embeding of form to minimum
1. The schema
So just to give you an idea let’s get things started. This is how the product might look like:
Product:
columns:
name: { type: string(255), notnull: true }
description: { type: blob }
price: { type: decimal }
Once again, this is not a final product, I’m just giving you something to work on. Expanding this model is up to you.
So now it’s time for the image:
Image:
columns:
name: { type: string(255), notnull: true }
image: { type: string(255), notnull: true }
ord: { type: integer }
product_id: { type: integer }
relations:
Product:
foreignAlias: Images
local: product_id
foreign: id
onDelete: cascade
Now just build all with:
./symfony doctrine:build --all
and generate an admin module for products (I’m assuming that you already have you backend app):
./symfony doctrine:generate-admin backend Product
2. The new image form
Now the thing we want to do is to embed a “add new Image” form in our existing form of Product edition. So go to you lib/form directory and create a class like this one.
// lib/form/BackendEmbedImageForm.class.php
<?php
/**
* Description of BackendEmbedImageForm
*
* @author Karol Sójko <karolsojko@gmail.com>
*/
class BackendEmbedImageForm extends ImageForm
{
public function configure()
{
unset($this['product_id'], $this['ord']);
$this->widgetSchema['name'] = new sfWidgetFormInput();
$this->widgetSchema['image'] = new sfWidgetFormInputFileEditable(array(
'file_src' => sfConfig::get('app_product_pictures_folder') . 'thumbnail/' . $this->getObject()->getImage(),
'is_image' => true,
'edit_mode' => !$this->isNew(),
'template' => '%file% %input%'
));
$this->validatorSchema['name'] = new sfValidatorString();
$this->validatorSchema['image'] = new sfValidatorFile(array(
'path' => sfConfig::get('sf_web_dir') . sfConfig::get('app_product_pictures_folder'),
'required' => false,
'mime_types' => 'web_images'
));
}
}
As you can see the paths to the folders where the pictures will be stored are defined in the app.yml. This form extends the ImageForm as we don’t want to mess that one up and just keep things nice and clean. And as we want to have some thumbnails let’s override the save function in Image class:
public function save(Doctrine_Connection $conn = null)
{
if($this->isModified())
{
$uploadDir = sfConfig::get('sf_web_dir') . sfConfig::get('app_product_pictures_folder');
$thumbnail = new sfThumbnail(100, 100);
$thumbnail->loadFile($uploadDir.'/'.$this->getPhoto());
$thumbnail->save($uploadDir.'/thumbnail/'. $this->getPhoto());
}
return parent::save($conn);
}
3. The Product form
So now we want the product form to embed the new image form and we want to override the bind function to corespond to the changes as well.
// lib/form/doctrine/ProductForm.class.php
<?php
/**
* Product form.
*
* @author Karol Sójko <karolsojko@gmail.com>
*/
class ProductForm extends BaseProductForm
{
public function configure()
{
$imageForm = new BackendEmbedImageForm();
$this->embedForm('image', $imageForm);
$this->widgetSchema['image']->setLabel('New Image');
}
public function bind(array $taintedValues = null, array $taintedFiles = null)
{
if (is_null($taintedValues['image']['name']) ||
strlen($taintedValues['image']['name']) === 0 )
{
unset($this->embeddedForms['image'], $taintedValues['image']);
$this->validatorSchema['image'] = new sfValidatorPass();
}
else
{
$this->embeddedForms['image']->getObject()->setProduct($this->getObject());
}
$output = parent::bind($taintedValues, $taintedFiles);
foreach ($this->embeddedForms as $name => $form)
{
$this->embeddedForms[$name]->isBound = true;
$this->embeddedForms[$name]->values = $this->values[$name];
}
return $output;
}
}
4. The CSS, jQuery, routing and actions
First of all let’s get some style going on here:
/* web/css/imageList.css */
#info {
display: block;
padding: 10px; margin-bottom: 20px;
background-color: #efefef;
}
#images-list {
list-style: none;
}
#images-list li {
display: block;
padding: 20px 10px; margin-bottom: 3px;
background-color: #efefef;
}
#images-list li img.handle {
margin-right: 20px;
cursor: move;
}
Then let’s install the sfJQueryReloaded plugin. This is very important as we will use the jQuery helper. So now let’s make ourselves some jQuery magic:
// web/js/sortList.js
$(document).ready(function() {
$("#images-list").sortable({
handle : '.handle',
update : function () {
var order = $('#images-list').sortable('serialize');
$("#info").load("sort-images?"+order);
}
});
});
As you can se we invoke a route sort-images with the load function. So let’s make that route in the routing.yml and let’s add a delete route while we’re at it.
product_sort_images:
url: /product/:id/sort-images
param: { module: product, action: sortImages }
product_delete_image:
url: /product/:id/delete
param: { module: product, action: deleteImage }
Here are the coresponding actions:
// backend/modules/product/actions/actions.class.php
public function executeDeleteImage(sfWebRequest $request)
{
$image = Doctrine::getTable('Image')->findOneById($request->getParameter('id'));
$image->delete();
return sfView::NONE;
}
public function executeSortImages(sfWebRequest $request)
{
foreach($request->getParameter('listItem') as $position => $item)
{
$image = Doctrine::getTable('Image')->findOneById($item);
if($image != null)
{
$image->setOrd($position);
$image->save();
}
}
return sfView::NONE;
}
5. The component
Now let’s make a component that will display all the available images for us. We want to call the component “showImages”, so let’s just add this to the generator.yml.
// backend/modules/product/config/generator.yml config: ... form: display: [ ~showImages, image, name, description, price ]
Now let’s add a method to the components class in our product module:
// backend/modules/product/actions/components.class.php
public function executeShowImages(sfWebRequest $request)
{
$productId = $request->getParameter('id');
$this->images = array();
if($productId)
{
$query = Doctrine_Query::create()->from('Image i')
->where('i.product_id = ?', $productId)
->orderBy('i.ord ASC');
$this->images = $query->execute();
}
}
The reason why i didn’t use the $product->getImages() method is that I want them to be ordered by the ord field. Now let’s create the view for this component:
<?php use_stylesheet('imageList'); ?>
<?php use_javascript('sortList'); ?>
<?php use_helper('jQuery'); ?>
<div id="info"></div>
<div id="imagesList">
<ul id="images-list">
<?php foreach ($images as $image): ?>
<li id="listItem_<?php echo $image->getId(); ?>">
<img src="/images/arrow.png" alt="move" width="16" height="16" class="handle" />
<strong><?php echo $image->getName(); ?></strong>
<?php echo image_tag(sfConfig::get('app_product_pictures_folder') .
'thumbnail/' . $image->getImage()); ?>
<?php echo jq_link_to_remote(image_tag('/images/delete.png', array()), array(
'url' => '@product_delete_image?id=' . $image->getId(),
'complete' => '$("#listItem_' . $image->getId() . '").hide();',
), array('style' => 'background-image: none;')); ?>
</li>
<?php endforeach; ?>
</ul>
</div>
As you can see we use here the jq_link_to_remote function to invoke the delete function with jQuery.
Hope this helps. Share the love

Hi there,
this tutorial helped me a lot, so I wanted to share something.
In the embedded form, when you validate, you may want to set the image field to be required (here showing your unedited version):
$this->validatorSchema['image'] = new sfValidatorFile(array(
‘path’ => sfConfig::get(‘sf_web_dir’) . sfConfig::get(‘app_product_pictures_folder’),
‘required’ => false,
‘mime_types’ => ‘web_images’
));
if you leave it as it is, and you insert the image name but you don’t choose the file, you get a symfony error.
If you set that required to true, it won’t complain if you leave both empty, but will just flash “Required” on the file upload element, if you have filled the name field but didn’t choose the file.
ciao
thanks!that was wonderful!..
I always error message
The component does not exist: “product”, “showImages”.
when I access to this application “http://127.0.0.1/symfony/web/backend_dev.php/product/new ”
can you help me please
Can’t really do much with a description like that – search if you didn’t do a typo (case sensitive typo maybe). It seems to work for everyone else just fine.
I have been browsing online more than 2 hours today, yet I never
found any interesting article like yours. It’s pretty worth enough for me. Personally, if all web owners and bloggers made good content as you did, the net will be a lot more useful than ever before.
Thank you
Hey Karol, I got another question.
I want to automatically rename my pictures and let them have the name of my article number.
Do you know how I can achieve this ?
greets
Hey, I am trying to implement your image gallery but i am facing the error:
500 | Internal Server Error | Doctrine_Record_UnknownPropertyException
Unknown record property / related component “image” on “Image”
I followed the reply and changed getPhoto() to getImage().
could you help me on my issue ? I could send you my code if that helps …
greetings
It seems like don’t have an “image” field defined in your model. Please check your classes for that field and the yml model.
hey, thank you for your answer, but i have an image field in my schema:
Image:
columns:
name: { type: string(255) }
article_id: { type: integer }
order: { type: integer }
metainfo: { type: string(255) }
the classes are created and inside the database it exists
omg, yea you say it, now i realize, THANKS
Thanks, its working now, you rock man
my 3rd day on symfony, I love it, but if I am honest, i barely understood your code, I couldn’t rebuild that on my own … thanks for sharing!
you rock!
hey, I still have a question to your code:
BackendImageForm.class.php:
$this->widgetSchema['image'] = new sfWidgetFormInputFileEditable(array(
‘file_src’ => sfConfig::get(‘app_product_pictures_folder’) . ‘thumbnail/’ . $this->getObject()->getImage(),
‘is_image’ => true,
‘edit_mode’ => !$this->isNew(),
‘template’ => ‘%file% %input%’
));
isnt ‘thumbnail/’ missing a / at beginning ?
because here, in the Image class:
if($this->isModified())
{
$uploadDir = sfConfig::get(‘sf_web_dir’) . sfConfig::get(‘app_product_pictures_folder’);
$thumbnail = new sfThumbnail(100, 100);
$thumbnail->loadFile($uploadDir.’/’.$this->getPhoto());
$thumbnail->save($uploadDir.’/thumbnail/’. $this->getPhoto());
}
it has one …
Witam.
Na wstepie – swietny tutorial. Gratuluje.
Postanowilem wdrozyc Twoje rozwiazanie do mojego skromnego projektu, jednak zetknalem sie z problemem przy zapisie kolejnosci zdjec w ramach wybranej galerii (pozycja w ogole nie jest zapisywana).
W pliku konfiguracyjnym routing.yml mam wpis postaci:
modgallery_sort_images:
url: /gallery/:id/sort-images
param: { module: gallery, action: sortImages }
W partalu odpowiedzialnym za wyswietlanie zdjec:
<li id="listItem_getId(); ?>”>
__(‘Przesuń’), ‘title’ => __(‘Przesuń’), ‘size’ => ’16×16′, ‘class’ => ‘handle’)) . __(‘Przesuń’) ?>
getName(); ?>
__(‘Skasuj’),
‘alt’ => __(‘Skasuj’))) . __(‘Skasuj’), array(
‘confirm’ => __(‘Czy na pewno chcesz usunąć wybrane zdjęcie?’),
‘url’ => ‘@modgallery_delete_image?id=’ . $image->getId(),
‘complete’ => ‘$(“#listItem_’ . $image->getId() . ‘”).hide();’,
), array(‘style’ => ‘background-image: none;’)); ?>
Metoda obslugujaca sortowanie zdjec wyglada tak:
public function executeSortImages(sfWebRequest $request) {
foreach($request->getParameter(‘listItem’) as $position => $item) {
$image = Doctrine::getTable(‘Image’)->findOneById($item);
if($image != null) {
$image->setOrd($position);
$image->save();
}
}
return sfView::NONE;
}
Jednak nie jest ona wywolywana z poziomu skryptu:
$(document).ready(function() {
$(“#images-list”).sortable({
handle : ‘.handle’,
update : function () {
var order = $(‘#images-list’).sortable(‘serialize’);
$(“#info”).load(“sort-images?”+order);
}
});
});
Prosilbym o jakies wskazowki, gdzie szukac bledu.
Bylbym wdzieczny za pomoc.
Pozdrawiam.
yes… You right… it’s problem with $productId… Thank You Karol
Ok so controller of the component “_showImages” is file “component.class.php”. I have there line $this->images = $query->execute();. Component is inculde to layout but all the time I have error… Have You any idea ?
check if your controller enters the if($productId) statement -> maybe the images doesn’t get to be declared. And if it is, try to print out what your $query->execute() returns.
Add $this->images = array(); before the if statement so error does not get thrown if product id is empty and variable is not created.
Fixed. Thanks
Hi Karol…
I have a problem with:
Notice: Undefined variable: images in C:\xampp\xampplite\htdocs\Galeria2\apps\backend\modules\product\templates\_showImages.php on line 10
Can You help me?
The variable should be declared and assigned in the controller of the component f.e. $this->images = ……
hey,
i’m wondering, if you want to add more upload fields, you need to:
1. in ProductFrom -> configure -> add for loop to embed as many forms you wont
2. … no idea yet
maybe somebody?
Hi,
Great tutorial BTW…
It all seems to be firing up well and am building on the great model.
I had a question:
My model is slightly different to yours but the conceptual modelling is similar.
I’m not clear on how to update the “product_id” of Image with the “id” of Product.
As you can imagine, to get the images to show at the moment I have to manually add the “product_id” into the table Image.
What method do you add this and how…?
Any help greatly appreciated..
Not quite sure if I understood correctly but here’s the thing :
- the Image is in schema defined to have a relation with Product on the field product_id
- in the product form you’ve got a line in the bind method:
$this->embeddedForms['image']->getObject()->setProduct($this->getObject());
- so you’re Image has a getter and setter for the product, which updates the product_id if necessary
Hey,
Thanks for the feedback BTW. i have got it up and running for adding 1 PHOTO.
I can also edit the other info of the JOB model (the photo is imbedded)
However , If I add any further images i get the error below:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry ’4′ for key ‘PRIMARY’
It appears from the SQL dump it is attempting to INSERT INTO rather than UPDATE.
Do you know of a way to make it UPDATE instead..?
doUpdateObject.?
Thanks in advance..
Hi,
first of all, thanks for sharing your knowledge and work.
I’ve implemented what you explain and have a question:
The “new image form” should appear directly in the product form (where you fill up the product information)?
I it does, each time I press save for saving “product information” I have a warning message showing some mandatory fields in the “new image form”. So, each time I want to change a product field, I have to updload and image.
Did I do something wrong?
thanks in advance
Another thing, where should I put the
public function save(Doctrine_Connection $conn = null)
function?I wrote that it should be in Image class
search in lib/model/doctrine/
Check if you have done the binding right. In worst case just make changes to the validators setting required to false.
Ok, now is right. Thanks.
But now, a new problem is coming
. Is about the $this->getPhoto() in the save function. I get
Unknown record property / related component "photo". should I change it for the name/realname of the image?yup, you’re right
should be ‘getImage’
Man, i’m becoming crazy. I’m so sorry of being disturbing you so much.
Now i get:
The file "C:\wamp\www\cabanacafe\web/uploads/galerias/e3ed0f7ffdea280893b01c4af719c40a7b173e6d.jpg" is not readable.This makes me asking the following: In the thumbnail generation:
$uploadDir = sfConfig::get('sf_web_dir') . sfConfig::get('app_galeria_images_folder');
$thumbnail = new sfThumbnail(100, 100);
$thumbnail->loadFile($uploadDir.'/'.$this-getImage());
$thumbnail->save($uploadDir.'/thumbnail/'. $this->getImage());
when calling loadFile, shouldn’t be previously saved the image in $uploadDir? In old fashioned PHP i used to take the image from temp_name.
I think I’m gonna have some fresh air :@
one more time, thanks
esnure that the file is in the directory in which the app is looking for it. Make sure as well that you have read write rights on that folder (chmod)
. Any further problems please mail me
Hi Karol,
Thanks for this post, its very good and helpful! I would highly appreciate your advice on problems I have which are related to your tutorial.
In bind function the if-loop checks whether a name of an image is given or not:
if (is_null($taintedValues['image']['name']) || strlen($taintedValues['image']['name']) === 0 )
But what to do in case when ‘name’ of image is not required and the presence of the file name, so ‘image’ field has to be tested? I thought of something like this:
if (is_null($taintedFiles['image']['image'])
but it doesnt work. Do you have any ideas how to solve it?
The second problem I have is when submitting a form with empty required fields. I get message:
‘Notice: Undefined index: image … on line … ‘
and reference to this line:
$this->embeddedForms[$name]->values = $this->values[$name];
Do you know what it could be?
Thanks for your time,
Cheers
Hallo, Carol!
This gallery is really well done, but really, if we do not need the name of the picture, the script gives error.
how can bind() to rewrite to work?
Thanks.
In the generator.yml I put the display on edit section.
If I do this, edit doesn’t show “new Image”.
How can I fix it?
Go to the dev enviorment by backend_dev.php and check what errors/warnings does it show ? And it always doesn’t hurt to clear cache
I think I’m doing something wrong, but I don’t know what.
I’m in dev mode, my log shows nothing wrong.
It shows in new item, but doesn’t in edit item
My generator.yml shows like this:
generator:
class: sfDoctrineGenerator
param:
model_class: noticia
theme: admin
non_verbose_templates: true
with_show: false
singular: ~
plural: ~
route_prefix: noticia
with_doctrine_route: true
actions_base_class: sfActions
config:
actions: ~
fields: ~
list: ~
filter: ~
form: ~
edit:
display: [ ~muestraAnexos, fecha, title, texto ]
new: ~
you have the display section under edit .. try putting it under form (to have it both in new and edit)
so:
form:
display: [ ... ]
sigh… I don’t get it…
If I put on “form:”, It doesn’t show in new neither.
check if the indentation in your yaml is correct and clear the cache.
LOL I’m so dumb!
I don’t display in “display:”!!! shiiit!
A “noticia” has some “anexo”‘s… so…
display: [ ~muestraAnexos, fecha, title, texto, anexo ]
xD
Thanks for your time!
thanks for the great post.
Erm.. although its shameless selfpromotion but you might want to check out sfImageTransformExtraPlugin instead of sfThumbnail. The separation between design and code will be a lot easier.
I sure will, did this tutorial based on an app where I had sfThumbnail already
Pingback: My Bookmarks For January 23rd – April 6th | Cristiano on Tech/Life
Hi!
I had this problem:
Error: $(“#images-list”).sortable is not a function
Source: http://mywebsite.com/js/sortList.js
Line: 4
Apparently, the jquery-ui-1.7.2.custom.min.js that contains the corresponding function – sortable – was not included. Adding the line below solved the problem (found as usage example in the jQueryHelper.php)
// add this right after use_helper(‘jQuery’)
echo jq_add_plugins_by_name(array(‘sortable’));
Interestingly, it seems for me that this is the correct usage. Or did it come with a version change of the helper…?
Hello, thanks for the comment. I had the latest jquery and the jquery-ui included (provided by google) and everything was fine. So you should remember to include those libraries before including your own script. Cheers !
Hi!
I had this problem:
Error: $(“#images-list”).sortable is not a function
Source: http://mywebsite.com/js/sortList.js
Line: 4
Apparently, the jquery-ui-1.7.2.custom.min.js that contains the corresponding function – sortable – was not included. Adding the line below solved the problem (found as usage example in the jQueryHelper.php)
Interestingly, it seems for me that this is the correct usage. Or did it come with a version change of the helper…?
You have to have a jquery version with all the additional plugins, including sortable
Nice tutorial!
Been struggling for a few hours, though, until I found out something important: sfJqueryReloadedPlugin is a bit out of date and it is not aware of its own structure!
So I had to add this in my settings.yml :
all:
.settings:
jquery_web_dir: /sfJqueryReloadedPlugin
(the default path is /sfJQueryPlugin)
Like Gabor I couldn’t get the sortable plugin to autoload and I had to use the “jq_add_plugins_by_name(array(‘sortable’));” trick.
Notice that if anyone wants to use the latest jQuery libs and plugins, they’ll have to download them into their /plugins/sfJqueryReloadedPlugin/web/js and /plugins/sfJqueryReloadedPlugin/web/js/plugin folders and get Symfony to point at them in the setting.yml :
all:
.settings:
jquery_web_dir: /sfJqueryReloadedPlugin
jquery_core: jquery-1.4.2.min.js
jquery_sortable: jquery-ui-1.8.2.custom.min.js
Well, that’s all, folks!
Very useful, cheers!
thank you
Problem rozwiązany
w metodzie save() trzeba dodac parent::save().
Swietny tutorial!
Pozdrawiam
Faktycznie, dziękuję .. save poprawiłem
Cieszę się, że pomogło
Witam.
Zrobiłem wszystko dokładnie z Pana opisem, zdjęcie jest uploadowane, jednak dane nie są zapisywane w tabeli Image. Komponent dziala, bo jak wrzucę coś do tabeli Image to wyświetla się w panelu… Gdzie może być błąd? W formularzu produktu mam embedI18n oraz to co w Pana opisie.
Pozdrawiam
Hello Karol.
Thanks for the tutorial, but I can’t get it to work.
I get an error:
The component does not exist: “product”, “showImages”.
I have a few questions, if I may
1. (Step 5) After I add
form:
display: [ ~showImages, image, name, description, price ]
Do I need to rebuild the project or do anything apart from saving?
2. I don’t have // backend/modules/product/actions/components.class.php
Do I just create it?
3. You say “Now let’s create the view for this component:” Where do I store this and what should the file name be?
Thank you for your time.
1. No you don’t need to do anything else
2. You have an error saying that the components doesn’t exist because you don’t have backend/modules/product/actions/components.class.php. Yes you have to create it name the class in it productComponents and make it extend sfComponents, then paste the component I’ve written executeShowImages.
3. The view is stored in a partial. It’s place is in backend/modules/product/templates/_showImages.php
Hope this helps
you are the best!
thank you
I’d love to see a final effect, but “Widget ‘image’ does not exist” alert drives me crazy. It shows up after clicking any action (new,edit).. Any suggestions?
There is a problem in SF_ROOT_DIR\cache\backend\dev\modules\autoProduct\templates\_form_field.php in line 6..
Byt the way, I’m particularly interested in ajax based tutorials, drag&drop scripts … of course in connection with symfony. If you have some interesting sources, thanks for sharing!
Ps. język polski mile widziany, chciałem utrzymać międzynarodowy charakter bloga,
Co do powyższego błędu, możliwe, że coś pominąłem, dopiero zaczynam przygodę z symfony. Niemniej jednak utworzyłem wszystkie wymagane pliki, uzupełniłem bazę i wgrałem kilka zdjęć podając uprzednio ścieżkę w pliku app.yml
First of all try clearing the cache : symfony cc
Then check if you have done everything corectly. The ‘image’ widget is defined in BackendEmbedImageForm so it’s on its place.
Hope you get it going soon, cheers
interesting info, thanks