Sonata Project    AdminBundle
Who am I ?• Thomas Rabaix• Speaker at Symfony Live Conferences• Author of many symfony1 plugins• lead developer of the son...
Talk• Sonata Project presentation• Quick Tour• Under the hood• Customize / Advanced features• Conclusion
Sonata Project• A not so young project• Based on symfony1 plugins• Recoded with the best practices of  Symfony2• Built on ...
Sonata Project• An ecommerce toolbox• How :   • avoiding reinvented the wheel   • contribution to the community   • built ...
Sonata’s bundles• PageBundle : a page manager with block  as service and strong caching mechanism• MediaBundle : a media m...
http://sonata-project.org
AdminBundle   why ?
• No admin generator for Symfony 2.0• Frustrating by the admin generator provided by  symfony1• Admin is not only about Mo...
Quick Tour http://www.flickr.com/photos/38104873@N03/4559985343/
Admin Class•   An metadata description    of CRUD operations•   No code generation•   Based on Symfony    services + Sonat...
Dashboard        Actions       shortcut       Group +        Model
DashboardRegister admin class with the tag “sonata.admin”  And admin will appears into the dashboard1.         <services>2...
Breadcrumb             List Action                                          Model                                         ...
List Action fields, custom        fields, typetemplates, type     detection, based   detection           on Form            ...
List Action1.     protected function configureDatagridFilters(DatagridMapper $datagridMapper) {2.                 $datagri...
Edit/Create Formside menu                      Field Group
Edit/Create Form                        Helper message                                add relation                        ...
Edit/Create Form1.          protected function configureFormFields(FormMapper $formMapper)2.          {3.              $te...
Other Features• Permissions management• Flash messages• Nested Admin• Command lines utilities• Translated into more than10...
Quick Tour Summary• Dashboard• Consistent Interface across bundles• Easy to configure, but powerful for  advanced users• Ad...
Under the hood   http://www.flickr.com/photos/52251564@N08/5937620090
Admin Class Dependencies                    Security              Builder                                                 ...
Security• Based on the SecurityHandlerInterface• 2 built-in implementations   • NoopSecurityHandler : use the      Symfony...
Security          • Admin Usage1.     protected function configureFormFields(FormMapper $formMapper)2.     {3.         $fo...
Security : ACL• Required to have a custom external bundle to  manage permissions and user : see FOS/  UserBundle and Sonat...
Routing          • Definition set from the Admin class          • Can be tweaked by the configureRoute        1.         /**...
Admin Model Manager• Persistency layer abstraction for Admin Bundle.• All Persistencies actions are done in the Model  Man...
Admin Model Manager• Some bundle provides custom Model Manager• You can define your own proxy Admin Model  Manager to reuse...
Model Manager                              1. class AdminModelManager extends ModelManager {                              ...
Translator• 2 catalogues   • SonataAdminBundle used to translate     shared messages   • messages used to translate curren...
Form• Originally built on the the first implementation• Too bad ..... major refactoring 3 months later   • Some admin featu...
Form Mapper• Interact with the FormMapper• Proxy class between the Admin Class and the  Symfony Form Component• Act as the...
Core types                                   Form Type                                                Model Manager Types ...
Validator• Assert rules can be defined in the validation.  [xml|yml] files (validator component)  nothing new ...• Condition...
Conditional Validation• Not mandatory, but allow to add inline validation  a runtime.    • ex : check only if a value is s...
Validator1.     /**2.        * @param SonataAdminBundleValidatorErrorElement $errorElement3.        * @param $object4.    ...
Menu• Based on KnpMenu lib• Used for Breadcrumb and Side Menu• No magic for sidemenu, need to code your  own menu per admi...
Menu1. class PostAdmin extends Admin2. {3.     protected function configureSideMenu(MenuItemInterface $menu, $action, Admi...
customizehttp://www.flickr.com/photos/7552532@N07/449769140/
Form : one-to-many• Type : sonata_type_collection (CollectionType)• Form Option       by_reference => false• Sonata Option...
Form : many-to-many• Type : sonata_type_model (ModelType)• Sonata Options     • no options
Form : many-to-one• Type : sonata_type_model (ModelType)• Form Options :     • expanded : true|false• Sonata Options     •...
Form : many-to-one• Type : sonata_type_admin (AdminType)• Embed an admin form for an entity into the current  admin• Sonat...
Form Theme• Based on the form theme mechanism• Define a getFormTheme()   • a set of form templates   • allows to customize ...
Form Theme•   Just define a custom block with the correct name ...•   How to know the block name ... ?•   Provide a custom ...
Form block  •       Theme definition1.        protected $formTheme = array(2.            SonataAdminBundle:Form:form_admin_...
Custom List Template •   Field definition1.->add(custom, string, array(template =>  SonataMediaBundle:MediaAdmin:list_custo...
CRUD Controller• Use the admin class to generate required  objects• contains create, edit, update, delete and  batch actio...
Custom CRUD Controller1. namespace SonataMediaBundleController;2.  3. use SonataAdminBundleControllerCRUDController as Con...
Nested Admin• clean url : /admin/sonata/news/post/1/comment/list• reuse other admin definition• autofilter with the targeted...
Nested Admin1. class CommentAdmin extends Admin2. {3.     protected $parentAssociationMapping = post;4.  5.     protected ...
Nested AdminComment Admin   Nested Comment Admin
Admin Extension• Allow to add extra feature or redefine some field to  the admin• Good entry point if you extends some entit...
Debug•   Check out external configurations      •   validator configuration      •   doctrine schema definition      •   use ...
sonata:admin:list service name can be used toexplain the admin configuration
sonata:admin:explain
conclusion http://www.flickr.com/photos/rv-bordeaux/5909437215/
small numbers•   First commit in    november 2010•   650 commits•   49 Contributors!•   14 Translations           Thanks!
What’s next ?• Version 1   • Stabilize the AdminBundle   • Add more tests... : 56 tests, 145      assertions• Version 1.1 ...
Resources• http://sonata-project.org• irc : #symfony• twitter : @sonataproject• symfony google groups
Questions http://www.flickr.com/photos/colinkinner/2200500024/
Upcoming SlideShare
Loading in...5
×

sfDay Cologne - Sonata Admin Bundle

6,672

Published on

Published in: Technology, Business
1 Comment
10 Likes
Statistics
Notes
No Downloads
Views
Total Views
6,672
On Slideshare
0
From Embeds
0
Number of Embeds
6
Actions
Shares
0
Downloads
0
Comments
1
Likes
10
Embeds 0
No embeds

No notes for slide

sfDay Cologne - Sonata Admin Bundle

  1. 1. Sonata Project AdminBundle
  2. 2. Who am I ?• Thomas Rabaix• Speaker at Symfony Live Conferences• Author of many symfony1 plugins• lead developer of the sonata project• Working at Ekino, a french web agency
  3. 3. Talk• Sonata Project presentation• Quick Tour• Under the hood• Customize / Advanced features• Conclusion
  4. 4. Sonata Project• A not so young project• Based on symfony1 plugins• Recoded with the best practices of Symfony2• Built on top on very strong and powerful framework
  5. 5. Sonata Project• An ecommerce toolbox• How : • avoiding reinvented the wheel • contribution to the community • built on top of a strong framework
  6. 6. Sonata’s bundles• PageBundle : a page manager with block as service and strong caching mechanism• MediaBundle : a media manager on steroid, you don’t have to worry about managing files or videos• UserBundle, IntlBundle, etc ...• AdminBundle : A backend generator
  7. 7. http://sonata-project.org
  8. 8. AdminBundle why ?
  9. 9. • No admin generator for Symfony 2.0• Frustrating by the admin generator provided by symfony1• Admin is not only about Model; but about providing a consistent and rich user experience for managing data.
  10. 10. Quick Tour http://www.flickr.com/photos/38104873@N03/4559985343/
  11. 11. Admin Class• An metadata description of CRUD operations• No code generation• Based on Symfony services + Sonata Admin services
  12. 12. Dashboard Actions shortcut Group + Model
  13. 13. DashboardRegister admin class with the tag “sonata.admin” And admin will appears into the dashboard1.     <services>2.         <service id="sonata.news.admin.comment" class="%sonata.news.admin.comment.class%">3.             <tag name="sonata.admin" manager_type="orm" group="sonata_blog" label="comment"/>4.             <argument />5.             <argument>%sonata.news.admin.comment.entity%</argument>6.             <argument>%sonata.news.admin.comment.controller%</argument>7.         </service>8.  9.         <service id="sonata.news.admin.post" class="%sonata.news.admin.post.class%">10.            <tag name="sonata.admin" manager_type="orm" group="sonata_blog" label="post"/>11.            <argument />12.            <argument>%sonata.news.admin.post.entity%</argument>13.            <argument>%sonata.news.admin.post.controller%</argument>14.        </service>15. 16.        <service id="sonata.news.admin.tag" class="%sonata.news.admin.tag.class%">17.            <tag name="sonata.admin" manager_type="orm" group="sonata_blog" label="tag"/>18.            <argument />19.            <argument>%sonata.news.admin.tag.entity%</argument>20.            <argument>%sonata.news.admin.tag.controller%</argument>21.        </service>22.    </services>
  14. 14. Breadcrumb List Action Model Actions Batch Actions Filters
  15. 15. List Action fields, custom fields, typetemplates, type detection, based detection on Form Component
  16. 16. List Action1.     protected function configureDatagridFilters(DatagridMapper $datagridMapper) {2.         $datagridMapper3.  4.  5.                         ->add(name)     ->add(providerReference)     ->add(enabled) field guesser6.             ->add(context)7.         ;8.         $providers = array();9.  10.        foreach($this->pool->getProviderNamesByContext(default) as $name) {11.            $providers[$name] = $name;12.        }13. 14.        $datagridMapper->add(providerName, doctrine_orm_choice, array(15.            field_options=> array(16.                choices => $providers,17. 18. 19.                            required => false,         multiple => false,         expanded => false, custom filter20.            ),21.            field_type=> choice,22.        ));23.    } edit link24. 25.    protected function configureListFields(ListMapper $listMapper) {26.        $listMapper27.            ->addIdentifier(id)28.            ->add(image, string, array(template => SonataMediaBundle:MediaAdmin:list_image.html.twig))29.            ->add(custom, string, array(template => SonataMediaBundle:MediaAdmin:list_custom.html.twig))30.            ->add(enabled)31. 32.              ->add(_action, actions, array(             actions => array( custom template33.                    view => array(),34.                    edit => array(),35.                )36.            ))37. 38.          ; } row’s actions
  17. 17. Edit/Create Formside menu Field Group
  18. 18. Edit/Create Form Helper message add relation hide fieldssave options delete action
  19. 19. Edit/Create Form1.     protected function configureFormFields(FormMapper $formMapper)2.     {3.         $templates = array();4.         foreach ($this->cmsManager->getPageManager()->getTemplates() as $code => $template) {5.             $templates[$code] = $template->getName();6.         } create group7.  8.         $formMapper9.             ->with($this->trans(form_page.group_main_label))10.                 ->add(name)11.                 ->add(enabled, null, array(required => false))12.                 ->add(position)13.                 ->add(templateCode, choice, array(required => true, choices => $templates))14.                 ->add(parent, sonata_page_selector, array(15.                     page          => $this->getSubject() ?: null,16.                     model_manager => $this->getModelManager(),17.                     class         => $this->getClass(),18.  19.                           filter_choice => array(hierarchy => root),             required      => false Form Component20.                 ))21.             ->end()22.         ;23.  24.         $formMapper25.             ->with($this->trans(form_page.group_seo_label), array(collapsed => true))26.                 ->add(metaKeyword, textarea, array(required => false))27.                 ->add(metaDescription, textarea, array(required => false))28.             ->end() group options29.         ;30.  31.         $formMapper32.             ->with($this->trans(form_page.group_advanced_label), array(collapsed => true))33.                 ->add(javascript, null,  array(required => false))34.                 ->add(stylesheet, null, array(required => false))35.                 ->add(rawHeaders, null, array(required => false))36.             ->end()37.         ;38.  39.         $formMapper->setHelps(array(40.             name => $this->trans(help_page_name)41.  42.           )); } Define help messages
  20. 20. Other Features• Permissions management• Flash messages• Nested Admin• Command lines utilities• Translated into more than10 languages
  21. 21. Quick Tour Summary• Dashboard• Consistent Interface across bundles• Easy to configure, but powerful for advanced users• Advanced features• Inspired from the django admin module (user interactions)
  22. 22. Under the hood http://www.flickr.com/photos/52251564@N08/5937620090
  23. 23. Admin Class Dependencies Security Builder Sonata Admin Bundle ListSymfony Framework Translator Datagrid Admin Show Routing class Form Validator Model Manager Form
  24. 24. Security• Based on the SecurityHandlerInterface• 2 built-in implementations • NoopSecurityHandler : use the Symfony’s firewall • AclSecurityHandler : based on ACL - Advanced users only
  25. 25. Security • Admin Usage1.     protected function configureFormFields(FormMapper $formMapper)2.     {3.         $formMapper4.             ->with(General)5.                 ->add(enabled, null, array(required => false))6.                 ->add(author, sonata_type_model, array(), array(edit => list))7.                 ->add(title)8.             ->end()9.         ;10.       11.        if (!$this->isGranted(CREATE)) {12.            // do specific code if the user cannot create a new object13.        }14.    } • Template Usage 1. {% if admin.isGranted(CREATE) %} 2.    // DO YOUR STUFF 3. {% endif %}
  26. 26. Security : ACL• Required to have a custom external bundle to manage permissions and user : see FOS/ UserBundle and Sonata/UserBundle• Built on top of a custom MaskBuilder (basic roles : List,View, Edit, Create, Delete)• Command lines : • php app/console init:acl • php app/console sonata:admin:setup-acl
  27. 27. Routing • Definition set from the Admin class • Can be tweaked by the configureRoute 1.     /** 2.      * @param SonataAdminBundleRouteRouteCollection $collection 3.      * @return void 4.      */ 5.     protected function configureRoutes(RouteCollection $collection) 6.     { 7.         $collection->add(snapshots); Add the id parameter 8.         $collection->remove(edit); 9. 10.        $collection->add(test, $this->getRouterIdParameter()./test); 11.    } • Template Usage1. <a href="{{ admin.generateUrl(view, { id : media.id, format : reference}) }}">reference</a>2. <a href="{{ admin.generateObjectUrl(media, view, {format : reference}) }}">reference</a>
  28. 28. Admin Model Manager• Persistency layer abstraction for Admin Bundle.• All Persistencies actions are done in the Model Manager • delete, query, pagination, etc.. • form type manipulation delegation• For now only Doctrine ORM (Propel and Doctrine ODM are work in progress by external contributors)
  29. 29. Admin Model Manager• Some bundle provides custom Model Manager• You can define your own proxy Admin Model Manager to reuse the Bundle Model Manager• Use case : Sonata Media Bundle (delete action)
  30. 30. Model Manager 1. class AdminModelManager extends ModelManager { 2.     protected $manager;Sonata Media Bundle 3.   4.     public function __construct($entityManager, $manager) { 5.         parent::__construct($entityManager); 2 Model Managers : 6.         $this->manager = $manager; - entity : deal with deletion and so on ... 7.     } 8.   - admin : proxy some methods to the entity 9.     public function delete($object) { 10.         $this->manager->delete($object); 11.     } How to ? 12. } 1. create a dedicated Admin Model Manager 1. class BundleMediaManager extends AbstractMediaManager {   2. create a Bundle Model Manager 1. public function delete(MediaInterface $media) { 2.         $this->pool 3. redefine only required methods 3. ->getProvider($media->getProviderName()) 4. define services 4. ->preRemove($media); 5.         $this->em->remove($media); 6.         $this->em->flush(); Delete thumbnails 7.   8.         $this->pool 9. ->getProvider($media->getProviderName()) 10. ->postRemove($media); 11.         $this->em->flush(); 12.     } 2. } 1.       <service id="sonata.media.admin.media" class="SonataMediaBundleEntityBundleMediaManager"> 2.             <tag name="sonata.admin" manager_type="orm" group="sonata_media" label="media"/> 3.             <argument /> 4.             <argument>%sonata.media.admin.media.entity%</argument> 5.             <argument>%sonata.media.admin.media.controller%</argument> 6.   7.             <call method="setModelManager"> 8.                 <argument type="service" id="sonata.media.admin.media.manager" /> 9.             </call> 10.         </service> 11.   12.         <service id="sonata.media.admin.media.manager" class="SonataMediaBundleAdminManagerDoctrineModelManager"> 13.             <argument type="service" id="doctrine.orm.default_entity_manager" /> 14.             <argument type="service" id="sonata.media.manager.media" /> 15.         </service>
  31. 31. Translator• 2 catalogues • SonataAdminBundle used to translate shared messages • messages used to translate current Admin 1.         $formMapper 2.             ->with($this->trans(form_page.group_main_label)) 3.                 ->add(name) 4.             ->end() 5.         ;• Can be set by updating the translationDomain property
  32. 32. Form• Originally built on the the first implementation• Too bad ..... major refactoring 3 months later • Some admin features has been removed or still broken :( • But the new form implementation is pretty awesome .... once you know how to use it.
  33. 33. Form Mapper• Interact with the FormMapper• Proxy class between the Admin Class and the Symfony Form Component• Act as the Symfony FormBuilder component• You can use your custom field types
  34. 34. Core types Form Type Model Manager Types Form Types• AdminType : used to embedded form from another Admin class• CollectionType : use by one-to-many association• ModelType : select choice (like EntityType)• ModelReferenceType : handle an model id• ImmutableArrayType : specify a form type per array element 1.         $formMapper->add(settings, sonata_type_immutable_array, array( 2.             keys => array( 3.                 array(layout, textarea, array()), 4.                 array(action, text, array()), 5.                 array(parameters, text, array()), 6.             ) 7.         ));
  35. 35. Validator• Assert rules can be defined in the validation. [xml|yml] files (validator component) nothing new ...• Conditional Validation
  36. 36. Conditional Validation• Not mandatory, but allow to add inline validation a runtime. • ex : check only if a value is set• Validate method inside the admin class• Interact with a new ErrorElement object• The ErrorElement is just a validator service based on the Validator Component
  37. 37. Validator1.   /**2.      * @param SonataAdminBundleValidatorErrorElement $errorElement3.      * @param $object4.      * @return void5.      */6.     public function validate(ErrorElement $errorElement, $object)7.     {8.         $errorElement9.             ->with(name)10.                ->assertMaxLength(array(limit => 32))11.            ->end()12.        ; symfony constraint13.       14.        if ($object->getFoo()) {15.            $errorElement16.                ->with(test)17.                    ->addViolation(my_message)18.                ->end()19.            ;20.        }21.    } custom error
  38. 38. Menu• Based on KnpMenu lib• Used for Breadcrumb and Side Menu• No magic for sidemenu, need to code your own menu per admin (if required)
  39. 39. Menu1. class PostAdmin extends Admin2. {3.     protected function configureSideMenu(MenuItemInterface $menu, $action, Admin $childAdmin = null)4.     {5.         if (!$childAdmin && !in_array($action, array(edit))) {6.             return;7.         }8.  9.         $admin = $this->isChild() ? $this->getParent() : $this;10. 11.        $id = $admin->getRequest()->get(id);12. 13.        $menu->addChild(14.            $this->trans(view_post),15.            array(uri => $admin->generateUrl(edit, array(id => $id)))16.        );17. 18.        $menu->addChild(19.            $this->trans(link_view_comment),20.            array(uri => $admin->generateUrl(sonata.news.admin.comment.list, array(id => $id)))21.        );22.    }23.}
  40. 40. customizehttp://www.flickr.com/photos/7552532@N07/449769140/
  41. 41. Form : one-to-many• Type : sonata_type_collection (CollectionType)• Form Option by_reference => false• Sonata Options • edit : standard / inline • inline : table | list • position : field name (if exists)
  42. 42. Form : many-to-many• Type : sonata_type_model (ModelType)• Sonata Options • no options
  43. 43. Form : many-to-one• Type : sonata_type_model (ModelType)• Form Options : • expanded : true|false• Sonata Options • edit : standard (select box) / list (popup) • link_parameters : add extra link parameters to the link
  44. 44. Form : many-to-one• Type : sonata_type_admin (AdminType)• Embed an admin form for an entity into the current admin• Sonata Options • no option!
  45. 45. Form Theme• Based on the form theme mechanism• Define a getFormTheme() • a set of form templates • allows to customize an admin form
  46. 46. Form Theme• Just define a custom block with the correct name ...• How to know the block name ... ?• Provide a custom `block_name` option• or use a defined built-in pattern : admin_service_id_[type]_[widget|label|errors|row] admin_service_id_[fieldName]_[widget|label|errors|row]
  47. 47. Form block • Theme definition1.    protected $formTheme = array(2.        SonataAdminBundle:Form:form_admin_fields.html.twig,3.        SonataNewsBundle:Form:form.html.twig4.    ); • Block definition1.{% block sonata_user_admin_user_credentialsExpired_text_widget %}2.   PUT HERE THE WIDGET ...3.{% endblock %} • Et voila!
  48. 48. Custom List Template • Field definition1.->add(custom, string, array(template => SonataMediaBundle:MediaAdmin:list_custom.html.twig)) • Template1.{% extends SonataAdminBundle:CRUD:base_list_field.html.twig %}2. 3.{% block field %}4.    <div>5.        <strong>{{ object.name }}</strong> <br />6.        {{ object.providerName|trans({}, SonataMediaBundle) }}: {{ object.width }}x{{ object.height }} <br />7.    </div>8.{% endblock %} • Et voila!
  49. 49. CRUD Controller• Use the admin class to generate required objects• contains create, edit, update, delete and batch actions• can be extended to change some logic• use case : Sonata Media Bundle
  50. 50. Custom CRUD Controller1. namespace SonataMediaBundleController;2.  3. use SonataAdminBundleControllerCRUDController as Controller; Grant check4. use SymfonyComponentSecurityCoreExceptionAccessDeniedException;5.  6. class MediaAdminController extends Controller {7.     public function createAction() {8.         if (false === $this->admin->isGranted(CREATE)) {9.             throw new AccessDeniedException();10.        }11. 12.        $parameters = $this->admin->getPersistentParameters();13. 14.        if (!$parameters[provider]) { Custom Template15.            return $this- >render(SonataMediaBundle:MediaAdmin:select_provider.html.twig, array(16.                providers     => $this->get(sonata.media.pool)17. ->getProvidersByContext($this->get(request)- >get(context, default)),18.                base_template => $this->getBaseTemplate(),19.                admin         => $this->admin,20.                action        => create21.            ));22.        }23. 24.        return parent::createAction();25.    }26.} Parent action
  51. 51. Nested Admin• clean url : /admin/sonata/news/post/1/comment/list• reuse other admin definition• autofilter with the targeted elements• only work on one level• You don’t need to know routing name, as long as you use the admin class
  52. 52. Nested Admin1. class CommentAdmin extends Admin2. {3.     protected $parentAssociationMapping = post;4.  5.     protected function configureFormFields(FormMapper $formMapper)6.     {7.         if(!$this->isChild()) {8.             $formMapper->add(post, sonata_type_model, array(), array(edit => list));9.         }10.  11.         $formMapper12.             ->add(name)13.             ->add(email)14.             ->add(url, null, array(required => false))15.             ->add(message)16.             ->add(status, choice, array(choices => Comment::getStatusList(), expanded => true, multiple => false))17.         ;18.     } Display custom field if the19.  20.     protected function configureListFields(ListMapper $listMapper)21.     {22.         $listMapper23.             ->addIdentifier(name) current admin is nested or not24.             ->add(getStatusCode, text, array(label => status_code, sortable => status))25.         ;26.  27.         if (!$this->isChild()) {28.             $listMapper->add(post);29.         }30.  31.         $listMapper32.             ->add(email)33.             ->add(url)34.             ->add(message);35.     }36. }
  53. 53. Nested AdminComment Admin Nested Comment Admin
  54. 54. Admin Extension• Allow to add extra feature or redefine some field to the admin• Good entry point if you extends some entities• Add extension must implement the AdminExtensionInterface and define as service with the sonata.admin.extension
  55. 55. Debug• Check out external configurations • validator configuration • doctrine schema definition • use firebug to check Ajax errors (missing toString method or type hinting)• Get information from the Admin command tools • sonata:admin:list • sonata:admin:explain
  56. 56. sonata:admin:list service name can be used toexplain the admin configuration
  57. 57. sonata:admin:explain
  58. 58. conclusion http://www.flickr.com/photos/rv-bordeaux/5909437215/
  59. 59. small numbers• First commit in november 2010• 650 commits• 49 Contributors!• 14 Translations Thanks!
  60. 60. What’s next ?• Version 1 • Stabilize the AdminBundle • Add more tests... : 56 tests, 145 assertions• Version 1.1 • Add missing features from the original form factoring • Add more layer persistency (contribution)
  61. 61. Resources• http://sonata-project.org• irc : #symfony• twitter : @sonataproject• symfony google groups
  62. 62. Questions http://www.flickr.com/photos/colinkinner/2200500024/

×