Sonataadminbundle: I'm unable to make Sonata work with nested entities, could anyone help?

Created on 21 Aug 2017  路  3Comments  路  Source: sonata-project/SonataAdminBundle

I'm not sure if it's some sort of bug or not, but I have serious problem with making it working, I must missing some important detail, that I couldn't find reading documentation and a lot of issues here.

Setup

My Entities:

<?php

namespace AppBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="educational_module")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\CourseUnitRepository")
 *
 * Class EducationalUnit
 */
class EducationalModule
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\EducationalUnitCategory", inversedBy="educationalUnits")
     * @ORM\JoinColumn(name="category", referencedColumnName="course")
     */
    private $category;

    /**
     * @var string
     *
     * @ORM\Column(type="string", nullable=false)
     */
    private $name;

    /**
     * @ORM\OneToMany(targetEntity="AppBundle\Entity\Course", mappedBy="module")
     */
    private $courses;

    /**
     * @ORM\OneToMany(targetEntity="AppBundle\Entity\EducationalUnit", mappedBy="module", cascade={"persist","remove"})
     */
    private $units;


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

    /**
     * @return mixed
     */
    public function getCategory()
    {
        return $this->category;
    }

    /**
     * @param mixed $category
     */
    public function setCategory($category)
    {
        $this->category = $category;
    }

    /**
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getName() : ?string
    {
        return $this->name;
    }

    /**
     * @param string $name
     */
    public function setName(string $name)
    {
        $this->name = $name;
    }

    /**
     * @return mixed
     */
    public function getCourses()
    {
        return $this->courses;
    }

    /**
     * @return mixed
     */
    public function getUnits()
    {
        return $this->units;
    }

    public function setUnits($units)
    {
        $this->units = new ArrayCollection();


        foreach ($units as $unit) {
            $this->addUnits($unit);
        }
    }


    public function addUnits(EducationalUnit $file)
    {
        $this->units->add($file);
    }


    public function removeUnits($file)
    {
        $this->units->removeElement($file);
    }
}
<?php

namespace AppBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="educational_unit")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\CourseUnitRepository")
 *
 * Class EducationalUnit
 */
class EducationalUnit
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\EducationalModule", inversedBy="units")
     */
    private $module;

    /**
     * @ORM\Column(type="integer", options={"default" : 0})
     */
    private $position = 0;

    /**
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\EducationalUnitCategory", inversedBy="educationalUnits")
     * @ORM\JoinColumn(name="category", referencedColumnName="course")
     */
    private $category;

    /**
     * @var \Doctrine\Common\Collections\ArrayCollection
     *
     * @ORM\OneToMany(targetEntity="AppBundle\Entity\EducationalFile", mappedBy="unit", cascade={"persist","remove"})
     */
    protected $files;



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


    /**
     * @return mixed
     */
    public function getCategory()
    {
        return $this->category;
    }

    /**
     * @param mixed $category
     */
    public function setCategory($category)
    {
        $this->category = $category;
    }

    /**
     * @return mixed
     */
    public function getModule()
    {
        return $this->module;
    }


    public function setModule($module)
    {
        $this->module = $module;
    }

    /**
     * @return mixed
     */
    public function getPosition()
    {
        return $this->position;
    }

    /**
     * @param mixed $position
     */
    public function setPosition($position)
    {
        $this->position = $position;
    }

    /**
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return \Doctrine\Common\Collections\ArrayCollection
     */
    public function getFiles(): ArrayCollection
    {
        return $this->files;
    }


    public function setFiles($files)
    {
        $this->files = new ArrayCollection();


        foreach ($files as $file) {
            $this->addFiles($file);
        }
    }


    public function addFiles(EducationalFile $file)
    {
        $this->files->add($file);
    }


    public function removeFiles($file)
    {
        $this->files->removeElement($file);
    }
}
<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
 * Class EducationalFile
 *
 * @ORM\Entity
 * @Vich\Uploadable
 * @ORM\HasLifecycleCallbacks
 */
