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())
->setProperty($propertyName)
->setLabel($label)
->setTemplateName('crud/field/integer')
->setFormType(IntegerType::class)
->addCssClass('field-integer')
->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
->setTemplatePath('admin/field/votes.html.twig')
// this is used in 'edit' and 'new' pages to edit the field contents
// you can use your own form types too
->setFormType(IntegerType::class)
... 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!

16
Login or Register to join the conversation
t5810 Avatar

Hi everyone

I have project that has the latest version of EasyAdmin, and I use PostgreSQL.
My problem is the following: I have imported some data in one of my tables directly from an older database to the database that I use in this project. The result is: I can't insert new record from EasyAdmin. The error that I get is:

An exception occurred while executing a query: SQLSTATE[23505]: Unique violation: 7 ERROR: duplicate key value violates unique constraint "skill_pkey"
DETAIL: Key (id)=(7) already exists.

As I have read, the issue is in the way how PostgreSQL manage the primary keys. As far as I have understand, I have now to provide the latest id + 1 to the form. While I guess that I have now to add new hidden field in the insert new action, I have no idea which method I have to override in my SkillCrudController so I can add somehow add (I am now guessing) hidden field and the value for the id? Again, I am guessing here...please correct me if I am wrong.

Cheers

Reply

Hey @t5810

I believe you need to fix your DB table instead of changing how your app determines the ID. I leave you here a SO post that may help you out https://stackoverflow.com/questions/244243/how-to-reset-postgres-primary-key-sequence-when-it-falls-out-of-sync

Hope it helps. Cheers!

Reply
t5810 Avatar

Hi MolloKhan

Brilliant. Works like a charm now Thank you very much!
Cheers

Reply
Brent Avatar

Hello,

My project is using EasyAdmin v.3.5.16 and Symfony v5.3.15. I have a description field which is a TextEditorField. I am wanting to either configure this field, add a custom type or some other solution in order to have this description field have 'Source' WYISWYG option to toggle HTML formatting. I've been looking and and seems Trix Editor doesn't have this option? Perhaps installing FOSCKEditorBundle would be best to accomplish this? Looking for some advice/direction on how best to have Rich Text field with a 'Source' option. Thank you in advance.

Reply

Hey Brent,

Yeah, unfortunately, the built-in TextEditorField does not have this option. So, the best would be to create a custom field and attach a more advanced JS WYSIWYG editor like CKEditor you mentioned. Or TinyMCE is another popular WYSIWYG editor with the source code option, but there're a lot of others too, so just use your favorite.

Well, as a workaround, you may try to add some JS that will toggle the TrixEditor/textarea. If you inspected the code - you would see that the textarea is hidden nearby that editor with display: none CSS prop. So, in theory, you can add a link with some custom JS code attached to it to hide the editor and show that textarea - it might be a nice and simple solution, but you would need to find a way to sync the changes in the source code (i.e. textarea) back with the TrixEditor if you need it - there should be a way I think, but I've never tried it myself, so can't tell you for sure :)

I hope this helps!

Cheers!

Reply
Delenclos-A Avatar
Delenclos-A Avatar Delenclos-A | posted 3 months ago

Hi,

In my index page, i would like to create an editable field (or dropdown list). It's for a votation dances. I imagine the same logic than a boolean field that's update database.at each change.

Can you help me to do that?
Have a good day
Alain from France

Reply

Hey Delenclos-A ,

Unfortunately, nothing is available out of the box in EasyAdmin for this, so you literally would need to write a custom field for this and add some javascript code to make it work. Probably you could re-use some other JS lib for the modern dropdown list (e.g. https://select2.org/ ), etc.

This chapter should give you a good start with the EA custom field creation, but you would need to add some JS code to it to make it actually work. You can add a custom JS code via addJsFiles() or via addWebpackEncoreEntries() in case you're using Webpack Encore - similar to addCssClass() that we're using in this video.

You can also take a look at the core BooleanField to see how it works. It seems like you would also need to create a custom endpoint to which you will actually send the AJAX request and handle the field value change.

I hope this helps!

Cheers!

Reply
Delenclos-A Avatar
Delenclos-A Avatar Delenclos-A | Victor | posted 3 months ago

Thank’s team for your help, I’ll try this in everything month, because my own logic for votation is complex (you have some points to give, all of them must be given) and it’s not easy to do that for my old head. Maybe an unmapped field… for the moment i try this in a simple controller. Thank’s again and have a good day.

Reply

Hey Delenclos-A,

I hope those ideas will be useful! Try to watch this course till the end, it should give you understanding of more things that you can do in EA, and better understand the EA.

I hope this will help!

Cheers!

Reply
HJT Avatar
HJT Avatar HJT | posted 10 months ago | edited

Hi, worth every Euro! But I'm stuck in creating a custom template for showing message 1 of x. The messages are in a message_group. In the message Entity I can do this.

    public function __toString(): string
    {
        return $this->getSequence() . ' of ' .$this->getMessageGroup()->getMessages()->count();
    }

But it would be better to do this in a custome easyAdmin twig template.. but how can I get this total message count in the template ?
number of replies om posts is commonly there ... eg reply 1 of 10 .. Can't find it..

Reply

Hey HJT,

You'll need to create a custom field template. This video may help you out https://symfonycasts.com/screencast/easyadminbundle/field-template
or perhaps this other video https://symfonycasts.com/screencast/easyadminbundle/override-template

Cheers!

Reply
Szabolcs Avatar

Hello! I just wondering, why the Fields (IntegerField, TextField, etc.) are all marked as final classes? Let's say I want to have an own IntegerField which I use everywhere, with inheritance I just could extend the IntegerField, override the static "new"-method, but the Configurator-system would still work in the background. But why this is not allowed :-D? Thanks!

Reply

Hey Szabolcs,

This is a good question. Unfortunately, I’m not a maintainer of this bundle and you probably better to ask it in the repository of this bundle, or ask a question there if you want the exact reason behind of it.

Though I think I can give you hints why it might be done this way :) the real problem is in maintenance I think. Making some classes final unload the maintainer, ie the maintainer can easily change the code inside those clases without worrying about BC brakes. In turn, it allows easier to make new minor releases instead of making major ones that would contain BC brakes. Moreover, another important thing is that fields are simple, you can easily write a new standalone field, and if you want - just copy/paste the existent code from the core classes. That’s in my opinion and I hope it helps, but if you really curious - you can ask maintainers I think :)

Cheers!

Reply
Szabolcs Avatar

ok, thank you very much for the answer!

Reply

You're welcome! :)

1 Reply
Szabolcs Avatar

By the way, of course I can write a custom factory-class, which can create my IntegerField and the Configurator-system would work, I am just curious about the idea, to mark all Field-classes as final classes.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "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
    }
}
userVoice