Adding an Image Preview On Edit


In our untested approach we now have both Create and Update for Wallpapers in a working state. 'Working', however, is not necessarily finished. It would be super nice to improve upon our Update journey in order to display the existing updated Wallpaper image file when visiting the Update screen.

In order to do this we will need to continue customising the custom_file_widget that we created a couple of videos ago.

Here's what we have currently:

<!-- /app/Resources/views/Form/fields.html.twig -->

{% block custom_file_widget %}
    {% spaceless %}
        <div class="custom-file">
            {{ form_widget(form.file) }}
        </div>
    {% endspaceless %}
{% endblock %}

There are likely multiple ways to approach this problem. The way we are going to do this is to make use of the buildView method on our CustomFileType.

For the longest time I managed to get by without using more than just the buildView and configureOptions (previously setDefaultOptions) methods of my forms. I might sprinkle in a getBlockPrefix, or getParent as needed, but I never used buildView or finishView.

The thing that I found is that I didn't quite understand what these methods were for, so I avoided them.

I don't want you to feel the same way. First, we are going to figure out where they come from. Then we are going to use buildView specifically to help us accomplish our goal.

Let's start by examining the class definition for our CustomFileType:

<?php

// src/AppBundle/Form/Type/CustomFileType.php

namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;

class CustomFileType extends AbstractType
{

I've removed a bunch of noise from this file to make it more obvious as to what's happening here.

We have declared our class CustomFileType as extendsing AbstractType.

In extending AbstractType we inherit all of its functionality.

When first learning about object oriented programming, I found what happens next to be quite confusing. If we look at the AbstractType class, this confusion may become more evident:

<?php

// note this is correct as of Symfony 3.3.x
// /vendor/symfony/symfony/src/Symfony/Component/Form/AbstractType.php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Form;

use Symfony\Component\Form\Util\StringUtil;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
abstract class AbstractType implements FormTypeInterface
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function buildView(FormView $view, FormInterface $form, array $options)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function finishView(FormView $view, FormInterface $form, array $options)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return StringUtil::fqcnToBlockPrefix(get_class($this));
    }

    /**
     * {@inheritdoc}
     */
    public function getParent()
    {
        return 'Symfony\Component\Form\Extension\Core\Type\FormType';
    }
}

We extend AbstractType, an abstract class. An abstract class is a class we cannot call new on directly.

AbstractType has a bunch of public methods defined, but with the exception of getBlockPrefix, and getParent, they are all empty.

What confused me more was the use of {@inheritdoc}. This documentation might be useful to me, but I wasn't sure where to find it. The key here is to keep following the information in the class definition.

In this case:

abstract class AbstractType implements FormTypeInterface

We should continue our search inside the FormTypeInterface. But if we look at the two use statements, neither matches FormTypeInterface. In PHP we can omit the use statement for anything that exists in the same namespace as our current file.

In other words the file FormTypeInterface.php exists in the same directory:

/vendor/symfony/symfony/src/Symfony/Component/Form/

as AbstractType.php, and when opening the FormTypeInterface interface file, the namespace definition is the same as AbstractType:

<?php

// note this is correct as of Symfony 3.3.x
// /vendor/symfony/symfony/src/Symfony/Component/Form/FormTypeInterface.php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Form;

use Symfony\Component\OptionsResolver\OptionsResolver;

