Backbeat Software

First look at Symfony UX

Our first impressions of the Symfony UX initiative.

Glynn Forrest
Friday, December 11, 2020

At Symfony World 2020 the brand new Symfony UX initiative was announced, aiming to drastically simplify JavaScript usage in Symfony apps. Let’s take a look!

The problem being solved

Getting rich JavaScript features working in Symfony has always involved some manual steps. Picking packages and frameworks, figuring out how to pass data between PHP and the frontend, and dealing with different entrypoints across pages adds a lot of complexity.

This complexity gets much worse when trying to distribute JavaScript as a vendor. For example, if the AcmeColorPickerBundle included a custom ColorPickerType form type, and wanted to include JavaScript to power that form type, it would need to figure out distributing that code to work with all possible JavaScript setups the user might have!

There are some existing approaches to solve this, but they’re not great:

Include ‘dist’ assets

Distribute ‘dist’ assets with the bundle to include manually in HTML. This is quite ‘old school js’, where developers include libraries globally on a page, followed by the application code:

<script src="{{asset('bundles/acmecolorpicker/picker.js')}}"></script>
<script src="{{asset('build/app.js')}}"></script>

It also means we can’t integrate it with build tooling (Webpack), minify it how we like, etc.

Import the bundle javascript

Another option is to import the javascript ‘source’ files from the bundle into the application’s build toolchain. You could tell Webpack how to import the file directly:

// webpack.config.js
{
  resolve: {
    modules: [
      'acme-color-picker': path.resolve(__dirname, './vendor/acme/color-picker-bundle/Resources/assets/picker.js'),
    ],
  }
}
// app.js
import picker from 'acme-color-picker';

picker();

Or in package.json directly instead:

{
    "devDependencies": {
        "acme-color-picker": "file:vendor/acme/color-picker-bundle/Resources/assets"
    }
}

This is what Symfony UX does, but in an automated and structured way.

It also solves the other problems - passing data between frontend and backend, and structuring entrypoints effectively.

How? With the following features:

  • Symfony Flex can update package.json when PHP packages are installed;
  • Webpack Encore can use a controller.json file to handle Stimulus JavaScript controllers and load files supplied by PHP packages;
  • Symfony can integrate with Stimulus to add JavaScript ‘snippets’ to existing HTML, and pass data from Symfony controllers to Stimulus controllers.

Chart example

Let’s examine each step of the Chart.js example in the announcement post to see how it works. Let’s assume you have a working Symfony application already!

Step 1

composer update symfony/flex
yarn upgrade "@symfony/webpack-encore@^0.32.0"
composer recipes:install symfony/webpack-encore-bundle --force -v

Fairly straightforward - this ensures you have the latest version of Symfony Flex (which can update package.json) and Webpack Encore (which can work with controllers.json).

The second command ensures that your project is updated to configure symfony/webpack-encore-bundle correctly - you’ll see new options in config/packages/webpack_encore.yaml.

Step 2

composer require symfony/ux-chartjs

Here’s where the magic happens - by requiring a new Composer package, Symfony Flex kicks in and updates a few files, notably package.json and controllers.json:

// package.json
{
    "devDependencies": {
        ...
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets"
    }
}
// assets/controllers.json
{
    "controllers": {
        "@symfony/ux-chartjs": {
            "chart": {
                "enabled": true,
                "webpackMode": "eager"
            }
        }
    },
    "entrypoints": []
}

It knew how to do this by looking at the composer.json for symfony/ux-chartjs:

{
    "symfony": {
        "controllers": {
            "chart": {
                "main": "dist/controller.js",
                "webpackMode": "eager",
                "enabled": true
            }
        }
    },
}

Webpack Encore now knows how to load the JavaScript files in the symfony/ux-chartjs composer package, and the data-controller="@symfony/ux-chartjs/chart" Stimulus controller is available to use on HTML elements.

Step 3

yarn install --force
yarn encore dev

Since Flex updated package.json, we need to install the new dependencies. symfony/ux-chartjs requires chart.js, so it’ll install that package for us too.

Step 4

This updates a controller and template to render a simple chart. Here’s an abridged version:

use Symfony\UX\Chartjs\Builder\ChartBuilderInterface;
use Symfony\UX\Chartjs\Model\Chart;