class EducationalFile
{
    /**
      * @ORM\Id
      * @ORM\Column(type="integer")
      * @ORM\GeneratedValue(strategy="AUTO")
      */
    private $id;


    /**
     * NOTE: This is not a mapped field of entity metadata, just a simple property.
     *
     * @Vich\UploadableField(mapping="educational_file", fileNameProperty="fileName", size="fileSize")
     *
     * @var File
     */
    private $educational_file;

    /**
     * @ORM\Column(type="string", length=255)
     *
     * @var string
     */
    private $fileName;

    /**
     * @ORM\Column(type="integer")
     *
     * @var integer
     */
    private $fileSize;

    /**
     * @ORM\Column(type="datetime")
     *
     * @var \DateTime
     */
    private $updatedAt;

    /**
     * @var EducationalUnit
     *
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\EducationalUnit", inversedBy="files")
     */
    private $unit;

    /**
     * If manually uploading a file (i.e. not using Symfony Form) ensure an instance
     * of 'UploadedFile' is injected into this setter to trigger the  update. If this
     * bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
     * must be able to accept an instance of 'File' as the bundle will inject one here
     * during Doctrine hydration.
     *
     * @param File|\Symfony\Component\HttpFoundation\File\UploadedFile $file
     *
     * @return EducationalFile
     */
    public function setEducationalFile(File $file = null)
    {
        $this->educational_file = $file;

        if ($file) {
            // It is required that at least one field changes if you are using doctrine
            // otherwise the event listeners won't be called and the file is lost
            $this->updatedAt = new \DateTimeImmutable();
        }

        return $this;
    }

    /**
     * @return File|null
     */
    public function getEducationalFile()
    {
        return $this->educational_file;
    }

    /**
     * @param string $imageName
     *
     * @return EducationalFile
     */
    public function setFileName($imageName)
    {
        $this->fileName = $imageName;

        return $this;
    }

    /**
     * @return string|null
     */
    public function getFileName()
    {
        return $this->fileName;
    }

    /**
     * @param integer $imageSize
     *
     * @return EducationalFile
     */
    public function setFileSize($imageSize)
    {
        $this->fileSize = $imageSize;

        return $this;
    }

    /**
     * @return integer|null
     */
    public function getFileSize()
    {
        return $this->fileSize;
    }

    /**
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return mixed
     */
    public function getUnit()
    {
        return $this->unit;
    }

    public function setUnit($unit)
    {
        $this->unit = $unit;
    }
}

As you can see I'm also using VichUploaderBundle but I think it's irrelevant here.

Next are my admin classes:

<?php

namespace AppBundle\Admin;

use AppBundle\Entity\EducationalFile;
use AppBundle\Entity\EducationalModule;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;

/**
 * Class EducationalModuleAdmin
 */
class EducationalModuleAdmin extends AbstractAdmin
{
    protected function configureFormFields(FormMapper $formMapper)
    {

        $formMapper->add('name', 'text');
        $formMapper->add('units', 'sonata_type_collection', [
            'required' => false,
            'by_reference' => false,
            'label' => 'Units',
            'type_options' => array(
                // Prevents the "Delete" option from being displayed
                'delete' => false,
            ),
        ], [
            'edit' => 'inline',
            'inline' => 'table',
            'sortable' => 'position',
            //'limit' => 1,
        ]);

    }

    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper->add('id');
        $datagridMapper->add('name');
    }

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper->add('id');
        $listMapper->add('name');
    }
}

/**
 * Class EducationalUnitAdmin
 */
class EducationalUnitAdmin extends AbstractAdmin
{
    protected $parentAssociationMapping = 'module';

    protected function configureFormFields(FormMapper $formMapper)
    {
        //$formMapper->add('module', 'sonata_type_model_hidden');
        $formMapper->add('files', 'sonata_type_collection', [
            'required' => false,
            'by_reference' => false,
            'label' => 'Files',
            'type_options' => array(
                // Prevents the "Delete" option from being displayed
                'delete' => false,
            ),
        ], [
            'edit' => 'inline',
            'inline' => 'table',
            //'sortable' => 'position',
            'limit' => 1,
        ]);
    }

    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper->add('id');
    }

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper->add('id');
    }

    /**
     * @param EducationalModule $object
     */
    //public function preUpdate($object)
    //{
    //    /** @var EducationalUnit $unit */
    //    foreach ($object->getUnits() as $unit) {
    //        $unit->setModule($object);
    //    }
    //}