/**
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
interface FormTypeInterface
{
    /**
     * Builds the form.
     *
     * This method is called for each type in the hierarchy starting from the
     * top most type. Type extensions can further modify the form.
     *
     * @see FormTypeExtensionInterface::buildForm()
     *
     * @param FormBuilderInterface $builder The form builder
     * @param array                $options The options
     */
    public function buildForm(FormBuilderInterface $builder, array $options);

    /**
     * Builds the form view.
     *
     * This method is called for each type in the hierarchy starting from the
     * top most type. Type extensions can further modify the view.
     *
     * A view of a form is built before the views of the child forms are built.
     * This means that you cannot access child views in this method. If you need
     * to do so, move your logic to {@link finishView()} instead.
     *
     * @see FormTypeExtensionInterface::buildView()
     *
     * @param FormView      $view    The view
     * @param FormInterface $form    The form
     * @param array         $options The options
     */
    public function buildView(FormView $view, FormInterface $form, array $options);

    /**
     * Finishes the form view.
     *
     * This method gets called for each type in the hierarchy starting from the
     * top most type. Type extensions can further modify the view.
     *
     * When this method is called, views of the form's children have already
     * been built and finished and can be accessed. You should only implement
     * such logic in this method that actually accesses child views. For everything
     * else you are recommended to implement {@link buildView()} instead.
     *
     * @see FormTypeExtensionInterface::finishView()
     *
     * @param FormView      $view    The view
     * @param FormInterface $form    The form
     * @param array         $options The options
     */
    public function finishView(FormView $view, FormInterface $form, array $options);

    /**
     * Configures the options for this type.
     *
     * @param OptionsResolver $resolver The resolver for the options
     */
    public function configureOptions(OptionsResolver $resolver);

    /**
     * Returns the prefix of the template block name for this type.
     *
     * The block prefix defaults to the underscored short class name with
     * the "Type" suffix removed (e.g. "UserProfileType" => "user_profile").
     *
     * @return string The prefix of the template block name
     */
    public function getBlockPrefix();

    /**
     * Returns the name of the parent type.
     *
     * @return string|null The name of the parent type if any, null otherwise
     */
    public function getParent();
}

The interface tells us lots of useful information, even if some of it is quite cryptic.

As mentioned above, the methods that seem most likely to help us in our efforts to expand on our form output / display are those methods related to the View.

Throughout all the time I've worked with Symfony, I cannot remember a time I have needed to use the finishView method. I guess this says the forms I make are not super complex. To me, this is a good thing. Complexity is something I try to avoid at all costs.

Let's start by updating our custom_form_widget to approximate what we are trying to achieve:

<!-- /app/Resources/views/Form/fields.html.twig -->

{% block custom_file_widget %}
    {% spaceless %}
        <div class="custom-file">
            {{ form_widget(form.file) }}

            <!-- new stuff below -->
            {% if has an image file %}
                <img src="{{ path_to_image_file }}" class="img img-responsive img-preview thumbnail"/>
            {% endif %}

        </div>
    {% endspaceless %}
{% endblock %}

Seems fairly reasonable, right?

We need to take into consideration that we're using this same custom form widget for both Creating, and Updating wallpapers. If we are in the 'Create' form, we won't have an existing image to display. We need to ensure this small detail doesn't accidentally completely stop our form from being able to display :)

However, if we do have an existing image file associated with this wallpaper then cool, let's display it. We will wrap the image in some Bootstrap CSS classes to make it look nice and be responsive.

Our first order of business would therefore seem to be checking if we are in a 'Create' or 'Update' situation.

To do this, we will override the buildView method on our CustomFileType:

<?php

// src/AppBundle/Form/Type/CustomFileType.php

namespace AppBundle\Form\Type;

use AppBundle\Entity\Wallpaper;
use AppBundle\Form\DataTransformer\FileTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;

class CustomFileType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('file', FileType::class);

        $builder->addModelTransformer(new FileTransformer());
    }

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'custom_file';
    }
}

Note here the inclusion of the extra two use statements for FormView, and FormInterface.

At this stage it's perhaps not so obvious what to do next. Here's what I do when faced with a problem like this. I dump out what I have access too:

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        dump('build view');
        dump($view);
        dump($form);
        dump($options);
    }

Then all we need to do is to visit the form in our browser and see what Symfony dumps out for us.

Be sure to do this step for both the 'Create' and 'Update' forms, and look at how the available data differs.

When looking through this data the most interesting available piece for this particular instance is the ability to access the object - the Wallpaper entity - that is being used as the form's data. This can be found via the parent property of the $form variable. Under parent we have access to the modelData, viewData, and normData.

Regardless of whether you are in the 'Create' or 'Update' forms, both views should have access to the parent, and in both cases the underlying entity should be a Wallpaper instance. The difference between 'Create' and 'Update' would be whether the Wallpaper object has any of its properties set, or not.

