Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Override Controllers

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 $8.00

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

Login Subscribe

When we submit a form, obviously, EasyAdminBundle is taking care of everything: handling validation, saving and adding a flash message. That's the best thing ever! Until... I need to hook into that process... then suddenly, it's the worst thing ever! What if I need to do some custom processing right before an entity is created or updated?

There are 2 main ways to hook into EasyAdminBundle...and I want you to know both. Open the User entity. It has an updatedAt field:

... lines 1 - 16
class User implements UserInterface
{
... lines 19 - 82
/**
* @ORM\Column(name="updated_at", type="datetime", nullable=true)
*/
private $updatedAt;
... lines 87 - 279
}

To set this, we could use Doctrine lifecycle callbacks or a Doctrine event subscriber.

But, I want to see if we can set this instead, by hooking into EasyAdminBundle. In other words, when the user submits the form for an update, we need to run some code.

The protected AdminController Methods

The first way to do this is by adding some code to the controller. Check this out: open up the base AdminController from the bundle and search for protected function. Woh... There are a ton of methods that we can override, beyond just the actions. Like, createNewEntity(), prePersistEntity() and preUpdateEntity().

If we override preUpdateEntity() in our controller, that will be called right before any entity is updated. There are a few other cool things that you can override too.

Per-Entity Override Methods

Ok, easy! Just add preUpdateEntity() to our AdminController, right? Yep... but we can do better! If we override preUpdateEntity(), it will be called whenever any entity is updated. But we really only want it to be called for the User entity.

Once again, EasyAdminBundle has our back. Inside the base controller, search for preUpdate. Check this out: right before saving, it calls some executeDynamicMethod function and passes it preUpdate, a weird <EntityName> string, then Entity.

Actually, the bundle does this type of thing all over the place. Like above, when it calls createEditForm(). Whenever you see this, it means that bundle will first look for an entity-specific version of the method - like preUpdateUserEntity() - and call it. If that doesn't exist, it will call the normal preUpdateEntity().

This is huge: it means that each entity class can have its own set of hook methods in our AdminController!

One Controller per Entity

And now that I've told you that... we're going to do something completely different. Instead of having one controller - AdminController - full of entity-specific hook methods like preUpdateUserEntity or createGenusEditForm - I prefer to create a custom controller class for each entity.

Try this: in the EasyAdmin directory, copy AdminController and rename it to UserController. Then, remove the function. Use the "Code"->"Generate menu" - or Command+N on a Mac - to override the preUpdateEntity() method. And don't forget to update your class name to UserController:

... lines 1 - 2
namespace AppBundle\Controller\EasyAdmin;
use AppBundle\Entity\User;
use JavierEguiluz\Bundle\EasyAdminBundle\Controller\AdminController as BaseAdminController;
class UserController extends BaseAdminController
{
/**
* @param User $entity
*/
protected function preUpdateEntity($entity)
{
... line 15
}
}

We're going to configure things so that this UserController is used only for the User admin section. And that means we can safely assume that the $entity argument will always be a User object:

... lines 1 - 4
use AppBundle\Entity\User;
... lines 6 - 7
class UserController extends BaseAdminController
{
/**
* @param User $entity
*/
protected function preUpdateEntity($entity)
{
... line 15
}
}

And that makes life easy: $entity->setUpdatedAt(new \DateTime()):

... lines 1 - 7
class UserController extends BaseAdminController
{
... lines 10 - 12
protected function preUpdateEntity($entity)
{
$entity->setUpdatedAt(new \DateTime());
}
}

But how does EasyAdminBundle know to use this controller only for the User entity? That happens in config.yml. Down at the bottom, under User, add controller: AppBundle\Controller\EasyAdmin\UserController:

... lines 1 - 80
easy_admin:
... lines 82 - 97
entities:
... lines 99 - 158

And just like that! We have one controller that's used for just our User.

Try it out! Let's go find a user... how about ID 20. Right now, its updateAt is null. Edit it... make some changes... and save! Go back to show and... we got it!

Organizing into a Base AdminContorller

