Symfony2: How to Dynamically Modify Forms Using Form Events, the missing parts

The tutorial in Symfony2’s CookBook on how to dynamically modify your form using form events is easy to follow and implement, apart from two things:

1. What goes inside the customizeForm() method, to dynamically add the field to the form?

If you’re following the 3rd example of the CookBook recipie on how to modify your form, you might be wondering what to put in the customiseForm method of your form event listener. The answer is hinted at in the 2nd example, here.

In my form, there are two entity-driven choice widgets. When a selection is made on the first, an ajax request is sent to a controller action and the second choice widget is populated with entity objects that relate to the above chosen entity. So, my customizeForm method looks like this:

I pass in the first selected object, and use it in a repository query to find the relevant entities to load in the second choice widget.

2. How can do I handle an old selection in the second choice widget if the user changes their selection in the first choice widget?

At this stage, you should be able to make a choice in the first widget, then select a related choice in the second. A problem crops up, however, if you go on to change your selection in the first widget again. Your old selection in the second choice widget is submitted with the form and a validation error occurs. You have two options, you can either clear the selected value in the second widget using JavaScript, or you can handle it in the controller that deals with your ajax request:

Here, the old value in the second choice widget is cleared (set to ”) before the form is bound and will no longer be the cause of the form failing validation (your form validation may still fail, but it will be due to invalid data in other fields).

Now, everything should be working fine regardless how many times you change your selection in the first choice widget.

Symfony2: Including charts with Liuggio/ExcelBundle

I am working on a Symfony2 web app that outputs a crosstab report to Excel using Liuggio’s ExcelBundle (a wrapper for PHPExcel).

I wanted to include a chart or two in the Excel file and needed a way to call the setIncludeCharts() method in vendor/CodePlex/PHPExcel/PHPExcel/Writer/Excel2007.php passing ‘true’. Without this, the chart is not included in the outputted Excel file.

I couldn’t find a way to call this method directly, so instead I found a way to do it using ExcelBundle’s StreamWriterWrapper class (vendor/irongit/symfony2-stream-response/n3b/Bundle/Util/HttpFoundation/StreamResponse/StreamWriterWrapper.php).

StreamWriterWrapper has a setWriter() method that takes a writer object and the name of the method you want to call. By default the StreamWriterWrapper is primed to call the ‘save’ method on the given writer object (an instance of vendor/CodePlex/PHPExcel/PHPExcel/Writer/Excel2007.php in my case).

You can use StreamWriterWrapper’s setWriter() method in conjunction with it’s write() method to call methods on the writer object. Any arguments provided in the write() method are passed to the method specified in setWriter(). For example:


// create and setup the chart here - see some good examples here 
// https://github.com/PHPOffice/PHPExcel/blob/develop/Examples/33chartcreate-bar-stacked.php

// obtain a writer object
$objWriter = PHPExcel_IOFactory::createWriter($this->excel->excelObj, 'Excel2007');

// $this->excel holds an instance of the Liuggio Excel service
$this->excel->getStreamWriter()->setWriter($objWriter, 'setIncludeCharts');
$this->excel->getStreamWriter()->write(true);

// now, the writer will include charts in the outputted Excel file
// we need to set the writer's next method call back to 'save'

$this->excel->getStreamWriter()->setWriter($objWriter, 'save');

// obtain the first sheet in the Excel file
$this->excel->excelObj->setActiveSheetIndex(0);

// finally, prepare the Symfony2 response
$response = $this->excel->getResponse();

$response->headers->set(
    'Content-Type',
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf-8'
);
        
$response->headers->set(
    'Content-Disposition',
    'attachment;filename=test.xls'
);

return $response;

Your excel file will now include any charts that you created.

Override display of sonata_type_collection form in specific admin classes

It is possible to customise how a collection is edited in Sonata Admin Bundle on a collection-by-collection basis (ie, you can target a specific collection in a form, which is handy if your form has several related collections), furthermore, the customisation is only displayed on the specific parent form to which you apply it (the default sonata collection form is displayed in any other parent forms that you might want to edit the same collection from).

As an example, I have a GrantAdmin.php admin class, whose configureFormFields() method defines various Grant properties that should be edited, along with a sonata_collection_type for related Report objects and another sonata_type_collection for related BudgetYear objects:

// Grants/AdminBundle/Admin/GrantAdmin.php

protected function configureFormFields(FormMapper $formMapper)
{

    $formMapper
            ->with('Details')
            ->add('title')
            //...more properties here
            ->with('Budget', array('collapsed' => true))
            ->add(
                'budgetYears',
                'sonata_type_collection',
                array(
                    'by_reference' => false,
                    'label' => " ",
                    'required' => false
                ),
                array(
                    'edit' => 'inline',
                    'inline' => 'table',
                )
            )->with('Reports', array('collapsed' => true))
            ->add(
                'reports',
                'sonata_type_collection',
                array(
                    'by_reference' => false,
                    'label' => " ",
                    'required' => false
                ),
                array(
                    'edit' => 'inline',
                    'inline' => 'standard'
                )
            )
            ->end();
}

