Buy

Installation and First Admin

Hello! And welcome to another amazing episode of: "Recipes with Ryan". Today we'll be baking a positively mind-blowing, mile-high cinnamon bread!

Huh? It's not the recipe video? EasyAdminBundle? Ok, cool!

Um... we'll be baking a positively mind-blowing admin interface in Symfony with the wonderful EasyAdminBundle.

What about SonataAdminBundle

Wait, but what about SonataAdminBundle? SonataAdminBundle is super powerful... more powerful than EasyAdminBundle. But it's also a bit of a beast - a lot bigger and more difficult to use. If it has a feature that you need... go for it! Otherwise, jump into the easy side with EasyAdminBundle.

Code with Me!

To make your cinnamon bread a hit, code along with me. Download the course code from this page and unzip it. Inside, you'll find a directory called start/, which will have the same code you see here. Open README.md for step-by-step baking instructions... and also details on how to get your project setup!

The last step will be to find your favorite terminal, move into the project and run:

php bin/console server:run

to start the built-in web server. Find your fanciest browser and open that: http://localhost:8000. Welcome to AquaNote! The project we've been building in our Symfony series.

Actually, in that series, we spent some serious time making an admin area for one of our entities: Genus. Go to /admin/genus... and then login: username weaverryan+1@gmail.com, password: iliketurtles.

A genus is a type of animal classification. And after all our work, we can create and edit them really nicely.

That's great... but now I need an admin interface for a bunch of other entities: GenusNote, SubFamily, and User. Doing that by hand... well... that would take a while... and we've got baking to do! So instead, we'll turn to EasyAdminBundle.

Installing EasyAdminBundle

Google for EasyAdminBundle and find its GitHub page. Ah, it's made by our friend Javier! Hi Javier! Let's get this thing installed. Copy the composer require line, go back to your terminal, open a new tab, and paste:

composer require javiereguiluz/easyadmin-bundle

While Jordi is downloading that package, let's keep busy!

Copy the new bundle line, find app/AppKernel.php, and put it here!

61 lines app/AppKernel.php
... lines 1 - 2
use JavierEguiluz\Bundle\EasyAdminBundle\EasyAdminBundle;
... lines 4 - 6
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
... lines 12 - 25
new EasyAdminBundle(),
);
... lines 28 - 38
}
... lines 40 - 59
}

Unlike most bundles, this bundle actually gives us a new route, which we need to import. Copy the routing import lines, find our app/config/routing.yml and paste anywhere:

14 lines app/config/routing.yml
... lines 1 - 9
easy_admin_bundle:
resource: "@EasyAdminBundle/Controller/"
type: annotation
prefix: /easyadmin

Since we already have some pages that live under /admin, let's change the prefix to /easyadmin:

14 lines app/config/routing.yml
... lines 1 - 9
easy_admin_bundle:
... lines 11 - 12
prefix: /easyadmin

Finally, one last step: run the assets:install command. This should run automatically after Composer is finished... but just in case, run it again:

php bin/console assets:install --symlink

This bundle comes with some CSS and JavaScript, and we need to make sure it's available.

Setting up your First Admin

Ok, we are installed! So... what did we get for our efforts? Try going to /easyadmin. Well... it's not much to look at yet... but it will be! I promise! We just need to configure what admin sections we need... and Javier gave us a great error message about this!

In a nut shell, EasyAdminBundle can create an admin CRUD for any entity with almost zero configuration. In the docs, it shows an example of this minimal config. Copy that, find config.yml, scroll to the bottom, and paste. Change these to our entity names: AppBundle\Entity\Genus, AppBundle\Entity\GenusNote, AppBundle\Entity\SubFamily, skip GenusScientist - we'll embed that inside one of the other forms, and add AppBundle\Entity\User:

87 lines app/config/config.yml
... lines 1 - 80
easy_admin:
entities:
- AppBundle\Entity\Genus
- AppBundle\Entity\GenusNote
- AppBundle\Entity\SubFamily
- AppBundle\Entity\User

We have arrived... at the moment of truth. Head back to your browser and refresh Ah, hah! Yes! A full CRUD for each entity: edit, delete, add, list, show, search, party! For a wow factor, it's even responsive: if you pop it into iPhone view, it looks pretty darn good!

This is exactly what I want for my admin interface: I want it to be amazing and I want it to let me be lazy!

Of course the trick with this bundle is learning to configure and extend it. We're 80% of the way there with no config... now let's go further.

