sonata-project / SonataAdminBundle

The missing Symfony Admin Generator
https://docs.sonata-project.org/projects/SonataAdminBundle
MIT License
2.11k stars 1.26k forks source link

The problem with Doctrine ORM Class Table Inheritance in nested forms #4543

Closed vyshkant closed 6 years ago

vyshkant commented 7 years ago

Environment

Sonata packages ```bash $ composer show sonata-project/* sonata-project/admin-bundle 3.20.1 The missing Symfony Admin Generator sonata-project/block-bundle 3.3.2 Symfony SonataBlockBundle sonata-project/cache 1.0.7 Cache library sonata-project/core-bundle 3.4.0 Symfony SonataCoreBundle sonata-project/datagrid-bundle 2.2.1 Symfony SonataDatagridBundle sonata-project/doctrine-extensions 1.0.2 Doctrine2 behavioral extensions sonata-project/doctrine-orm-admin-bundle 3.1.5 Symfony Sonata / Integrate Doctrine ORM into the SonataAdminBundle sonata-project/easy-extends-bundle 2.2.0 Symfony SonataEasyExtendsBundle sonata-project/exporter 1.7.1 Lightweight Exporter library sonata-project/media-bundle 3.5.1 Symfony SonataMediaBundle sonata-project/notification-bundle 3.1.0 Symfony SonataNotificationBundle ```
Symfony packages ```bash $ composer show symfony/* symfony/assetic-bundle v2.8.1 Integrates Assetic into Symfony2 symfony/monolog-bundle v3.1.0 Symfony MonologBundle symfony/phpunit-bridge v3.3.3 Symfony PHPUnit Bridge symfony/polyfill-apcu v1.4.0 Symfony polyfill backporting apcu_* functions to lower PHP versions symfony/polyfill-intl-icu v1.4.0 Symfony polyfill for intl's ICU-related data and classes symfony/polyfill-mbstring v1.4.0 Symfony polyfill for the Mbstring extension symfony/polyfill-php56 v1.4.0 Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions symfony/polyfill-php70 v1.4.0 Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions symfony/polyfill-util v1.4.0 Symfony utilities for portability of PHP codes symfony/security-acl v3.0.0 Symfony Security Component - ACL (Access Control List) symfony/swiftmailer-bundle v2.6.2 Symfony SwiftmailerBundle symfony/symfony v3.2.10 The Symfony PHP framework ```
Doctrine packages ```bash $ composer show doctrine/* doctrine/annotations v1.4.0 Docblock Annotations Parser doctrine/cache v1.6.1 Caching library offering an object-oriented API for many cache backends doctrine/collections v1.4.0 Collections Abstraction library doctrine/common v2.7.2 Common Library for Doctrine projects doctrine/data-fixtures v1.2.2 Data Fixtures for all Doctrine Object Managers doctrine/dbal v2.5.12 Database Abstraction Layer doctrine/doctrine-bundle 1.6.8 Symfony DoctrineBundle doctrine/doctrine-cache-bundle 1.3.0 Symfony Bundle for Doctrine Cache doctrine/doctrine-fixtures-bundle 2.3.0 Symfony DoctrineFixturesBundle doctrine/doctrine-migrations-bundle v1.2.1 Symfony DoctrineMigrationsBundle doctrine/inflector v1.1.0 Common String Manipulations with regard to casing and singular/plural rules. doctrine/instantiator 1.0.5 A small, lightweight utility to instantiate objects in PHP without invoking their constructors doctrine/lexer v1.0.1 Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers. doctrine/migrations v1.5.0 Database Schema migrations using Doctrine DBAL doctrine/orm v2.5.6 Object-Relational-Mapper for PHP ```
PHP version ```bash $ php -v PHP 7.1.6-1~ubuntu16.04.1+deb.sury.org+1 (cli) (built: Jun 9 2017 08:26:34) ( NTS ) Copyright (c) 1997-2017 The PHP Group Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies with Zend OPcache v7.1.6-1~ubuntu16.04.1+deb.sury.org+1, Copyright (c) 1999-2017, by Zend Technologies with Xdebug v2.5.5, Copyright (c) 2002-2017, by Derick Rethans ```

Subject

Suppose that we need to describe at the database level and administer using SonataAdminBundle the following data structure.