//
    ///**
    // * @param EducationalModule $object
    // */
    //public function prePersist($object)
    //{
    //    /** @var EducationalUnit $unit */
    //    foreach ($object->getUnits() as $unit) {
    //        $unit->setModule($object);
    //    }
    //}

}

namespace AppBundle\Admin;

use AppBundle\Entity\EducationalFile;
use AppBundle\Entity\EducationalUnit;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;

/**
 * Class EducationalFileAdmin
 */
class EducationalFileAdmin extends AbstractAdmin
{
    protected $parentAssociationMapping = 'unit';

    protected function configureFormFields(FormMapper $formMapper)
    {
        //$formMapper->add('unit', 'sonata_type_model_hidden');
        $formMapper->add('educational_file', 'file');

    }

    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper->add('id');
    }

    protected function configureListFields(ListMapper $listMapper)
    {
        $listMapper->add('id');
    }

    /**
     * @param EducationalUnit $object
     */
    //public function prePersist($object)
    //{
    //    /** @var EducationalFile $file */
    //    foreach ($object->getFiles() as $file) {
    //        $file->setUnit($object);
    //    }
    //}
//
    ///**
    // * @param EducationalUnit $object
    // */
    //public function preUpdate($object)
    //{
    //    /** @var EducationalFile $file */
    //    foreach ($object->getFiles() as $file) {
    //        $file->setUnit($object);
    //    }
    //}
}

And my related services:

    app.admin.educational_unit:
        class: AppBundle\Admin\EducationalUnitAdmin
        arguments: [~, AppBundle\Entity\EducationalUnit, ~]
        tags:
            - { name: sonata.admin, manager_type: orm, label: Educational unit }
        calls:
            - method: addChild
              arguments: ['@app.admin.educational_file']
        public: true

    app.admin.educational_file:
        class: AppBundle\Admin\EducationalFileAdmin
        arguments: [~, AppBundle\Entity\EducationalFile, ~]
        tags:
            - { name: sonata.admin, manager_type: orm, label: Educational file }
        public: true

    app.admin.educational_module:
        class: AppBundle\Admin\EducationalModuleAdmin
        arguments: [~, AppBundle\Entity\EducationalModule, ~]
        tags:
            - { name: sonata.admin, manager_type: orm, label: Educational module }
        calls:
            - method: addChild
              arguments: ['@app.admin.educational_unit']
        public: true

And my config:

sonata_block:
    default_contexts: [cms]
    blocks:
        # enable the SonataAdminBundle block
        sonata.admin.block.admin_list:
            contexts: [admin]

vich_uploader:
    db_driver: orm
    mappings:
        educational_file:
            uri_prefix:         /educational_files
            upload_destination: '%kernel.root_dir%/../web/educational_files'

Subject

I'm trying to create EducationalModule entity, and its children on the way. Unfortunately, with the above code, when adding an EducationalFile to the form, on the AJAX request I'm getting:

Could not get element id from s599b3074a0b2f_units_0_files Failing part: files

Stacktrace:

Exception:
Could not get element id from s599b3074a0b2f_units_0_files Failing part: files

  at vendor/sonata-project/admin-bundle/Admin/AdminHelper.php:283
  at Sonata\AdminBundle\Admin\AdminHelper->getElementAccessPath('s599b3074a0b2f_units_0_files', object(EducationalModule))
     (vendor/sonata-project/admin-bundle/Admin/AdminHelper.php:127)
  at Sonata\AdminBundle\Admin\AdminHelper->appendFormFieldElement(object(EducationalModuleAdmin), object(EducationalModule), 's599b3074a0b2f_units_0_files')
     (vendor/sonata-project/admin-bundle/Controller/HelperController.php:107)
  at Sonata\AdminBundle\Controller\HelperController->appendFormFieldElementAction(object(Request))
  at call_user_func_array(array(object(HelperController), 'appendFormFieldElementAction'), array(object(Request)))
     (vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php:153)
  at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
     (vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php:68)
  at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
     (vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php:171)
  at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
     (web/app_dev.php:28)