Leave a comment!

  • 2017-10-13 kaizoku

    Thanks Ryan !
    As you said, no big deal :)

  • 2017-10-13 weaverryan

    Yo kaizoku!

    Unfortunately, assets:install always does *all* bundles, you can't select only some. But, it's no big deal - you could *not* run assets:install, and instead, create the symlink manually :). The task doesn't do anything special.

    Cheers!

  • 2017-10-12 kaizoku

    Is it possible to specify only this bundle for the command

    php bin/console assets:install --symlink

    I don't wan't all my other bundle to have symlinks into /web as I manage my assets with gulp.

  • 2017-10-11 Diego Aguiar

    Hmm, interesting, could you show me your composer.json file?

    Try this: php bin/console assets:install --symlink --relative

    I believe there is a way to tell Symfony about the name of your public directory, but I just can't find how to do it right now

  • 2017-10-11 Dennis

    Hi Diego Aguiar ,
    Thanks for your reply.

    I already tried it with the --symlink option. Same error.
    Is there no other way? Because when the package is installed, it also executes that command, and it works then.

    Greetings,
    Dennis

  • 2017-10-10 Diego Aguiar

    Hey Dennis!

    How are you running the command? Try passing to it "--symlink" option, if that doesn't fix it, I believe you are correct and you would have to change your public directory to web

    Cheers!

  • 2017-10-10 Dennis

    I get an error when I run assets:install. The error is: 'The target directory "public" does not exist'. My docroot is public_html and not web. Maybe this has something to do with it?

  • 2017-10-09 Diego Aguiar

    Yo!

    About your first error, I believe you just have to add __toString() method to GenusScientist class
    And about the delete error, that's happening because you are trying to remove a row which other rows depend on, so you have two options:
    1) Add cascade={"remove"} as recommended or
    2) Tweak your delete action so you can manually remove the related object

    Cheers!

  • 2017-10-08 Mailbox Spain Mail forwarding

    I am using symfony 3.3 and i am getting error when i try to edit any rows:
    Catchable Fatal Error: Object of class AppBundle\Entity\GenusScientist could not be converted to string

    Exception Logs 1 Stack Trace
    Symfony\Component\Debug\Exception\
    ContextErrorException
    in vendor/symfony/symfony/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php (line 59)
    DoctrineType::createChoiceLabel(object(GenusNote), 0, '1')
    call_user_func(array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceLabel'), object(GenusNote), 0, '1')
    in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php (line 130)
    DefaultChoiceListFactory::addChoiceView(object(GenusNote), '1', array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceLabel'), array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99), array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceName'), null, array(), array(), array())
    in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php (line 196)
    DefaultChoiceListFactory::addChoiceViewsGroupedBy(array('1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '80', '81', '82', '83', '84', '85', '86', '87', '88', '89', '90', '91', '92', '93', '94', '95', '96', '97', '98', '99', '100'), array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceLabel'), array(object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote), object(GenusNote)), array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99), array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceName'), null, array(), array(), array())
    in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php (line 95)
    DefaultChoiceListFactory->createView(object(LazyChoiceList), array(), array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceLabel'), array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceName'), null, null)
    in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/PropertyAccessDecorator.php (line 227)
    PropertyAccessDecorator->createView(object(LazyChoiceList), array(), array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceLabel'), array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceName'), null, null)
    in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/CachingFactoryDecorator.php (line 168)
    CachingFactoryDecorator->createView(object(LazyChoiceList), array(), array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceLabel'), array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceName'), null, null)
    in vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php (line 447)
    ChoiceType->createChoiceListView(object(LazyChoiceList), array('block_name' => null, 'disabled' => false, 'label_format' => null, 'auto_initialize' => true, 'trim' => true, 'property_path' => null, 'mapped' => true, 'by_reference' => true, 'inherit_data' => false, 'method' => 'POST', 'action' => '', 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.', 'help' => null, 'error_mapping' => array(), 'invalid_message' => 'This value is not valid.', 'invalid_message_parameters' => array(), 'allow_extra_fields' => false, 'extra_fields_message' => 'This form should not contain extra fields.', 'csrf_protection' => true, 'csrf_field_name' => '_token', 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.', 'csrf_token_manager' => object(CsrfTokenManager), 'csrf_token_id' => null, 'expanded' => false, 'label' => null, 'attr' => array('multiple' => true, 'data-widget' => 'select2'), 'translation_domain' => 'messages', 'data_class' => null, 'multiple' => true, 'empty_data' => array(), 'required' => false, 'error_bubbling' => false, 'label_attr' => array(), 'compound' => false, 'upload_max_size_message' => object(Closure), 'validation_groups' => null, 'constraints' => array(), 'choices' => null, 'choices_as_values' => true, 'query_builder' => null, 'em' => object(DoctrineORMEntityManager_0000000010991fcd00000000739c3d57f4f8a253a9865f12c8f3d1e0f1f8846b), 'class' => 'AppBundle\\Entity\\GenusNote', 'id_reader' => object(IdReader), 'choice_loader' => object(DoctrineChoiceLoader), 'choice_label' => array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceLabel'), 'choice_name' => array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceName'), 'choice_value' => array(object(IdReader), 'getIdValue'), 'choice_attr' => null, 'preferred_choices' => array(), 'group_by' => null, 'placeholder' => null, 'choice_translation_domain' => false))
    in vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php (line 193)
    ChoiceType->buildView(object(FormView), object(Form), array('block_name' => null, 'disabled' => false, 'label_format' => null, 'auto_initialize' => true, 'trim' => true, 'property_path' => null, 'mapped' => true, 'by_reference' => true, 'inherit_data' => false, 'method' => 'POST', 'action' => '', 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.', 'help' => null, 'error_mapping' => array(), 'invalid_message' => 'This value is not valid.', 'invalid_message_parameters' => array(), 'allow_extra_fields' => false, 'extra_fields_message' => 'This form should not contain extra fields.', 'csrf_protection' => true, 'csrf_field_name' => '_token', 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.', 'csrf_token_manager' => object(CsrfTokenManager), 'csrf_token_id' => null, 'expanded' => false, 'label' => null, 'attr' => array('multiple' => true, 'data-widget' => 'select2'), 'translation_domain' => 'messages', 'data_class' => null, 'multiple' => true, 'empty_data' => array(), 'required' => false, 'error_bubbling' => false, 'label_attr' => array(), 'compound' => false, 'upload_max_size_message' => object(Closure), 'validation_groups' => null, 'constraints' => array(), 'choices' => null, 'choices_as_values' => true, 'query_builder' => null, 'em' => object(DoctrineORMEntityManager_0000000010991fcd00000000739c3d57f4f8a253a9865f12c8f3d1e0f1f8846b), 'class' => 'AppBundle\\Entity\\GenusNote', 'id_reader' => object(IdReader), 'choice_loader' => object(DoctrineChoiceLoader), 'choice_label' => array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceLabel'), 'choice_name' => array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceName'), 'choice_value' => array(object(IdReader), 'getIdValue'), 'choice_attr' => null, 'preferred_choices' => array(), 'group_by' => null, 'placeholder' => null, 'choice_translation_domain' => false))
    in vendor/symfony/symfony/src/Symfony/Component/Form/ResolvedFormType.php (line 148)
    ResolvedFormType->buildView(object(FormView), object(Form), array('block_name' => null, 'disabled' => false, 'label_format' => null, 'auto_initialize' => true, 'trim' => true, 'property_path' => null, 'mapped' => true, 'by_reference' => true, 'inherit_data' => false, 'method' => 'POST', 'action' => '', 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.', 'help' => null, 'error_mapping' => array(), 'invalid_message' => 'This value is not valid.', 'invalid_message_parameters' => array(), 'allow_extra_fields' => false, 'extra_fields_message' => 'This form should not contain extra fields.', 'csrf_protection' => true, 'csrf_field_name' => '_token', 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.', 'csrf_token_manager' => object(CsrfTokenManager), 'csrf_token_id' => null, 'expanded' => false, 'label' => null, 'attr' => array('multiple' => true, 'data-widget' => 'select2'), 'translation_domain' => 'messages', 'data_class' => null, 'multiple' => true, 'empty_data' => array(), 'required' => false, 'error_bubbling' => false, 'label_attr' => array(), 'compound' => false, 'upload_max_size_message' => object(Closure), 'validation_groups' => null, 'constraints' => array(), 'choices' => null, 'choices_as_values' => true, 'query_builder' => null, 'em' => object(DoctrineORMEntityManager_0000000010991fcd00000000739c3d57f4f8a253a9865f12c8f3d1e0f1f8846b), 'class' => 'AppBundle\\Entity\\GenusNote', 'id_reader' => object(IdReader), 'choice_loader' => object(DoctrineChoiceLoader), 'choice_label' => array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceLabel'), 'choice_name' => array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceName'), 'choice_value' => array(object(IdReader), 'getIdValue'), 'choice_attr' => null, 'preferred_choices' => array(), 'group_by' => null, 'placeholder' => null, 'choice_translation_domain' => false))
    in vendor/symfony/symfony/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php (line 110)
    ResolvedTypeDataCollectorProxy->buildView(object(FormView), object(Form), array('block_name' => null, 'disabled' => false, 'label_format' => null, 'auto_initialize' => true, 'trim' => true, 'property_path' => null, 'mapped' => true, 'by_reference' => true, 'inherit_data' => false, 'method' => 'POST', 'action' => '', 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.', 'help' => null, 'error_mapping' => array(), 'invalid_message' => 'This value is not valid.', 'invalid_message_parameters' => array(), 'allow_extra_fields' => false, 'extra_fields_message' => 'This form should not contain extra fields.', 'csrf_protection' => true, 'csrf_field_name' => '_token', 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.', 'csrf_token_manager' => object(CsrfTokenManager), 'csrf_token_id' => null, 'expanded' => false, 'label' => null, 'attr' => array('multiple' => true, 'data-widget' => 'select2'), 'translation_domain' => 'messages', 'data_class' => null, 'multiple' => true, 'empty_data' => array(), 'required' => false, 'error_bubbling' => false, 'label_attr' => array(), 'compound' => false, 'upload_max_size_message' => object(Closure), 'validation_groups' => null, 'constraints' => array(), 'choices' => null, 'choices_as_values' => true, 'query_builder' => null, 'em' => object(DoctrineORMEntityManager_0000000010991fcd00000000739c3d57f4f8a253a9865f12c8f3d1e0f1f8846b), 'class' => 'AppBundle\\Entity\\GenusNote', 'id_reader' => object(IdReader), 'choice_loader' => object(DoctrineChoiceLoader), 'choice_label' => array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceLabel'), 'choice_name' => array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceName'), 'choice_value' => array(object(IdReader), 'getIdValue'), 'choice_attr' => null, 'preferred_choices' => array(), 'group_by' => null, 'placeholder' => null, 'choice_translation_domain' => false))
    in vendor/symfony/symfony/src/Symfony/Component/Form/ResolvedFormType.php (line 145)
    ResolvedFormType->buildView(object(FormView), object(Form), array('block_name' => null, 'disabled' => false, 'label_format' => null, 'auto_initialize' => true, 'trim' => true, 'property_path' => null, 'mapped' => true, 'by_reference' => true, 'inherit_data' => false, 'method' => 'POST', 'action' => '', 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.', 'help' => null, 'error_mapping' => array(), 'invalid_message' => 'This value is not valid.', 'invalid_message_parameters' => array(), 'allow_extra_fields' => false, 'extra_fields_message' => 'This form should not contain extra fields.', 'csrf_protection' => true, 'csrf_field_name' => '_token', 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.', 'csrf_token_manager' => object(CsrfTokenManager), 'csrf_token_id' => null, 'expanded' => false, 'label' => null, 'attr' => array('multiple' => true, 'data-widget' => 'select2'), 'translation_domain' => 'messages', 'data_class' => null, 'multiple' => true, 'empty_data' => array(), 'required' => false, 'error_bubbling' => false, 'label_attr' => array(), 'compound' => false, 'upload_max_size_message' => object(Closure), 'validation_groups' => null, 'constraints' => array(), 'choices' => null, 'choices_as_values' => true, 'query_builder' => null, 'em' => object(DoctrineORMEntityManager_0000000010991fcd00000000739c3d57f4f8a253a9865f12c8f3d1e0f1f8846b), 'class' => 'AppBundle\\Entity\\GenusNote', 'id_reader' => object(IdReader), 'choice_loader' => object(DoctrineChoiceLoader), 'choice_label' => array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceLabel'), 'choice_name' => array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceName'), 'choice_value' => array(object(IdReader), 'getIdValue'), 'choice_attr' => null, 'preferred_choices' => array(), 'group_by' => null, 'placeholder' => null, 'choice_translation_domain' => false))
    in vendor/symfony/symfony/src/Symfony/Component/Form/Extension/DataCollector/Proxy/ResolvedTypeDataCollectorProxy.php (line 110)
    ResolvedTypeDataCollectorProxy->buildView(object(FormView), object(Form), array('block_name' => null, 'disabled' => false, 'label_format' => null, 'auto_initialize' => true, 'trim' => true, 'property_path' => null, 'mapped' => true, 'by_reference' => true, 'inherit_data' => false, 'method' => 'POST', 'action' => '', 'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.', 'help' => null, 'error_mapping' => array(), 'invalid_message' => 'This value is not valid.', 'invalid_message_parameters' => array(), 'allow_extra_fields' => false, 'extra_fields_message' => 'This form should not contain extra fields.', 'csrf_protection' => true, 'csrf_field_name' => '_token', 'csrf_message' => 'The CSRF token is invalid. Please try to resubmit the form.', 'csrf_token_manager' => object(CsrfTokenManager), 'csrf_token_id' => null, 'expanded' => false, 'label' => null, 'attr' => array('multiple' => true, 'data-widget' => 'select2'), 'translation_domain' => 'messages', 'data_class' => null, 'multiple' => true, 'empty_data' => array(), 'required' => false, 'error_bubbling' => false, 'label_attr' => array(), 'compound' => false, 'upload_max_size_message' => object(Closure), 'validation_groups' => null, 'constraints' => array(), 'choices' => null, 'choices_as_values' => true, 'query_builder' => null, 'em' => object(DoctrineORMEntityManager_0000000010991fcd00000000739c3d57f4f8a253a9865f12c8f3d1e0f1f8846b), 'class' => 'AppBundle\\Entity\\GenusNote', 'id_reader' => object(IdReader), 'choice_loader' => object(DoctrineChoiceLoader), 'choice_label' => array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceLabel'), 'choice_name' => array('Symfony\\Bridge\\Doctrine\\Form\\Type\\DoctrineType', 'createChoiceName'), 'choice_value' => array(object(IdReader), 'getIdValue'), 'choice_attr' => null, 'preferred_choices' => array(), 'group_by' => null, 'placeholder' => null, 'choice_translation_domain' => false))
    in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php (line 1019)
    Form->createView(object(FormView))
    in vendor/symfony/symfony/src/Symfony/Component/Form/Form.php (line 1022)
    Form->createView()
    in vendor/javiereguiluz/easyadmin-bundle/src/Controller/AdminController.php (line 221)
    AdminController->editAction()
    call_user_func_array(array(object(AdminController), 'editAction'), array())
    in vendor/javiereguiluz/easyadmin-bundle/src/Controller/AdminController.php (line 710)
    AdminController->executeDynamicMethod('edit<entityname>Action')
    in vendor/javiereguiluz/easyadmin-bundle/src/Controller/AdminController.php (line 75)
    AdminController->indexAction(object(Request))
    call_user_func_array(array(object(AdminController), 'indexAction'), array(object(Request)))
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php (line 153)
    HttpKernel->handleRaw(object(Request), 1)
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php (line 68)
    HttpKernel->handle(object(Request), 1, true)
    in vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php (line 171)
    Kernel->handle(object(Request))
    in web/app_dev.php (line 30)
    $kernel = new AppKernel('dev', true);$kernel->loadClassCache();$request = Request::createFromGlobals();$response = $kernel->handle($request);$response->send();$kernel->terminate($request, $response);
    require('/Library/WebServer/Documents/ea/web/app_dev.php')
    in vendor/symfony/symfony/src/Symfony/Bundle/WebServerBundle/Resources/router.php (line 42)

    Delete is not working too:
    There is a ForeignKeyConstraintViolationException for the Doctrine entity associated with "Genus". Solution: disable the "delete" action for this entity or configure the "cascade={"remove"}" attribute for the related property in the Doctrine entity. Full exception: An exception occurred while executing 'DELETE FROM genus WHERE id = ?' with params [10]:

    SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails (`easy_admin_tutorial`.`genus_note`, CONSTRAINT `FK_6478FCEC85C4074C` FOREIGN KEY (`genus_id`) REFERENCES `genus` (`id`))

  • 2017-08-02 weaverryan

    Hey Diaconescu Petrisor!

    Ah, nice work! Ok, a few things:

    A) In Post::getPostTags() you wrote return $this->postTags->toArray(); This *might* be fine... but just be careful with this. Usually, when I call a getter method like this, I expect the pure value - the ArrayCollection. I don't expect it to be transformed to an array. So, I think you'll be fine, but I would prefer if you just returned $this->postTags and then updated your data transformer to handle the ArrayCollection there.

    B) About how to *remove* tags when they are removed from an edit Post form. Yes, I would simply clear the tags before adding them. So:


    public function setPostTags($tags){
    // start with this empty
    $this->postTags->clear();

    foreach ($tags->toArray() as $key => $tag){
    if ($this->postTags->contains($tag)){
    continue;
    }
    $this->addTag($tag);
    }

    return $this;
    }

    Because this is a ManyToMany relationships, Doctrine will automatically see that some tags are now removed from postTags and it should delete those rows :).

    Cheers!

  • 2017-08-01 Diaconescu Petrisor

    I modified some fields in Tag and Post model to:
    /**
    * @ORM\ManyToMany(targetEntity="Post", mappedBy="postTags", cascade={"persist"}, orphanRemoval=true)
    * @ORM\JoinTable(name="post_tag")
    */
    private $tagPosts;

    /**
    * @ORM\ManyToMany(targetEntity="Tag", inversedBy="tagPosts", fetch="EXTRA_LAZY")
    * @ORM\JoinTable(name="post_tag")
    */
    private $postTags;

    I know I have some duplication in reverseTranform method in the form and setPostTags in the model.

  • 2017-08-01 Diaconescu Petrisor

    I didn't delve into EasyBundle Admin yet. Currently I try to make this application to work. I mantained the transform part in forem as before but I override __toString() method of Tag model return $this->getTagName(); and in Post model I write


    public function getPostTags(){
    return $this->postTags->toArray();
    }

    before you manage to respond . With these alterations in place transform part is back in business.

    For reverse I wrote :


    function ($tagsAsString){
    $tagNamesArray = explode(', ', $tagsAsString);
    $tagsArrayCollection = new ArrayCollection();

    foreach($tagNamesArray as $key => $tagName){
    $tag = $this->em->getRepository('AppBundle:Tag')->findOneBy(['tagName' => $tagName]);

    if (!$tag){
    $tag = new Tag();
    $tag->setTagName($tagName);
    $this->em->persist($tag);
    $this->em->flush();
    }

    $tagsArrayCollection->add($tag);
    }

    return $tagsArrayCollection;
    }

    In Post model I wrote:


    public function setPostTags($tags){
    foreach ($tags->toArray() as $key => $tag){
    if ($this->postTags->contains($tag)){
    continue;
    }
    $this->addTag($tag);
    }

    return $this;
    }

    I made a service for PostFormType of course.

    These setups works great for new Posts. I'm not so sure how to write this method for edit part. If some post have some tags initially and remove them in form I wish to reflect in database these changes. Duplication should be resolved with these code. I think to begin with an empty slate of tags array collection at the beginning of setPostTagsMethod and fill it only with tags provided in the form. For everything else probably I should invent some removeTag method or so to remove the undesired tags. How would you write this method?

  • 2017-07-31 weaverryan

    At first glance, this controller looks ok to me! But, why do you need a custom controller at all? You should be able to allow EasyAdminBundle to do all this work for you :).

    Cheers!

  • 2017-07-31 weaverryan

    Hi Diaconescu Petrisor!

    Ok, I like your setup :). First, let's talk just about the error. Because you have a field called "postTags", the form system is looking for a setPostTags() method to call when your form submits. The error basically is telling you that it can't figure out how to set the data back onto your Post object. So, add a setPostTags method.

    Now, to actually get this working! Basically, you're close... and you do want to use a data transformer. The problem is that you're starting with an ArrayCollection of Post object. So, the first part of your transformer would look more like this:


    function (ArrayCollection $tagsAsArray) {
    // transform the ArrayCollection to a string
    $tagStrings = array_map(function(Tag $tag) {
    return $tag->getTagName()
    }, $tagsAsArray->toArray());

    return implode(', ', $tagStrings);
    },

    But, the other side of the transformer is a bit more complex: you need to transform a CSV of tags into an ArrayCollection of Tag *objects*. This means you need to (A) use explode to get an array (B) query for Tag objects for each tag string and (C) put the final thing into a new ArrayCollection($tagObjects) object. This is the example you want to follow in the docs: https://symfony.com/doc/cur...

    Let me know if that helps!

    Cheers!

  • 2017-07-31 Victor Bocharsky

    Hey Antsen,

    That's weird. Please, also make sure you register this bundle in AppKernel.php before doing those commands. Looks like your application just do not see this bundle or you have a permissions issue, but I suppose the command should fail in this case.

    Cheers!

  • 2017-07-30 Diaconescu Petrisor

    Most probably controller isn't ok too:
    /**
    * @Route("/post/new", name="admin_new_post")
    *
    */
    public function newAction(Request $request){
    $post = new Post();
    $form = $this->createForm(PostFormType::class, $post);

    // only handles data on POST
    $form->handleRequest($request);
    if ($form->isSubmitted() && $form->isValid()) {
    $post = $form->getData();
    $em = $this->getDoctrine()->getManager();

    $em->persist($post);
    $em->flush();
    $this->addFlash('success', 'Post created');

    return $this->redirectToRoute('admin_list_posts');
    } else if ($form->isSubmitted() && !$form->isValid()){
    $this->addFlash('failed', "fail to create post");
    }

    return $this->render(
    'admin/post/new.html.twig',
    array(
    'postForm' => $form->createView()
    )
    );
    }

  • 2017-07-30 Diaconescu Petrisor

    I have Post model like this:



    namespace AppBundle\Entity;

    use Doctrine\ORM\Mapping as ORM;
    use Doctrine\Common\Collections\ArrayCollection;
    use Symfony\Component\Validator\Constraints as Assert;

    /**
    * Post
    *
    * @ORM\Table(name="post")
    * @ORM\Entity(repositoryClass="AppBundle\Repository\PostRepository")
    */
    class Post
    {
    /**
    * @var int
    *
    * @ORM\Column(name="id", type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    private $id;
    /**
    * @ORM\ManyToMany(targetEntity="Tag", cascade={"persist"})
    * @ORM\JoinTable(name="post_tag")
    */
    private $postTags;

    public function __construct()
    {
    $this->postTags = new ArrayCollection();
    }

    public function getPostTags()
    {
    $postTagsArrayName = [];
    foreach ($this->postTags as $key => $tag) {
    array_push($postTagsName, $tag->getTagName());
    }
    return $postTagsArrayName;
    }

    public function getPostTagsName()
    {
    $postTagsName = [];
    foreach ($this->postTags as $key => $tag) {
    array_push($postTagsName, $tag->getTagName());
    }
    return implode(', ', $postTagsName);
    }

    public function addTag(Tag $tag)
    {
    $this->postTags->add($tag);
    }
    }

    I have also PostFormType like this, I tried to use ModelTransformer like in Symfony Docs. But transform and reverseTransform functions doesn't use Tags ArrayCollection as far as I understand, but only an array of tag names:


    namespace AppBundle\Form;

    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\FormBuilderInterface;
    use Symfony\Component\OptionsResolver\OptionsResolver;
    use Symfony\Bridge\Doctrine\Form\Type\EntityType;
    use AppBundle\Entity\Category;
    use AppBundle\Repository\CategoryRepository;
    use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
    use Symfony\Component\Form\Extension\Core\Type\TextareaType;
    use Symfony\Component\Form\Extension\Core\Type\FileType;
    use Symfony\Component\Form\Extension\Core\Type\TextType;
    use Symfony\Component\Form\CallbackTransformer;

    class PostFormType extends AbstractType
    {
    /**
    * {@inheritdoc}
    */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder
    ->add('postTitle', TextType::class)
    ->add('postAuthor', TextType::class)
    ->add('postContent', TextareaType::class)
    ->add('postCategory', EntityType::class, [
    'placeholder' => 'Choose a Category',
    'class' => 'AppBundle:Category',
    'query_builder' => function (CategoryRepository $repo) {
    return $repo->createAlphabeticalQueryBuilder();
    }
    ])
    ->add('postStatus', ChoiceType::class, array(
    'choices' => array(
    'published' => 'published',
    'not_published' => 'draft'
    ),
    'expanded' => TRUE
    ))
    ->add('postImage', FileType::class, array('data_class' => null))
    ->add('postTags', TextType::class);

    $builder
    ->get('postTags')
    ->addModelTransformer(new CallbackTransformer(
    function ($tagsAsArray) {
    //transform the array to a string
    return implode(', ', $tagsAsArray);
    },
    function ($tagsAsString) {
    //transform the string back to an array
    return explode(', ', $tagsAsString);
    }
    ));
    }

    /**
    * {@inheritdoc}
    */
    public function configureOptions(OptionsResolver $resolver)
    {
    $resolver->setDefaults(array(
    'data_class' => 'AppBundle\Entity\Post'
    ));
    }
    }

    Tag model is like this:


    namespace AppBundle\Entity;

    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Validator\Constraints as Assert;

    /**
    * Tag
    *
    * @ORM\Table(name="tag")
    * @ORM\Entity(repositoryClass="AppBundle\Repository\TagRepository")
    */
    class Tag
    {
    /**
    * @var int
    *
    * @ORM\Column(name="id", type="integer")
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    private $id;

    /**
    * @var string
    * @ORM\Column(name="tag_name", type="string", length=255)
    * @Assert\NotBlank()
    */
    private $tagName;

    public function getTagName()
    {
    return $this->tagName;
    }

    public function setTagName($tagName)
    {
    $this->tagName = $tagName;
    }
    }

    And finally the form twig template is like so (_form.html.twig):


    {% form_theme postForm 'form/rows.html.twig' %}

    {{ form_start(postForm) }}
    {{ form_errors(postForm) }}

    {{ form_row(postForm.postTitle, {
    'label': 'Post Name',
    'attr': {
    'placeholder': "Give this post a name",
    'class' : 'form-control'
    }

    }) }}
    {{ form_row(postForm.postAuthor, {
    'attr' : {
    'placeholder': "Specify the author of this post",
    'class' : 'form-control'
    }
    }) }}
    {{ form_row(postForm.postContent, {
    'attr' : {
    'placeholder': "Fill in some content",
    'class' : 'form-control'
    }
    }) }}
    {{ form_row(postForm.postCategory, {
    'attr' : {
    'class' : 'form-control'
    }
    }) }}
    {{ form_row(postForm.postImage) }}
    {{ form_row(postForm.postStatus, {
    'label' : 'Is Published?'
    }) }}
    {{ form_row(postForm.postTags, {
    'attr' : {
    'placeholder': "Some tags separated by a coma. Eg: ruby, symfony",
    'class' : 'form-control'
    }
    }) }}

    <div class="form-group">
    <input type="submit" class="btn btn-primary" name="submit" value="Save"/>
    </div>

    {{ form_end(postForm) }}

    What I must write on Post model for this to work? I must write on Tag model part too? Example is here (https://symfony.com/doc/cur.... If i'd use something like src/AppBundle/Form/DataTransformer/IssueToNumberTransformer.php in the second example how first example should be translated into an eventually TagsCollectionTransformer? What I should write on model part in this case? Now I tried to use first example but I miss something because when I submit the new form I obtain this error -> Could not determine access type for property "postTags" .

  • 2017-07-28 Antsen

    Hi Victor,
    I'm under Windows7. I had already tried clearing the cache and re running the install assets command (with symlink & a hard copy) but it just won't work. Thanks for your reply anyways. Since i'm running out of time i'll just do it by hand.

  • 2017-07-26 Victor Bocharsky

    Hey Antsen,

    That's really weird because installing assets had to do the trick. Could you clear the cache first, and then try to install assets again? Btw, what OS are you on? try to do install assets by create a hard copy of them, not symlink them:


    $ rm -rf var/cache/*
    $ bin/console assets:install

    Does it fix the problem?

    Cheers!

  • 2017-07-25 Antsen

    Of course it works if i do it by hand : create the /web/bundles/easyadmin/ : /stylesheet , /javascript & /fonts folders and put the files there by hand (taken from the vendor /ressources/public folders of the bundle). But i want to make it work the right way.

  • 2017-07-25 Antsen

    Hi guys,
    For some reasons the easyadmin-all.css & easyadmin-all.js files a not loaded. When i check in my browser's console they both have a 404. When i open the source code of the page and try to click both css & js links it opens them normally. Also, i don't understand why i literraly have no /web/bundles/easyadmin folder with those 2 files. Using the assets:install cmd gives me this msg : " [OK] All assets were successfully installed. "

    So right now i'm stuck with a pretty ugly interface... and i have no clue how to make the easyadminbundle assets install and work properly.

    Any ideas ?

  • 2017-07-20 Diaconescu Petrisor

    Cheers. Thank you very much weaverryan. You are very kind. Works like a dream.

  • 2017-07-19 weaverryan

    Hi Diaconescu Petrisor!

    I know the issue! In JavaScript, you cannot add line breaks inside a string. I know, it seems kind of crazy :). But as soon as you add the new line after the h3 tag, that string is now a syntax error. Here's a similar, but better way to do what you want:


    {#
    it's totally valid to put a little "template" inside a script tag. Your browser does not parse this,
    but it's available for you to read in JavaScript.
    #}
    <script type="text/template" id="admin-category-edit-template">
    {{ include("admin/category/edit.html.twig") }}
    </script>

    <script type="text/javascript">
    $(function(){
    var html = $('#admin-category-edit-template').html();
    $('table').on('click', '.js-category-edit', function(event){
    event.preventDefault();
    });
    })
    </script>

    Let me know if that helps!

  • 2017-07-19 Diaconescu Petrisor

    I try to embed in list.html.twig template some form with javascript. I tried with this script


    <script type="text/javascript">
    $(function(){
    html = '{{ include("admin/category/edit.html.twig") }}';
    $('table').on('click', '.js-category-edit', function(event){
    event.preventDefault();


    });
    })
    </script>

    Idea is to modify after afterwards the action attribute and use a promise to modify category name but I got error in javascript. If I use the same include syntax in html tag I have no problem.

    In Javascript I got incessantly:
    Uncaught SyntaxError: Invalid or unexpected token


    $(function(){
    html = '<h3>Edit Category</h3>\**I have a red cross right here**\

    <form name="category_form" method="post">


    <div class="form-group"><label for="category_form_catTitle" class="required">Category</label><input type="text" id="category_form_catTitle" name="category_form[catTitle]" required="required" maxlength="255" class="form-control" placeholder="Category name"/></div>

    <div class="form-group">
    <input type="submit" class="btn btn-primary" name="submit" value="Save"/>
    </div>

    <input type="hidden" id="category_form__token" name="category_form[_token]" value="AlB6SZHwdEhQDe1vNV3_3f7bOq_bjKFqjownijJhDXY"/>
    </form>
    ';

    Is there something I can do?

  • 2017-07-19 Victor Bocharsky

    Hey Gaetan,

    You're right, and we use the same FQCN in this screencast, note the use statement in AppKernel.php ;)

    Cheers!

  • 2017-07-19 Gaétan Darquié

    It seems we have to add : "new JavierEguiluz\Bundle\EasyAdminBundle\EasyAdminBundle()," in AppKernell now.

  • 2017-07-13 weaverryan

    Ok, it's definitely the second option - with the \\. But why the server crashed? I'm not sure - that's very interesting. I would temporarily add an @Route("/test/tmp") annotation above the embedded controller and then go to /test/tmp to see if the page works on its own :).

    Cheers!

  • 2017-07-13 Diaconescu Petrisor

    Cheers!
    i tried {{ render(controller('AppBundle\Controller\Admin\CategoryAdminController::newAction' )) }} but it says An exception has been thrown during the rendering of a template ("Class "AppBundleControllerAdminCategoryAdminController" does not exist.").
    I tried {{ render(controller('AppBundle\\Controller\\Admin\\CategoryAdminController::newAction' )) }} but after some period of time simply blows out completely. Server simply crashes.
    I tried {{ render(controller('AppBundle/Controller/Admin/CategoryAdminController::testAction')) }} but it says An exception has been thrown during the rendering of a template ("Class "AppBundle/Controller/Admin/CategoryAdminController" does not exist.").

    Thank for your attention.

  • 2017-07-13 weaverryan

    Hey Diaconescu Petrisor!

    Ah, you're close! It should be:


    {{ render(controller('AppBundle:Admin\CategoryAdmin:new')) }}

    It's just a change from / to \ (you may need \\ instead of a single \ due to escaping problems, I can;'t remember. If a single \ doesn't work, try 2). This is because you're really pointing to the *namespace* of this class, not the subdirectory. Alternatively, you can always use the "long" format:


    {{ render(controller('AppBundle\Controller\Admin\CategoryAdminController::newAction')) }}

    This is actually what the first string "expands" into internally. Symfony really wants the format ControllerClassName::methodName.

    Cheers!

  • 2017-07-12 Diaconescu Petrisor

    I want to render in some template a controller that lies under Admin subdirectory in Controller directory
    The name of the controller is CategoryAdminController.
    How to write this?
    {{ render(controller("AppBundle:Admin/CategoryAdmin:new")) }} is not correct but I don't find valid reference for this situation and I don't know how to correct this sintax