This is good. But we still need to access this inside our buildView method:

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        dump('build view');
        dump($view);
        dump($form);
        dump($options);

        $entity = $form->getParent()->getData();
    }

Seems reasonable, right?

You may be wondering why, if we have modelData, viewData, and normData, that we are calling the more generic getData() method.

By calling getData we are asking for the data as it is represented in our model. I like to think of this as our data as we expect our data to be, not in any transformed or altered state. Again, if unsure use dump statements to view the output of all three method calls.

Having viewed both the 'Create' and 'Update' forms and seen the output of the dump statements, we know another fact: the $entity variable is going to hold a Wallpaper instance. But that depending on which form we are in, the $entity will either have, or not have its properties set.

We primarily care about when the property - specifically the filename property - is set. However, as covered above, we must also provide code to cover what happens when this filename property is not set.

Looking again at our twig template:

<!-- /app/Resources/views/Form/fields.html.twig -->

{% block custom_file_widget %}
    {% spaceless %}
        <div class="custom-file">
            {{ form_widget(form.file) }}

            <!-- new stuff below -->
            {% if has an image file %}
                <img src="{{ path_to_image_file }}" class="img img-responsive img-preview thumbnail"/>
            {% endif %}

        </div>
    {% endspaceless %}
{% endblock %}

We need to figure out a way to work with the if conditional.

Somehow we need to set some data that this template can access to evaluate and run or skip the img tag display accordingly.

To do this we can make use of the View variables.

In order to understand this, let's cover where these things are coming from:

public function buildView(FormView $view, FormInterface $form, array $options)

buildView will be called by Symfony's form component behind the scenes. We don't need to worry about how this is called, just know that it will be, and with these three arguments.

We've already used the FormInterface to get access to the $entity.

Now we can use the $view to set some vars as needed. Before we do, we can look at the FormView class to figure out how this thing works:

<?php

// note this is correct as of Symfony 3.3.x
// /vendor/symfony/symfony/src/Symfony/Component/Form/FormView.php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\Form;

use Symfony\Component\Form\Exception\BadMethodCallException;

/**
 * @author Bernhard Schussek <bschussek@gmail.com>
 */
class FormView implements \ArrayAccess, \IteratorAggregate, \Countable
{
    /**
     * The variables assigned to this view.
     *
     * @var array
     */
    public $vars = array(
        'value' => null,
        'attr' => array(),
    );

    /**
     * The parent view.
     *
     * @var FormView
     */
    public $parent;

    /**
     * The child views.
     *
     * @var FormView[]
     */
    public $children = array();

I've omitted the bulk of this class as we really only care about the part shown.

Note that $vars is a public variable. It is an array. It does not have a getter / setter.

In order to access or update this $vars property we will use a slightly unusual syntax, at least, it's unusual if you haven't seen it before:

$view->vars['our_property_name_here']

This is very similar to how we would call any typical public method on a class. We still use an ->, only now we directly access the property, rather than invoke a function. In other words, it doesn't end in brackets, $view->vars().

Because vars holds an array, we can access and update the vars property just like any other array.

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        $entity = $form->getParent()->getData();

        if ($entity) {

            /**
             * @var Wallpaper $entity
             */
            $view->vars['file_uri'] = ($entity->getFilename() === null)
                ? null
                : '/images/' . $entity->getFilename()
            ;
        }
    }

There's a lot happening here, so let's break it down.

Whether we have a new, or existing Wallpaper we always want a known view variable to be set. This will be our file_uri variable. This is a key that doesn't exist by default in a standard Symfony form, so we shouldn't be overwriting anything that Symfony relies upon.

Next, we use the ternary operator to do an if / else statement in a shorthand format.

First, we check if the outcome of $entity->getFilename() is equal to null.

If it is, then we set the value of $view->vars['file_uri'] to null.

If it is not then we take whatever we get as the outcome from getFilename() and concatenate this with the /images path. This is a bit of a hack at this stage. We will need a better way of coming up with the real image path, but for the moment, let's just try this out. However, before we do, let's move our dump statements down, or we won't see our output:

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        $entity = $form->getParent()->getData();

        if ($entity) {

            /**
             * @var Wallpaper $entity
             */
            $view->vars['file_uri'] = ($entity->getFilename() === null)
                ? null
                : '/images/' . $entity->getFilename()
            ;
        }

        dump('build view');
        dump($view);
        dump($form);
        dump($options);
    }