// Grants/AdminBundle/Admin/ReportAdmin.php
 protected function configureFormFields(FormMapper $formMapper)
{
    $formMapper
        ->add(
            'reportType',
            'sonata_type_model_list'
        )
        ->add('instruction')
        ->add('amount')
        ->add('invoiceNumber')
        ->add('grant')
        ->add(
            'dueDate',
            'date',
            array(
                'input' => 'datetime',
                'format' => 'dd-MM-yyyy',
                'empty_value' => ''
            )
        )
        ->add('isComplete', null)
        ->add('link');
}

// Grants/AdminBundle/Admin/BudgetYearAdmin.php

protected function configureFormFields(FormMapper $formMapper)
{
    $formMapper
        ->add('year')
        ->add('startDate')
        ->add('endDate')        
        ->add(
            'isActive',
            null
        )
            ->add(
                 'budgets',
                 'sonata_type_collection',
                 array(
                     'by_reference' => false,
                     'label' => " ",
                     'required' => false
                 ),
                 array(
                     'edit' => 'inline',
                     'inline' => 'table',
                 )
             );
}

I want to be able to edit report objects within the GrantAdmin form, so I am forced to use ‘edit’ => ‘inline’ inside the sonata_type_collection definition. However, there are too many fields defined in the ReportAdmin class form definition to display in an inline table within the GrantAdmin form, so I am forced to use ‘inline’ => ‘standard’. This outputs each Report form in the collection in an unorganised muddle so I want to override this and customise the display.

To achieve this, I created a template in my admin bundle:

//Grants/AdminBundle/Resources/views/Form/form_admin_fields.twig

{% use 'SonataAdminBundle:Form:form_admin_fields.html.twig' %}

{% block grants_grant_admin_grant_reports_sonata_type_collection_widget %}

    // copy, paste then edit Sonata/DoctrineORMAdminBundle/Resources/views/CRUD/edit_orm_one_to_many.html.twig

{% endblock %}

The name of the block in my template is derived from a combination of my GrantAdmin class’s service name, the name of the collection within the configureFormFields method and “sonata_type_collection_widget” added on at the end:

// app/config/config.yml

services:
    grants.grant.admin.grant:  // with('Reports', array('collapsed' => true))
            ->add(
                'reports',  // " ",
                    'required' => false
                ),
                array(
                    'edit' => 'inline',
                    'inline' => 'standard'
                )
            )
            ->end();
}

// added together, makes up the block name:

{% block grants_grant_admin_grant_reports_sonata_type_collection_widget %}

It is also possible to keep display of the default form but add something extra via your custom template:

//Grants/AdminBundle/Resources/views/Form/form_admin_fields.twig

{% use 'SonataAdminBundle:Form:form_admin_fields.html.twig' %}

{% block grants_grant_admin_grant_reports_sonata_type_collection_widget %}
    
    {{  block('sonata_type_collection_widget') }}

    // add your something extra here

{% endblock %}

A final challenge for me was to display, within the GrantAdmin parent form, the ‘budgetYears’ collection but without the ‘budgets’ collection defined in the BudgetYears class configureFormFields method. This is essential, as it is not possible to edit a collection within a collection in Sonata Admin Bundle at present.

To achieve this, I simply added another block to my custom template:

//Grants/AdminBundle/Resources/views/Form/form_admin_fields.twig

{% use 'SonataAdminBundle:Form:form_admin_fields.html.twig' %}

{% block grants_budget_admin_budgetyear_budgets_sonata_type_collection_widget %}

    console.log('Suppressing display of Budget collection form within BudgetYears collection form inside the GrantAdmin parent form');

{% endblock %}

{% block grants_grant_admin_grant_reports_sonata_type_collection_widget %}
    
    {{  block('sonata_type_collection_widget') }}

    // add your something extra here

{% endblock %}

The name of the additional block in my custom template was derived in the same way as before.

With the above addition, my GrantAdmin form renders the ‘budgetYear’ collection form, but not the ‘budgets’ collection defined inside the BudgetYears class.

To add budgets to my BudgetYear entities, I simply display a separate BudgetYear parent form, and sonata’s default collection form for ‘budget’ is displayed, as I have only overridden it in my GrantsAdmin form.

Doctrine2 object introspection, an alternative to var_dump()

Using var_dump() on a Doctrine2 object or collection is very slow and may even cause your browser to freeze.

Doctrine provides a dump method which produces less verbose, more human-readable output:

DoctrineCommonUtilDebug::dump($entityObject);

You may prefer to declare the class in a use statement:

use DoctrineCommonUtilDebug

Then call the dump method inside your class like this:

Debug::dump($entityObject);

Display a subset or sorted list of relations in Sonata Admin Bundle

Here, we provide a collection of ‘active’ Status objects, sorted by name ascending, to a sonata_type_model form type:

// src/Acme/AdminBundle/Admin/ArticleAdmin.php

protected function configureFormFields(FormMapper $formMapper)
{

    $statusQuery = $this->modelManager

        ->getEntityManager('AcmeMyBundleEntityStatus')
        ->createQuery(
            'SELECT s
             FROM AcmeMyBundle:STATUS s
             WHERE s.isActive=1
             ORDER BY s.name ASC'
        );

    $formMapper
            ->with('Details')
            ->add('title')       
            ->add(
                'status',
                'sonata_type_model',
                array(
                    'class' => 'AcmeMyBundleEntityStatus',
                    'property' => 'name',
                    'query' => $statusQuery
                )
            )
            ->end();
}