There is a document. A document can consist of several pages. The page can be written by hand, or printed on the printer. The handwritten and printed pages have very different attributes.

For this purpose, the following data structure was created.

The Document doctrine-entity has a pages field, which is an array of Page doctrine-entities. Document is linked to the Page by the OneToMany relation.

Document class ```php /** * @ORM\Table(name="document") * @ORM\Entity */ class Document { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var Page[] * * @ORM\OneToMany(targetEntity="Page", cascade={"persist"}, mappedBy="document", orphanRemoval=true) * @ORM\OrderBy({"name": "ASC"}) */ private $pages; } ```


The Page doctrine-entity has the property wayOfObtaining, the value of which is one of the child entities of the class AbstractWayOfObtaining (more about this below). Page is connected to the AbstractWayOfObtaining by the OneToOne relation.

Page class ```php /** * @ORM\Table(name="page") * @ORM\Entity */ class Page { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var AbstractWayOfObtaining * * @ORM\OneToOne(targetEntity="AbstractWayOfObtaining", cascade={"persist"}) * @ORM\JoinColumn(name="way_of_obtaining_id", referencedColumnName="id") */ private $wayOfObtaining; /** * @var Document * * @ORM\ManyToOne(targetEntity="Document", cascade={"persist"}, inversedBy="pages") * @ORM\JoinColumn(name="document_id", referencedColumnName="id", nullable=false) */ private $document; } ```


The AbstractWayOfObtaining doctrine-entity is an abstract class used as the base class in the Doctrine ORM Class Table Inheritance mapping.

AbstractWayOfObtaining class ```php /** * @ORM\Table(name="way_of_obtaining") * @ORM\Entity * @ORM\InheritanceType("JOINED") * @ORM\DiscriminatorColumn(name="obtain_type", type="string") * @ORM\DiscriminatorMap({"handwritten" = "HandwriteWayOfObtaining", "printed" = "PrintWayOfObtaining"}) */ abstract class AbstractWayOfObtaining { /** * @var int * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; } ```


The AbstractWayOfObtaining class has two child doctrine-entities classes: HandwriteWayOfObtaining and PrintWayOfObtaining, the data for each of which are very different, therefore the entities are stored in different tables.

HandwriteWayOfObtaining class ```php /** * @ORM\Table(name="handwrite_way_of_obtaining") * @ORM\Entity */ class HandwriteWayOfObtaining extends AbstractWayOfObtaining { } /** * @ORM\Table(name="print_way_of_obtaining") * @ORM\Entity */ class PrintWayOfObtaining extends AbstractWayOfObtaining { } ```


This is what is.

My task is to administer this structure using SonataAdminBundle.

For these purposes I created a service of admin class AbstractWayOfObtainingAdmin. Since the entities of the AbstractWayOfObtaining are related to the Page entity by the OneToOne connection, I did not create a separate page for this admin class (show_in_dashboard: false).

AbstractWayOfObtainingAdmin service configuration ``` admin.abstract_way_of_obtaining: class: MyBundle\Admin\AbstractWayOfObtainingAdmin arguments: [~, MyBundle\Entity\AbstractWayOfObtaining, ~] calls: - [addSubClass, [MyBundle\Entity\HandwriteWayOfObtaining]] - [addSubClass, [MyBundle\Entity\PrintWayOfObtaining]] tags: - name: sonata.admin manager_type: orm show_in_dashboard: false ```


In the admin class AbstractWayOfObtainingAdmin in the configureFormFields method, I made sure that the fields are configured depending on which entity is actually present.

AbstractWayOfObtainingAdmin class ```php class AbstractWayOfObtainingAdmin extends AbstractAdmin { protected function configureFormFields(FormMapper $formMapper) { $subject = $this->getSubject(); if ($subject instanceof HandwriteWayOfObtaining) { $formMapper ->add('propertyOfHandwrite', TextType::class) // adding other HandwriteWayOfObtaining fields ; } elseif ($subject instanceof PrintWayOfObtaining) { $formMapper ->add('propertyOfPrint', TextType::class) // adding other PrintWayOfObtaining fields ; } } } ```


Next, I configured the admin class PageAdmin:

PageAdmin class ```php class PageAdmin extends AbstractAdmin { protected function configureFormFields(FormMapper $formMapper) { $formMapper ->add('wayOfObtaining', AdminType::class, array( 'data_class' => null, ), array( 'admin_code' => 'admin.abstract_way_of_obtaining', )) ; } } ```


The PageAdmin class is not shown in the admin panel itself, it is embade to DocumentAdmin page:

DocumentAdmin class ```php class DocumentAdmin extends AbstractAdmin { protected function configureFormFields(FormMapper $formMapper) { $formMapper ->add('pages', CollectionType::class, array( 'required' => false, ), array( 'edit' => 'inline', 'inline' => 'table', 'admin_code' => 'admin.page', )) ; } } ```


My goal is to make it so that you can go into the document entity admin and edit its attributes (including pages, including obtaining way of each page).

As a result, the code shown above allows me to view the already create data. And nothing more:

1) When I try to save the form data (for example, with the HandwriteWayOfObtaining object), I see an error:

Neither the property "propertyOfPrint" nor one of the methods "getPropertyOfPrint()", "propertyOfPrint()", "isPropertyOfPrint()", "hasPropertyOfPrint()", "__get()" exist and have public access in class "MyBundle\Entity\HandwriteWayOfObtaining".

Here is backtrace:

[1] Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException: Neither the property "propertyOfPrint" nor one of the methods "getPropertyOfPrint()", "propertyOfPrint()", "isPropertyOfPrint()", "hasPropertyOfPrint()", "__get()" exist and have public access in class "MyBundle\Entity\HandwriteWayOfObtaining". at n/a in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php line 505 at Symfony\Component\PropertyAccess\PropertyAccessor->readProperty(array(object(HandwriteWayOfObtaining)), 'excavation') in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php line 406 at Symfony\Component\PropertyAccess\PropertyAccessor->readPropertiesUntil(array(object(HandwriteWayOfObtaining)), object(PropertyPath), 1, true) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php line 178 at Symfony\Component\PropertyAccess\PropertyAccessor->getValue(object(HandwriteWayOfObtaining), object(PropertyPath)) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php line 58 at Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper->mapDataToForms(object(HandwriteWayOfObtaining), object(RecursiveIteratorIterator)) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/Form/Form.php line 387 at Symfony\Component\Form\Form->setData(object(HandwriteWayOfObtaining)) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php line 58 at Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper->mapDataToForms(object(Page), object(RecursiveIteratorIterator)) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/Form/Form.php line 387 at Symfony\Component\Form\Form->setData(object(Page)) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php line 58 at Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper->mapDataToForms(object(PersistentCollection), object(RecursiveIteratorIterator)) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/Form/Form.php line 884 at Symfony\Component\Form\Form->add(object(Form), 'sonata_type_admin', array('sonata_field_description' => object(FieldDescription), 'data_class' => 'MyBundle\\Entity\\Page', 'property_path' => '[0]', 'auto_initialize' => false)) in /path/to/my/project/vendor/sonata-project/core-bundle/Form/EventListener/ResizeFormListener.php line 175 at Sonata\CoreBundle\Form\EventListener\ResizeFormListener->preSubmit(object(FormEvent)) in /path/to/my/project/vendor/sonata-project/core-bundle/Form/EventListener/ResizeFormListener.php line 132 at Sonata\CoreBundle\Form\EventListener\ResizeFormListener->preBind(object(FormEvent), 'form.pre_bind', object(EventDispatcher)) in line at call_user_func(array(object(ResizeFormListener), 'preBind'), object(FormEvent), 'form.pre_bind', object(EventDispatcher)) in /path/to/my/project/var/cache/dev/classes.php line 15581 at Symfony\Component\EventDispatcher\EventDispatcher->doDispatch(array(array(object(TrimListener), 'preSubmit'), array(object(CsrfValidationListener), 'preSubmit'), array(object(ResizeFormListener), 'preBind')), 'form.pre_bind', object(FormEvent)) in /path/to/my/project/var/cache/dev/classes.php line 15496 at Symfony\Component\EventDispatcher\EventDispatcher->dispatch('form.pre_bind', object(FormEvent)) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/ImmutableEventDispatcher.php line 43 at Symfony\Component\EventDispatcher\ImmutableEventDispatcher->dispatch('form.pre_bind', object(FormEvent)) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/Form/Form.php line 555 at Symfony\Component\Form\Form->submit(array(array('wayOfObtaining' => array('propertyOfHandwrite' => 'value'))), true) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/Form/Form.php line 576 at Symfony\Component\Form\Form->submit(array('pages' => array(array('wayOfObtaining' => array('propertyOfHandwrite' => 'value')))), true) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php line 113 at Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler->handleRequest(object(Form), object(Request)) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/Form/Form.php line 502 at Symfony\Component\Form\Form->handleRequest(object(Request)) in /path/to/my/project/vendor/sonata-project/admin-bundle/Controller/CRUDController.php line 257 at Sonata\AdminBundle\Controller\CRUDController->editAction('559') in line at call_user_func_array(array(object(CRUDController), 'editAction'), array('559')) in /path/to/my/project/var/cache/dev/classes.php line 16525 at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1) in /path/to/my/project/var/cache/dev/classes.php line 16480 at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true) in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php line 168 at Symfony\Component\HttpKernel\Kernel->handle(object(Request)) in /path/to/my/project/web/app_dev.php line 28