This little trick unlocks a lot of hook points. But if you look at AdminController, it's a little messy. Because, changePublishedStatusAction() is only meant to be used for the Genus class:

... lines 1 - 6
class AdminController extends BaseAdminController
{
public function changePublishedStatusAction()
{
$id = $this->request->query->get('id');
$entity = $this->em->getRepository('AppBundle:Genus')->find($id);
... lines 13 - 24
}
}

But technically, this controller is being used by all entities, except User.

So let's copy AdminController and make a new GenusController! Empty AdminController completely:

... lines 1 - 2
namespace AppBundle\Controller\EasyAdmin;
use JavierEguiluz\Bundle\EasyAdminBundle\Controller\AdminController as BaseAdminController;
class AdminController extends BaseAdminController
{
}

Then, make sure you rename the new controller class to GenusController:

... lines 1 - 2
namespace AppBundle\Controller\EasyAdmin;
class GenusController extends AdminController
{
public function changePublishedStatusAction()
{
$id = $this->request->query->get('id');
$entity = $this->em->getRepository('AppBundle:Genus')->find($id);
$entity->setIsPublished(!$entity->getIsPublished());
$this->em->flush();
$this->addFlash('success', sprintf('Genus %spublished!', $entity->getIsPublished() ? '' : 'un'));
return $this->redirectToRoute('easyadmin', [
'action' => 'show',
'entity' => $this->request->query->get('entity'),
'id' => $id,
]);
}
}

But before we set this up in config, change the extends to extends AdminController, and remove the now-unused use statement:

... lines 1 - 2
namespace AppBundle\Controller\EasyAdmin;
class GenusController extends AdminController
{
... lines 7 - 23
}

Repeat that in UserController:

... lines 1 - 2
namespace AppBundle\Controller\EasyAdmin;
... lines 4 - 6
class UserController extends AdminController
{
... lines 9 - 15
}

Yep, now all of our sections share a common base AdminController class. And even though it's empty now, this could be really handy later if we ever need to add a hook that affects everything.

Love it! UserController has only the stuff it needs, GenusController holds only things that relate to Genus, and if we need to override something for all entities, we can do that inside AdminController.

Don't forget to go back to your config to tell the bundle about the GenusController. All the way on top, set the Genus controller to AppBundle\Controller\EasyAdmin\GenusController:

... lines 1 - 80
easy_admin:
... lines 82 - 97
entities:
Genus:
... line 100
controller: AppBundle\Controller\EasyAdmin\GenusController
... lines 102 - 159

Now we're setup to do some really, really cool stuff.

Leave a comment!

20
Login or Register to join the conversation
Default user avatar
Default user avatar Mohammad | posted 5 years ago

Hi,
It's so weird for me that just when I add "EasyAdmin" folder at my bundle, I get the message that:
No route found for "GET /admin/"
I've just check any possible solutions without any success :(

Any idea?

9 Reply

Hey Mohammad

Sorry for the late reply! Do you still have this problem?

By a chance did you do any other modification before adding that folder? I believe you are missing a route definition.
Or maybe, EasyAdminBundle people have made some changes since this tutorial was published.

If that's not the case, lets us know :)

Cheers!

Reply
Default user avatar

Btw: In Symfony 4 with EasyAdmin Bundle with Flex, you need to extend this class: EasyCorp\Bundle\EasyAdminBundle\Controller\AdminController instead of: JavierEguiluz\Bundle\EasyAdminBundle\Controller\AdminController

This worked for me :)

1 Reply

Hey @Adam

Thanks for notifying us about that. I just did some digging and found that that change was made in this release: https://github.com/EasyCorp...

So, yes, you have to update all your references if you were using our code and then you decided to update EasyAdminBundle.

Cheers!

Reply

Hi
Are you planning to update the tutorial on EasyAdmin with SF4 + connection with Guard ?
best regards,
Gregory

Reply

Hey Greg,

Btw, we do cover Guard in our Symfony 4 tutorials, for example in this one about Security: https://symfonycasts.com/sc... - have you watched it?

