Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Creating a Custom Field

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

On the Answers CRUD, we just created this nice custom "Votes" template, which we then use by calling ->setTemplatePath() on the votes field. But we also have a votes field over in the Questions section... which still renders the boring way. I want to use our new template in both places.

Technically, doing this is super easy! We could copy ->setTemplatePath(), go up, open QuestionCrudController, find the votes field... then paste to use that template path.

But instead, let's create a custom field.

We know that a field class - like TextareaField or AssociationField - defines how a field looks on the index and detail pages... as well as how it's rendered on a form. A custom field is a great way to encompass a bunch of custom field configuration in one place so you can reuse it. And creating a custom field is pretty easy.

Creating the Custom Field

Down in the src/EasyAdmin/ directory, create a new PHP class called, how about, VotesField.

The only rule for a field is that it needs to implement FieldInterface. This requires us to have two methods: new and getAsDto(). But what you'll typically do is use FieldTrait to make life easier.

... lines 1 - 4
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
... lines 6 - 7
class VotesField implements FieldInterface
use FieldTrait;
... lines 11 - 15

Click to open that. Ok, this FieldTrait helps manage the FieldDto object, with a bunch of useful methods like setLabel(), setValue() and setFormattedValue() that all fields share.

So now, if you go to Code Generate - or "cmd + N" on a Mac - the only thing we need to implement is new(). This is where we customize all the options for the field.

... lines 1 - 7
class VotesField implements FieldInterface
... lines 10 - 11
public static function new(string $propertyName, ?string $label = null)
// TODO: Implement new() method.

Our votes field is currently an IntegerField. Hold "cmd" or "ctrl" to open that and look at its new() method... because we want our method to look very much like this... with a few differences. So copy all of this, close, head to VotesField... and paste. Hit "Ok" to add that use statement on top. I'll also remove OPTION_NUMBER_FORMAT. We won't need that... and it relates to a field configurator that I'll show you in a minute.

... lines 1 - 6
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
... line 8
class VotesField implements FieldInterface
... lines 11 - 12
public static function new(string $propertyName, ?string $label = null)
return (new self())
->setDefaultColumns('col-md-4 col-xxl-3');

Ok, good start! You may have noticed that ->setDefaultColumns() is crossed out. That's because it's marked as "internal". That usually means it's a function that we shouldn't use directly. But in this case, the documentation says that it is ok to use from inside of a field class... which is where we are!

At this point, we can customize anything! Like ->addWebpackEncoreEntries() to add an extra Webpack Encore entry that will be included when this field is used. What we want to do, instead of calling ->setTemplateName() so that it uses the standard integer field template, is to say ->setTemplatePath() and pass the same thing we have in AnswerCrudController, which is admin/field/votes.html.twig.

As a reminder to myself, I'll add some comments about which part controls the index and detail pages... and which part controls the form.

... lines 1 - 12
public static function new(string $propertyName, ?string $label = null)
return (new self())
... lines 16 - 17
// this template is used in 'index' and 'detail' pages
// this is used in 'edit' and 'new' pages to edit the field contents
// you can use your own form types too
... lines 23 - 24

Using the Custom field

Ok, that's it! Let's go use this!

In AnswerCrudController, change this to VotesField... and we don't need ->setTemplatePath() anymore.

... lines 1 - 12
class AnswerCrudController extends AbstractCrudController
... lines 15 - 19
public function configureFields(string $pageName): iterable
... lines 22 - 28
yield VotesField::new('votes', 'Total Votes')
... lines 30 - 35

Then, in QuestionCrudController, do the same thing. Add VotesField and... done! If we wanted to, we could even put this ->setTextAlign('right') inside the custom field... or remove it.

... lines 1 - 4
use App\EasyAdmin\VotesField;
... lines 6 - 14
class QuestionCrudController extends AbstractCrudController
... lines 17 - 21
public function configureFields(string $pageName): iterable
... lines 24 - 35
yield VotesField::new('votes', 'Total Votes')
... lines 37 - 55

Testing time! Over in Questions, refresh and... got it! And on the Answers page... it looks great there too!

Watch out for Missing Configurators

But one tiny word of warning. Now that we've changed from IntegerField to VotesField, if there's a field configurator for the IntegerField, it will no longer be used.

And... there is such a configurator. Back down in vendor/easycorp/easyadmin-bundle/src/Field/Configurator, you'll find IntegerConfigurator. This operates only when the field you're using is an IntegerField. And so, this configurator was being used until a second ago... but not anymore.

If you look inside, it does some work with a custom number format, which allows you to control the format that the number is printed. We don't really need this, but don't forget about the "field configurator" system... and how a custom field won't be processed in the same way.

Next, let's learn how to configure a bit more of the CRUD itself, like how a CRUD section is sorted by default, pagination settings, and more.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=8.0.2",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", //
        "doctrine/doctrine-bundle": "^2.1", // 2.5.5
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.2.1
        "doctrine/orm": "^2.7", // 2.10.4
        "easycorp/easyadmin-bundle": "^4.0", // v4.0.2
        "handcraftedinthealps/goodby-csv": "^1.4", // 1.4.0
        "knplabs/knp-markdown-bundle": "dev-symfony6", // dev-symfony6
        "knplabs/knp-time-bundle": "^1.11", // 1.17.0
        "sensio/framework-extra-bundle": "^6.0", // v6.2.5
        "stof/doctrine-extensions-bundle": "^1.4", // v1.7.0
        "symfony/asset": "6.0.*", // v6.0.1
        "symfony/console": "6.0.*", // v6.0.2
        "symfony/dotenv": "6.0.*", // v6.0.2
        "symfony/flex": "^2.0.0", // v2.0.1
        "symfony/framework-bundle": "6.0.*", // v6.0.2
        "symfony/mime": "6.0.*", // v6.0.2
        "symfony/monolog-bundle": "^3.0", // v3.7.1
        "symfony/runtime": "6.0.*", // v6.0.0
        "symfony/security-bundle": "6.0.*", // v6.0.2
        "symfony/stopwatch": "6.0.*", // v6.0.0
        "symfony/twig-bundle": "6.0.*", // v6.0.1
        "symfony/ux-chartjs": "^2.0", // v2.0.1
        "symfony/webpack-encore-bundle": "^1.7", // v1.13.2
        "symfony/yaml": "6.0.*", // v6.0.2
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.7
        "twig/twig": "^2.12|^3.0" // v3.3.7
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.1
        "symfony/debug-bundle": "6.0.*", // v6.0.2
        "symfony/maker-bundle": "^1.15", // v1.36.4
        "symfony/var-dumper": "6.0.*", // v6.0.2
        "symfony/web-profiler-bundle": "6.0.*", // v6.0.2
        "zenstruck/foundry": "^1.1" // v1.16.0