As you can see, there is an attempt to access the data of class PrintWayOfObtaining on the object of class HandwriteWayOfObtaining.

So I cannot save the form.

2) When I try to add one more page (the link_add button), the wayOfObtaining section of just created page form is empty (that is, there are no fields of the HandwriteWayOfObtaining entity, nor the fields of the PrintWayOfObtaining entity, nor the possibility of selecting the desired entity).

So I cannot add new Page to the Document using SonataAdminBundle.

My question is: am I doing something wrong, or is this a probable bug? What should I do to be able to edit a document, pages and obtaining ways using DocumentAdmin?

I'll gladly give you any information and write any patches, if you tell me which way to look and give some advice.

vyshkant commented 7 years ago

Any ideas about this? Maybe I'm doing something wrong?

What is the expected behavior on my case? How should the target entity choice possibility be shown? Should it be the radio button, or dropdown, or something like the provider selection window in SonataMediaBundle?

vyshkant commented 6 years ago

@greg0ire @OskarStark @Soullivaneuh @core23 @jordisala1991

Sorry for concern, but I would like to understand: is the functionality described in the "Subject" section supported by SonataAdminBundle?

How should I configure SonataAdminBundle and admin pages to be able to choose (change or set for new objects) one of the classes in the "CTI" model? Is this functionality provided by SonataAdminBundle?

I would gladly write a patch, but, of course, I really need your advice.

greg0ire commented 6 years ago

Hello, I can't help because I never used CTI with admin bundle. I remember using inheritance, but I think it wasn't CTI. I'd say that anything that is not documented should be considered not supported.

If you want to contribute something, there will be a need for detailed docs and tests, because this is quite complicated, as you have shown.

vyshkant commented 6 years ago

I think it's quite obvious that this functionality requires some discussion. If I decide to add this functionality (to write a PR), should I open a new issue, or can I put my thoughts here?

greg0ire commented 6 years ago

You can put your thoughts here, or you can create a PR with only docs, as you wish.

jordisala1991 commented 6 years ago

IMO this issue is almost the same as #4519 but there is a differenece on how you want to access the inheritance.

I would like to centralize all the conversation on one issue. WDYT on closing this one?

vyshkant commented 6 years ago

@jordisala1991 I'm not sure it's really the same. But I'm also not sure if my issue is up-to-date.

So I think it can be closed (at least for now).

jordisala1991 commented 6 years ago

Ok, ping me to reopen or reopen yourself when you find out.

AFAIK there is no more work for inheritance on Sonata since you posted your issue, so if it was not working before, it wont work now.

johnpancoast commented 6 years ago

IMO, this is a general issue of sonata admin not being able to handle discriminator mapped fields and should either be stayed open or another issue should be created (whether it's a "bug" or "feature request" is another question), but it's most definitely something that admin would ideally support. I don't have the solutions, but it's definitely a roadblock I'm hitting at the moment.

I currently have a doctrine CTI setup (User parent with Employee and Customer subclasses and more possibly in the future) and the only reason I chose this option for my data in the first place was because if I instead had one User entity with my own type field (for example), then I'd need to specify admin_code in many places in my code to avoid ambiguity because I'd have 2 or more admins pointing to one entity.

smilesrg commented 8 months ago

Would be super useful, it is really a roadblock....