The exception goes away when I will uncomment //$formMapper->add('module', 'sonata_type_model_hidden'); and //$formMapper->add('unit', 'sonata_type_model_hidden');, but then whatever I try to do, references to parents in children entities are always null, using prePersist and preUpdate doesn't work.

I'm too new to Sonata to recognize it as a bug as I might be missing something, could anyone help me to sort this out? What am I missing here?

Environment

Sonata packages

sonata-project/admin-bundle              3.21.0 3.22.0 The missing Symfony Admin Generator
sonata-project/block-bundle              3.3.2  3.3.2  Symfony SonataBlockBundle
sonata-project/cache                     1.0.7  1.0.7  Cache library
sonata-project/core-bundle               3.4.0  3.4.0  Symfony SonataCoreBundle
sonata-project/datagrid-bundle           2.2.1  2.2.1  Symfony SonataDatagridBundle
sonata-project/doctrine-orm-admin-bundle 3.1.6  3.1.6  Symfony Sonata / Integrate Doctrine ORM into the SonataAdminBundle
sonata-project/exporter                  1.7.1  1.7.1  Lightweight Exporter library

Symfony packages

symfony/monolog-bundle     v3.1.0 v3.1.0 Symfony MonologBundle
symfony/phpunit-bridge     v3.3.6 v3.3.6 Symfony PHPUnit Bridge
symfony/polyfill-apcu      v1.4.0 v1.5.0 Symfony polyfill backporting apcu_* functions to lower PHP versions
symfony/polyfill-intl-icu  v1.4.0 v1.5.0 Symfony polyfill for intl's ICU-related data and classes
symfony/polyfill-mbstring  v1.4.0 v1.5.0 Symfony polyfill for the Mbstring extension
symfony/polyfill-php56     v1.4.0 v1.5.0 Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions
symfony/polyfill-php70     v1.4.0 v1.5.0 Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions
symfony/polyfill-util      v1.4.0 v1.5.0 Symfony utilities for portability of PHP codes
symfony/security-acl       v3.0.0 v3.0.0 Symfony Security Component - ACL (Access Control List)
symfony/swiftmailer-bundle v2.6.3 v3.0.3 Symfony SwiftmailerBundle
symfony/symfony            v3.3.6 v3.3.6 The Symfony PHP framework

PHP version

HP 7.1.2 (cli) (built: Mar  3 2017 22:56:29) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
    with Zend OPcache v7.1.2, Copyright (c) 1999-2017, by Zend Technologies
    with Xdebug v2.5.1, Copyright (c) 2002-2017, by Derick Rethans

bug unconfirmed

Most helpful comment

Ok I got it, it appears that this is actually required:

$formMapper->add('module', 'sonata_type_model_hidden');

Without this line the above exception happens. I wish this exception would be more telling.

All 3 comments

Oh, I think I got it, I changed $educational_file property of my EducationalFile entity into $educationalFile, then I changed $formMapper->add('educational_file', 'file'); into $formMapper->add('educationalFile', 'file'); in my EducationalFileAdmin and I think it helped, I also added to my OneToMany setters setting of the parent, so for example:

   public function addFiles(EducationalFile $file)
    {
        $this->files->add($file);
    }

Into:

    public function addFiles(EducationalFile $file)
    {
        $file->setUnit($this);
        $this->files->add($file);
    }

I hope it will help someone.

Unfortunately it seems the problem still persists (Could not get element id from...). I have no idea why it disappeared before but it came back now. This is really strange.

Ok I got it, it appears that this is actually required:

$formMapper->add('module', 'sonata_type_model_hidden');

Without this line the above exception happens. I wish this exception would be more telling.

Was this page helpful?
0 / 5 - 0 ratings