> EasyAdmin with SF4 + connection with Guard

Also, what do you mean about this? EasyAdmin is just a tool to generate admin interface for your Symfony aplpications. But Guard is all about authentication. So, they are not tight to each other and are separate things. And we do have separate screencasts about them. What exactly problem do you have using Guard and EasyAdmin?

Cheers!

Reply

Hi Victor

I found my way ;)
I had to implode my own method to encrypt the password when I create a user.
I didn't think before I posted the comment ;)
EasyAdmin is so powerful when you understood how to override the view and method.
BTW with the last version of EasyAdmin you can't override preUpdateEntity, I used to override updateEntity and persistEntity

Thanks again for your wonderful works to all the Symfonycast team.
Happy Christmas and Happy new Year

Reply

Hey Greg,

Ah, I see :) Glad you got it working!

And good to know about "preUpdateEntity()", thanks for this tip!

Cheers!

Reply

Hey Gregory

That's not in our milestone but thanks for letting us know what kind of tutorials you would like to see in the future. We *do* consider your opinion when deciding what tutorial is going to be next

BTW, do you have any particular problem installing EasyAdmin on SF4?

Cheers!

Reply

Hi Diego,

Thank you for your answer.
I have no problem with EasyAdmin, I just need to think more before I talked ;)
Cheers.

1 Reply
Dennis E. Avatar
Dennis E. Avatar Dennis E. | posted 5 years ago | edited

Hi,

I've got an image property in my product form. I just took it out of the workflow, and want to save the images in my other entity ProductImages. Now I want to save them, but how do I get the images in my controller?

I've tried this:


/**
 * @param object $entity
 */
protected function preUpdateEntity($entity)
{
    var_dump($entity->getImages()); die;
}

But that does not return the images. It just dumps the entity.

BR,
Dennis

Reply

Yo Dennis!

Hmm. Sorry for our late reply :). Can you show a little bit more code? If I understand it correctly, your editing a "Product", and you've added a custom "image" property to that form (but there is no image property on your Product entity - that is ok, I'm just clarifying). Now, you want to read the submitting image from the form and use it to actually save data on a different entity - ProductImage. Is that accurate?

So, you really need access to the form object in your controller, because it will contain the "image" data. And yea, that's a bit tricky - a bit too tricky in my opinion. Here is how I think it will need to work:

1) In your custom controller, override the createEntityForm method. Just call the parent() method, but store the form on a new property:


private $form;

protected function createEntityForm($entity, array $entityProperties, $view)
{
    $this->form = parent::createEntityForm($entity, $entityProperties, $view);

    return $this->form;
}

2) In your preUpdateEntity, you can now access the form, which should allow you to access the image field:


protected function preUpdateEntity($entity)
{
    dump($this->form['image']->getData()); die;
}

If the user uploaded a file, this should be an UploadedFile object.

Let me know if this helps!

Reply
Dennis E. Avatar
Dennis E. Avatar Dennis E. | weaverryan | posted 5 years ago | edited

Hi Ryan,

I figured it out already. I do have an image property in my product class. Because one product can have multiple images. So I do need it to be uploaded in the form.