class HomeController extends AbstractController
{
    public function index(ChartBuilderInterface $chartBuilder): Response
    {
        $chart = $chartBuilder->createChart(Chart::TYPE_LINE);
        $chart->setData([...]);
        $chart->setOptions([...]);

        return $this->render('home/index.html.twig', [
            'chart' => $chart,
        ]);
    }
}
{{ render_chart(chart) }}

When the page is rendered, we end up with something like this:

<canvas data-controller="@symfony/ux-chartjs/chart" data-view='{"type":"line","data":{"labels":[...],"datasets":[...]},"options":[...]}'></canvas>

The render_chart() twig function provided by the bundle rendered a canvas element with the Chart object serialized to the data-view property for Stimulus to use.

It’s a nice timesaver, although there’s nothing stopping us from rendering this HTML manually without a Chart object:

class HomeController extends AbstractController
{
    public function index(): Response
    {
        $chartData = [
            'type' => 'line',
            'data' => [...],
        ];

        return $this->render('home/index.html.twig', [
            'chart_data' => $chartData,
        ]);
    }
}
<canvas data-controller="@symfony/ux-chartjs/chart" data-view="{{chart_data|json_encode|e('html_attr')"></canvas>

Since Chart and ChartBuilderInterface are just basic wrappers at the moment, they don’t offer a lot of improvement over simple data objects. I imagine that more features are planned, and they’re features that the Symfony UX team will write for you!

Integration with a Doctrine query would be interesting. For example:

$query = $someRepository->queryAnnualReport();

$chart = $chartBuilder->createChart(Chart::TYPE_LINE);
$chart->setDataFromDoctrineQuery($query);

However, requiring a new composer package for each new supported npm package concerns me a bit. There are lots of npm packages out there. Do we need a composer package, twig extension, and builder objects for each of them?

Perhaps there’s an abstraction of some kind we could use for multiple packages. Here’s the chart example again with that in mind:

class HomeController extends AbstractController
{
    public function index(): Response
    {
        $chartData = new StimulusData('@symfony/ux-chartjs/chart', [
            'type' => 'line',
            'data' => [...],
        ]);

        return $this->render('home/index.html.twig', [
            'chart_data' => $chartData,
        ]);
    }
}
<canvas {{stimulus_attrs(chart_data)></canvas>

Regarding Chart.js specifically, there are some options that require javascript functions, e.g. label callbacks. How will we define those in PHP and pass them to Stimulus?

Verdict

I think Symfony UX is an interesting initiative with a lot of potential. It brings structure and standardisation to Symfony’s JavaScript story, enabling even faster app development than before.

In particular, I really like the choice to keep it simple, and to not force developers into a particular framework. There are other cool integrations I haven’t touched on here, like symfony/ux-swup for fancy SPA-like page transitions.

My main concerns at the moment are:

  • Developers complaining about Stimulus. It’s only a small library, so this shouldn’t be much of a problem. It isn’t asking developers to ‘pick a religion’ in the same way React vs Vue vs Angular does, but it could still be off-putting for some.
  • Risk of package bloat and maintainer burnout trying to write symfony/ux-* packages for every popular npm package. I hope the maintainers consider some higher level abstractions, e.g. the StimulusData class sketched out above.
  • How do multiple UX packages interact? If I wanted to use symfony/ux-chartjs and symfony/ux-dropzone in the same parent element, how would they be combined? Will I have to write my own Stimulus controller anyway?

Overall, I think it’s going to be great for a large amount of Symfony apps, and I’m excited to see where it goes next. Congratulations to the Symfony team on the new initiative, and good luck!

More from the blog

Asset cache busting in Symfony applications cover image

Asset cache busting in Symfony applications

Different techniques to send the newest css and javascript files to your users.


Glynn Forrest
Friday, January 31, 2020

Webpack hot module replacement in server-rendered apps cover image

Webpack hot module replacement in server-rendered apps

Get the benefits of webpack-dev-server without building an SPA.


Glynn Forrest
Friday, July 31, 2020

Logged out ajax requests in Symfony applications cover image

Logged out ajax requests in Symfony applications

Handling logged out ajax requests properly using Symfony’s security features.


Glynn Forrest
Monday, June 29, 2020