Browse Source

add doc on sonata_type_model sortable

Quentin Somazzi 9 năm trước cách đây
mục cha
commit
3c39023734

+ 470 - 0
Resources/doc/cookbook/recipe_sortable_sonata_type_model.rst

@@ -0,0 +1,470 @@
+Sortable Sonata Type Model in Admin
+===================================
+
+This is a full working example on how to implement a sortable feature in your Sonata admin form between two entities.
+
+Background
+----------
+
+The sortable function is already available inside Sonata for the ``ChoiceType``. But the ``ModelType`` (or sonata_type_model) extends from choice, so this function is already available in our form type.
+We just need some configuration to make it work.
+
+The goal here is to fully configure a working example to handle the following need :
+User got some expectations, but some are more relevant than the others.
+
+Pre-requisites
+--------------
+
+Configuration
+^^^^^^^^^^^^^
+- you already have SonataAdmin and DoctrineORM up and running.
+- you already have a ``UserBundle``.
+- you already have ``User`` and ``Expectation`` Entities classes.
+- you already have an ``UserAdmin`` and ``ExpectationAdmin`` set up.
+
+
+The recipe
+----------
+
+Part 1 : Update the data model configuration
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The first thing to do is to update the Doctrine ORM configuration and create the join entity between ``User`` and ``Expectation``.
+We are going to call this join entity ``UserHasExpectations``.
+
+.. note::
+
+   We can't use a ``Many-To-Many`` relation here because the joined entity will required an extra field to handle ordering.
+
+So we start by updating ``UserBundle/Resources/config/doctrine/User.orm.xml`` and adding a ``One-To-Many`` relation.
+
+.. code-block:: xml
+
+	<one-to-many field="userHasExpectations" target-entity="UserBundle\Entity\UserHasExpectations" mapped-by="user" orphan-removal="true">
+        <cascade>
+            <cascade-persist />
+        </cascade>
+        <order-by>
+            <order-by-field name="position" direction="ASC" />
+        </order-by>
+    </one-to-many>
+
+Then update ``UserBundle/Resources/config/doctrine/Expectation.orm.xml`` and also adding a ``One-To-Many`` relation.
+
+.. code-block:: xml
+
+    <one-to-many field="userHasExpectations" target-entity="UserBundle\Entity\UserHasExpectations" mapped-by="expectation" orphan-removal="false">
+        <cascade>
+            <cascade-persist />
+        </cascade>
+    </one-to-many>
+
+We now need to create the join entity configuration, create the following file in ``UserBundle/Resources/config/doctrine/UserHasExpectations.orm.xml``.
+
+.. code-block:: xml
+
+    <?xml version="1.0" encoding="utf-8"?>
+    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
+                      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                      xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
+
+        <entity name="UserBundle\Entity\UserHasExpectations" table="user__expectations">
+            <id name="id" type="integer">
+                <generator strategy="AUTO" />
+            </id>
+            <field name="position" column="position" type="integer">
+                <options>
+                    <option name="default">0</option>
+                </options>
+            </field>
+
+            <many-to-one field="user" target-entity="UserBundle\Entity\User" inversed-by="userHasExpectations" orphan-removal="false">
+                <join-column name="user_id" referenced-column-name="id" on-delete="CASCADE"/>
+            </many-to-one>
+
+            <many-to-one field="expectation" target-entity="UserBundle\Entity\Expectation" inversed-by="userHasExpectations" orphan-removal="false">
+                <join-column name="expectation_id" referenced-column-name="id" on-delete="CASCADE"/>
+            </many-to-one>
+        </entity>
+    </doctrine-mapping>
+
+Part 2 : Update the data model entities
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Update the ``UserBundle\Entity\User.php`` entity with the following :
+
+.. code-block:: php
+
+    // ...
+
+    /**
+     * @var UserHasExpectations[]
+     */
+    protected $userHasExpectations;
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct()
+    {
+        $this->userHasExpectations = new ArrayCollection;
+    }
+
+    /**
+     * @param ArrayCollection $userHasExpectations
+     */
+    public function setUserHasExpectations(ArrayCollection $userHasExpectations)
+    {
+        $this->userHasExpectations = new ArrayCollection;
+
+        foreach ($userHasExpectations as $one) {
+            $this->addUserHasExpectations($one);
+        }
+    }
+
+    /**
+     * @return ArrayCollection
+     */
+    public function getUserHasExpectations()
+    {
+        return $this->userHasExpectations;
+    }
+
+    /**
+     * @param UserHasExpectations $userHasExpectations
+     */
+    public function addUserHasExpectations(UserHasExpectations $userHasExpectations)
+    {
+        $userHasExpectations->setUser($this);
+
+        $this->userHasExpectations[] = $userHasExpectations;
+    }
+
+    /**
+     * @param UserHasExpectations $userHasExpectations
+     *
+     * @return $this
+     */
+    public function removeUserHasExpectations(UserHasExpectations $userHasExpectations)
+    {
+        $this->userHasExpectations->removeElement($userHasExpectations);
+
+        return $this;
+    }
+
+    // ...
+
+Update the ``UserBundle\Entity\Expectation.php`` entity with the following :
+
+.. code-block:: php
+
+    // ...
+
+    /**
+     * @var UserHasExpectations[]
+     */
+    protected $userHasExpectations;
+
+    /**
+     * @param UserHasExpectations[] $userHasExpectations
+     */
+    public function setUserHasExpectations($userHasExpectations)
+    {
+        $this->userHasExpectations = $userHasExpectations;
+    }
+
+    /**
+     * @return UserHasExpectations[]
+     */
+    public function getUserHasExpectations()
+    {
+        return $this->userHasExpectations;
+    }
+
+    /**
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->getLabel();
+    }
+
+    // ...
+
+Create the ``UserBundle\Entity\UserHasExpectations.php`` entity with the following :
+
+.. code-block:: php
+
+    <?php
+    namespace UserBundle\Entity;
+
+    class UserHasExpectations
+    {
+        /**
+         * @var integer $id
+         */
+        protected $id;
+
+        /**
+         * @var User $user
+         */
+        protected $user;
+
+        /**
+         * @var Expectation $expectation
+         */
+        protected $expectation;
+
+        /**
+         * @var integer $position
+         */
+        protected $position;
+
+        /**
+         * Get id
+         *
+         * @return integer $id
+         */
+        public function getId()
+        {
+            return $this->id;
+        }
+
+        /**
+         * @return User
+         */
+        public function getUser()
+        {
+            return $this->user;
+        }
+
+        /**
+         * @param User $user
+         *
+         * @return $this
+         */
+        public function setUser(User $user)
+        {
+            $this->user = $user;
+
+            return $this;
+        }
+
+        /**
+         * @return Expectation
+         */
+        public function getExpectation()
+        {
+            return $this->expectation;
+        }
+
+        /**
+         * @param Expectation $expectation
+         *
+         * @return $this
+         */
+        public function setExpectation(Expectation $expectation)
+        {
+            $this->expectation = $expectation;
+
+            return $this;
+        }
+
+        /**
+         * @return int
+         */
+        public function getPosition()
+        {
+            return $this->position;
+        }
+
+        /**
+         * @param int $position
+         *
+         * @return $this
+         */
+        public function setPosition($position)
+        {
+            $this->position = $position;
+
+            return $this;
+        }
+
+        /**
+         * @return string
+         */
+        public function __toString()
+        {
+            return (string) $this->getExpectation();
+        }
+    }
+
+Part 3 : Update admin classes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This is a very important part, the admin class **should** be created for the join entity. If you don't do that, the field will never display properly.
+So we are going to start by creating this ``UserBundle\Admin\UserHasExpectationsAdmin.php`` ...
+
+.. code-block:: php
+
+    <?php
+    namespace UserBundle\Admin;
+
+    use Sonata\AdminBundle\Admin\Admin;
+    use Sonata\AdminBundle\Datagrid\ListMapper;
+    use Sonata\AdminBundle\Form\FormMapper;
+
+    class UserHasExpectationsAdmin extends Admin
+    {
+        /**
+         * @param \Sonata\AdminBundle\Form\FormMapper $formMapper
+         */
+        protected function configureFormFields(FormMapper $formMapper)
+        {
+            $formMapper
+                ->add('expectation', 'sonata_type_model', array('required' => false))
+                ->add('position', 'hidden')
+            ;
+        }
+
+        /**
+         * @param \Sonata\AdminBundle\Datagrid\ListMapper $listMapper
+         */
+        protected function configureListFields(ListMapper $listMapper)
+        {
+            $listMapper
+                ->add('expectation')
+                ->add('user')
+                ->add('position')
+            ;
+        }
+    }
+
+... and define the service in ``UserBundle\Resources\config\admin.xml``.
+
+.. code-block:: xml
+
+    <service id="user.admin.user_has_expectations" class="UserBundle\Admin\UserHasExpectationsAdmin">
+        <tag name="sonata.admin" manager_type="orm" group="UserHasExpectations" label="UserHasExpectations" />
+        <argument />
+        <argument>UserBundle\Entity\UserHasExpectations</argument>
+        <argument />
+    </service>
+
+Now update the ``UserBundle\Admin\UserAdmin.php`` by adding the ``sonata_type_model`` field.
+
+.. code-block:: php
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function configureFormFields(FormMapper $formMapper)
+    {
+        // ...
+
+        $formMapper
+            ->add('userHasExpectations', 'sonata_type_model', array(
+                'label'        => 'User\'s expectations',
+                'query'        => $this->modelManager->createQuery('UserBundle\Entity\Expectation'),
+                'required'     => false,
+                'multiple'     => true,
+                'by_reference' => false,
+                'sortable'     => true,
+            ))
+        ;
+
+        $formMapper->get('userHasExpectations')->addModelTransformer(new ExpectationDataTransformer($this->getSubject(), $this->modelManager));
+    }
+
+There is two important things that we need to show here :
+* We use the field ``userHasExpectations`` of the user, but we need a list of ``Expectation`` to be displayed, that's explain the use of ``query``.
+* We want to persist ``UserHasExpectations``Entities, but we manage ``Expectation``, so we need to use a custom `ModelTransformer <http://symfony.com/doc/current/cookbook/form/data_transformers.html>`_ to deal with it.
+
+Part 4 : Data Transformer
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The last (but not least) step is create the ``UserBundle\Form\DataTransformer\ExpectationDataTransformer.php`` to handle the conversion of ``Expectation`` to ``UserHasExpectations``.
+
+.. code-block:: php
+
+    <?php
+    namespace UserBundle\Form\DataTransformer;
+
+    class ExpectationDataTransformer implements Symfony\Component\Form\DataTransformerInterface
+    {
+        /**
+         * @var User $user
+         */
+        private $user;
+
+        /**
+         * @var ModelManager $modelManager
+         */
+        private $modelManager;
+
+        /**
+         * @param User         $user
+         * @param ModelManager $modelManager
+         */
+        public function __construct(User $user, ModelManager $modelManager)
+        {
+            $this->user         = $user;
+            $this->modelManager = $modelManager;
+        }
+
+        /**
+         * {@inheritdoc}
+         */
+        public function transform($data)
+        {
+            if (!is_null($data)) {
+                $results = [];
+
+                /** @var UserHasExpectations $userHasExpectations */
+                foreach ($data as $userHasExpectations) {
+                    $results[] = $userHasExpectations->getExpectation();
+                }
+
+                return $results;
+            }
+
+            return $data;
+        }
+
+        /**
+         * {@inheritdoc}
+         */
+        public function reverseTransform($expectations)
+        {
+            $results  = new ArrayCollection;
+            $position = 0;
+
+            /** @var Expectation $expectation */
+            foreach ($expectations as $expectation) {
+                $userHasExpectations = $this->create();
+                $userHasExpectations->setExpectation($expectation);
+                $userHasExpectations->setPosition($position++);
+
+                $results->add($userHasExpectations);
+            }
+
+            // Remove Old values
+            $qb   = $this->modelManager->getEntityManager()->createQueryBuilder();
+            $expr = $this->modelManager->getEntityManager()->getExpressionBuilder();
+
+            $userHasExpectationsToRemove = $qb->select('entity')
+                                               ->from($this->getClass(), 'entity')
+                                               ->where($expr->eq('entity.user', $this->user->getId()))
+                                               ->getQuery()
+                                               ->getResult();
+
+            foreach ($userHasExpectationsToRemove as $userHasExpectations) {
+                $this->modelManager->delete($userHasExpectations, false);
+            }
+            $this->modelManager->getEntityManager()->flush();
+
+            return $results;
+        }
+    }
+
+Hope this will work for you :)

+ 1 - 0
Resources/doc/index.rst

@@ -75,3 +75,4 @@ Cookbook
    cookbook/recipe_virtual_field
    cookbook/recipe_bootlint
    cookbook/recipe_lock_protection
+   cookbook/recipe_sortable_sonata_type_model

+ 4 - 0
Resources/doc/reference/form_types.rst

@@ -94,6 +94,10 @@ btn_add, btn_list, btn_delete and btn_catalogue:
 
     An admin class for the linked model class needs to be defined to render this form type.
 
+.. note::
+
+    If you need to use a sortable ``sonata_type_model`` check the :doc:`recipe_sortable_sonata_type_model` page.
+
 sonata_type_model_hidden
 ^^^^^^^^^^^^^^^^^^^^^^^^
 Setting a field type of ``sonata_type_model_hidden`` will use an instance of