Again, browse back to both the 'Create' and 'Update' forms and evaluate the output from the dump statements. You should find that we now have 25 variables in our $view->vars array, one of which is the file_uri, which is either null, or /images/some-image-name.ext.

It would be helpful at this point if we could set a default value for the file_uri var, just in case the value cannot be set for any unexpected reason. To do this we can use the configureOptions method:

use Symfony\Component\OptionsResolver\OptionsResolver;

class CustomFileType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'file_uri' => null,
        ]);
    }

    // snip

As the setDefaults method name implies, this is a way we can ensure file_uri will always be set with some known value. There's a lot of power in the OptionsResolver, such as the ability to ensure passed in options are of a certain type, are in a list of preset values, or meet certain formatting needs. There's even more to this component than what I've just described, so would recommend you take a quick look at the docs, as this one is incredibly useful not just with Symfony's form component.

All we need to do now is to feed this information into our twig template's conditional logic:

<!-- /app/Resources/views/Form/fields.html.twig -->

{% block custom_file_widget %}
    {% spaceless %}
        <div class="custom-file">
            {{ form_widget(form.file) }}

            {% if file_uri is defined and file_uri is not null %}
                <img src="{{ file_uri }}" alt="" class="img img-responsive img-preview thumbnail"/>
            {% endif %}

        </div>
    {% endspaceless %}
{% endblock %}

Now, visit the 'Create' and 'Update' forms again, and at last, we have a nice image displayed - but only when Updating. Bonza!

Code For This Course

Get the code for this course.

Episodes

# Title Duration
1 Introduction and Site Demo 02:14
2 Setup and a Basic Wallpaper Gallery 08:43
3 Pagination 08:24
4 Adding a Detail View 04:47
5 Creating a Home Page 11:14
6 Creating our Wallpaper Entity 07:50
7 Wallpaper Setup Command - Part 1 - Symfony Commands As a Service 05:57
8 Wallpaper Setup Command - Part 2 - Injection Is Easy 08:53
9 Wallpaper Setup Command - Part 3 - Doing It With Style 05:37
10 Doctrine Fixtures - Part 1 - Setup and Category Entity Creation 08:52
11 Doctrine Fixtures - Part 2 - Relating Wallpapers with Categories 05:56
12 EasyAdminBundle - Setup and Category Configuration 06:02
13 EasyAdminBundle - Wallpaper Setup and List View 07:46
14 EasyAdminBundle - Starting with Wallpaper Uploads 05:57
15 Testing with PhpSpec to Guide Our Implementation 03:39
16 Using PhpSpec to Test our FileMover 05:34
17 Symfony Dependency Testing with PhpSpec 08:47
18 Defensive Counter Measures 06:33
19 No Tests - Part 1 - Uploading Files in EasyAdminBundle 11:01
20 No Tests - Part 2 - Uploading Files in EasyAdminBundle 07:05
21 Don't Mock What You Don't Own 09:36
22 You've Got To Take The Power Back 07:36
23 Making Symfony Work For Us 08:56
24 Testing The Wallpaper File Path Helper 15:11
25 Finally, It Works! 14:56
26 Why I Prefer Not To Use Twig 16:51
27 Fixing The Fixtures 11:20
28 Untested Updates 14:30
29 Untested Updates Part Two - Now We Can Actually Update 06:33
30 Adding an Image Preview On Edit 12:31
31 Delete Should Remove The Wallpaper Image File 11:02
32 Getting Started Testing Wallpaper Updates 10:02
33 Doing The Little Before The Big 08:13
34 Tested Image Preview... Sort Of :) 07:36
35 Finishing Up With a Tested Wallpaper Update 10:41
36 Test Driven Wallpaper Delete - Part 1 11:06
37 Test Driven Wallpaper Delete - Part 2 11:57
38 EasyAdminBundle Login Form Tutorial 08:01