So just to clarify:
I do have a product entity, with an image attribute. That image attribute is linked to my productImages entity. I save the images (URL's) in the database, and upload the images with a service I created. FileHandlerService. Now when I upload the productImages, it will return an array of URL's. And I pass that with this entity:


class ProductController extends AdminController
{
    protected $em;

    public function __construct(EntityManager $em)
    {
        $this->em = $em;
    }

    /* this one it for creating new products with images */
    protected function prePersistEntity($entity)
    {
        $fileHandler = new FileHandler($entity, $this->em);
        $productImages = $fileHandler->saveImages($this->getParameter('product_image_directory'));

        $entity->setImages($productImages);
    }


    /**
     * @param object $entity
     */
    protected function preUpdateEntity($entity)
    {
        $fileHandler = new FileHandler($entity, $this->em);
        $productImages = $fileHandler->saveImages($this->getParameter('product_image_directory'));

        $entity->setImages($productImages);
    }
}

Do you think this is bad practice?

Hope to hear from you, and thanks for your time, patience and help.

Best regards,
Dennis

Reply

Hey Dennis!

Sorry for my late reply - your message got lost in SPAM for some reason. Tisk tisk Disqus!

I still don't fully understand your setup - your have an "image" property on Product, but each Product also has many images? So, the image property is just a non-persisted property that you added to Product so that you form would work? Is that correct?

But mostly, I don't see any issues with your setup :). Unrelated to your original question, I would actually register that FileHandler class as a service, remove the first constructor argument from it and pass the productImagesDir to the constructor. So it would look like:


class FileHandler
{
    // ...

    public function __construct(EntityManagerInterface $em, $imagesDir)
    {
        // ... store these on properties like normal
    }
}

Then, when you call the method, pass in the entity only:


$fileHandler->saveImages($entity);

It's generally a good idea to pass services & static config to the constructor. Then pass any entities or "per-call configuration" to the method itself. This will create really-nicely and re-usable service classes.

Cheers!

1 Reply
Dennis E. Avatar

Hi Ryan,

I think I ask too many questions!

I'm sorry if I did not make myself clear enough. English is not my first language (I'm Dutch).

No that's not correct. I have 2 entities. Product and ProductImages. The ProductImages saves the URL to the database. The FileHandler *is* a service actually.

I should inject the FileHandler in the constructor of the productController, and then I should inject the EM in the FileHandler constructor, together with the upload directory, right? And pass the entity as a parameter of the saveImages function.

My FileHandler->saveImages() returns an array of objects, to save it to the DB (or atleast not get an error).

Thanks for your time and patience.

Best regards,
Dennis

Reply

Hey Dennis!

I think you can do all that work in your FileHandler (validate and save to the DB), by injecting EntityManager and the upload directory. Then, in your controller inject that service and just use it, passing through the entity

Cheers!

Reply
Default user avatar
Default user avatar Riccardo Previti | posted 5 years ago

Hi :-)
I'm trying to get a smooth admin interface similar as if when two entities are associated by a many-to-many relationship. I need the join table to define additional information like rank. I don't want to display the jointable entity on the backend, it should be writeable in the edit-menu of at least one entity. How can I make an edit form access the values of the jointable?

Reply

Hey @Riccardo Previti

Usually when you have a many-to-many relationship you don't need an associated entity, but when you need custom fields for that relation (let's say, createdAt), then you will have to create a new entity that will hold those values and a reference to the related entities. In this case you will end up with 3 classes with a one-to-many relationship.
You can find good examples here: http://codemonkeys.be/2013/...

Also you may find helpful this tutorial to start working with associations: http://knpuniversity.com/sc...

Have a nice day!

1 Reply
Default user avatar
Default user avatar Riccardo Previti | MolloKhan | posted 5 years ago

Thank you for your time. You're exactly talking about what I want. I already did the tutorial and the relation is already set up. Maybe I did not express myself well, but I am only concerned about how to make it look like a many to many in easyadmin's backend entity. For now, it only tries to list joinTable entities. Those should not be choosen, but generated when choosing the to-be-related entity. Example:

Many Questions can have many Options.

In my backend I want to create questions and assign options to it or create options and see which questions they belong to. Can't find any documentation anywhere on this

Reply

Oh I got you now, I'm not an expert using EasyAdminBundle, but I think you will have to put in practice almost everything you have learned in this tutorial, like override a layout, a form, controller action, etc.

After doing it, you will be able to do anything you want in EasyAdminBundle :)

Cheers!

Reply
Cat in space

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

This tutorial is built on an older version of Symfony & EasyAdminBundle. Many of the concepts are the same, but you can expect major differences in newer versions.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.3.*", // v3.3.18
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.5
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.17.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.7.1
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2", // v1.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "javiereguiluz/easyadmin-bundle": "^1.16" // v1.17.21
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.40
        "nelmio/alice": "^2.1", // v2.3.5
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice