Important

You are browsing the documentation for version 4.1 of OroCommerce, OroCRM and OroPlatform, which is no longer maintained. Read version 5.1 (the latest LTS version) of the Oro documentation to get up-to-date information.

See our Release Process documentation for more information on the currently supported and upcoming releases.

How to Use Drafts

This topic describes how to use draft.

For example, let’s create a simple bundle using instructions provided in the How to create a new bundle topic.

Following the steps described below, the released bundle will implement all the functionality provided by DraftBundle.

Add and Configure an Entity

Create an entity with 2 fields: title and content and implement the entity from DraftableInterface.

Name

Type

title

string

content

text

Note

For simplicity, use the predefined DraftableTrait trait:

  1<?php
  2
  3namespace ACME\Bundle\CMSBundle\Entity;
  4
  5use Doctrine\ORM\Mapping as ORM;
  6use Oro\Bundle\DraftBundle\Entity\DraftableInterface;
  7use Oro\Bundle\DraftBundle\Entity\DraftableTrait;
  8use Oro\Bundle\EntityBundle\EntityProperty\DatesAwareInterface;
  9use Oro\Bundle\EntityBundle\EntityProperty\DatesAwareTrait;
 10use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Config;
 11use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\ConfigField;
 12use Oro\Bundle\UserBundle\Entity\Ownership\UserAwareTrait;
 13
 14/**
 15 * Represents ACME Block
 16 *
 17 * @ORM\Table(name="acme_cms_block")
 18 * @ORM\Entity()
 19 * @Config(
 20 *      routeName="acme_cms_block_index",
 21 *      routeView="acme_cms_block_view",
 22 *      routeUpdate="acme_cms_block_update",
 23 *      defaultValues={
 24 *          "entity"={
 25 *              "icon"="fa-book"
 26 *          },
 27 *          "ownership"={
 28 *              "owner_type"="USER",
 29 *              "owner_field_name"="owner",
 30 *              "owner_column_name"="user_owner_id",
 31 *              "organization_field_name"="organization",
 32 *              "organization_column_name"="organization_id"
 33 *          },
 34 *          "security"={
 35 *              "type"="ACL",
 36 *              "group_name"=""
 37 *          }
 38 *      }
 39 * )
 40 */
 41class Block implements DraftableInterface, DatesAwareInterface
 42{
 43    use DraftableTrait;
 44    use UserAwareTrait;
 45    use DatesAwareTrait;
 46
 47    /**
 48     * @var integer
 49     *
 50     * @ORM\Column(name="id", type="integer")
 51     * @ORM\Id
 52     * @ORM\GeneratedValue(strategy="AUTO")
 53     */
 54    protected $id;
 55
 56    /**
 57     * @var string
 58     *
 59     * @ORM\Column(type="string", nullable=false, length=255, unique=true)
 60     * @ConfigField(
 61     *      defaultValues={
 62     *          "draft"={
 63     *              "draftable"=true
 64     *          }
 65     *      }
 66     * )
 67     */
 68    protected $title;
 69
 70    /**
 71     * @var string
 72     *
 73     * @ORM\Column(type="text", nullable=true)
 74     * @ConfigField(
 75     *      defaultValues={
 76     *          "draft"={
 77     *              "draftable"=true
 78     *          }
 79     *      }
 80     * )
 81     */
 82    protected $content;
 83
 84    /**
 85     * @return integer
 86     */
 87    public function getId()
 88    {
 89        return $this->id;
 90    }
 91
 92    /**
 93     * @return string
 94     */
 95    public function getTitle(): ?string
 96    {
 97        return $this->title;
 98    }
 99
100    /**
101     * @param string $title
102     * @return Block
103     */
104    public function setTitle(string $title): Block
105    {
106        $this->title = $title;
107
108        return $this;
109    }
110
111    /**
112     * @return string
113     */
114    public function getContent(): ?string
115    {
116        return $this->content;
117    }
118
119    /**
120     * @param string|null $content
121     *
122     * @return $this
123     */
124    public function setContent(?string $content): Block
125    {
126        $this->content = $content;
127
128        return $this;
129    }
130}

Then, add the entity configuration to the title and content fields. The draftable parameter ensures that the field is involved in the draft operation.

Follow the instructions provided in the Configure Entities topic.

 1    /**
 2     * @var string
 3     *
 4     * @ORM\Column(type="string", nullable=false, length=255, unique=true)
 5     * @ConfigField(
 6     *      defaultValues={
 7     *          "draft"={
 8     *              "draftable"=true
 9     *          }
10     *      }
11     * )
12     */
13    protected $title;
14
15    /**
16     * @var string
17     *
18     * @ORM\Column(type="text", nullable=true)
19     * @ConfigField(
20     *      defaultValues={
21     *          "draft"={
22     *              "draftable"=true
23     *          }
24     *      }
25     * )
26     */
27    protected $content;

Important

To be able to restrict the entity from using ACL, follow the instructions provided in the Protect Entities Using ACLs topic.

Restrictions for drafts have specific peculiarities. For more information, see the How to use draft ACL topic.

Create an Installer/Migration

An installer ensures that upon the application installation, the database will contain the entity with the fields defined within bundle. Follow the instructions provided in the How to generate an installer topic for more details.

The fields responsible for the draft must match the interface and have the appropriate types.

Name

Type

Attributes

draft_project_id

ManyToOne

nullable

draft_source_id

ManyToOne

nullable

draft_uuid

guid

nullable

draft_owner_id

ManyToOne

nullable

After you complete it, you will have the installer with the following content:

 1<?php
 2
 3namespace ACME\Bundle\CMSBundle\Migrations\Schema;
 4
 5use Doctrine\DBAL\Schema\Schema;
 6use Oro\Bundle\MigrationBundle\Migration\Installation;
 7use Oro\Bundle\MigrationBundle\Migration\QueryBag;
 8
 9/**
10 * Creates all tables required for ACME CMSBundle.
11 */
12class ACMECMSBundleInstaller implements Installation
13{
14    /**
15     * {@inheritdoc}
16     */
17    public function getMigrationVersion()
18    {
19        return 'v1_0';
20    }
21
22    /**
23     * {@inheritdoc}
24     */
25    public function up(Schema $schema, QueryBag $queries)
26    {
27        /** Tables updates **/
28        $this->createAcmeCmsBlockColumns($schema);
29
30        /** Foreign keys generation **/
31        $this->addAcmeCmsBlockForeignKeys($schema);
32    }
33
34    /**
35     * @param Schema $schema
36     */
37    private function createAcmeCmsBlockColumns(Schema $schema): void
38    {
39        $table = $schema->createTable('acme_cms_block');
40        $table->addColumn('id', 'integer', ['autoincrement' => true]);
41        $table->addColumn('title', 'string', ['length' => 255, 'notnull' => true]);
42        $table->addColumn('content', 'text', ['notnull' => false]);
43        $table->addColumn('created_at', 'datetime', ['comment' => '(DC2Type:datetime)']);
44        $table->addColumn('updated_at', 'datetime', ['comment' => '(DC2Type:datetime)']);
45        $table->addColumn('user_owner_id', 'integer', ['notnull' => false]);
46        $table->addColumn('organization_id', 'integer', ['notnull' => false]);
47        $table->addColumn('draft_project_id', 'integer', ['notnull' => false]);
48        $table->addColumn('draft_source_id', 'integer', ['notnull' => false]);
49        $table->addColumn('draft_uuid', 'guid', ['notnull' => false]);
50        $table->addColumn('draft_owner_id', 'integer', ['notnull' => false]);
51        $table->setPrimaryKey(['id']);
52        $table->addIndex(['user_owner_id'], 'IDX_11767F6E9EB185F9');
53        $table->addIndex(['organization_id'], 'IDX_11767F6E32C8A3DE');
54        $table->addIndex(['draft_project_id'], 'IDX_11767F6E29386E37');
55        $table->addIndex(['draft_source_id'], 'IDX_11767F6E754B6AC7');
56        $table->addIndex(['draft_owner_id'], 'IDX_11767F6EDCA3D9F3');
57        $table->addUniqueIndex(['title'], 'UNIQ_11767F6E2B36786B');
58    }
59
60    /**
61     * @param Schema $schema
62     */
63    private function addAcmeCmsBlockForeignKeys(Schema $schema): void
64    {
65        $table = $schema->getTable('acme_cms_block');
66        $table->addForeignKeyConstraint(
67            $schema->getTable('acme_cms_block'),
68            ['draft_source_id'],
69            ['id'],
70            ['onDelete' => 'CASCADE']
71        );
72        $table->addForeignKeyConstraint(
73            $schema->getTable('oro_draft_project'),
74            ['draft_project_id'],
75            ['id'],
76            ['onDelete' => 'CASCADE']
77        );
78        $table->addForeignKeyConstraint(
79            $schema->getTable('oro_user'),
80            ['draft_owner_id'],
81            ['id'],
82            ['onDelete' => 'CASCADE']
83        );
84        $table->addForeignKeyConstraint(
85            $schema->getTable('oro_user'),
86            ['user_owner_id'],
87            ['id'],
88            ['onDelete' => 'SET NULL', 'onUpdate' => null]
89        );
90        $table->addForeignKeyConstraint(
91            $schema->getTable('oro_organization'),
92            ['organization_id'],
93            ['id'],
94            ['onDelete' => 'SET NULL', 'onUpdate' => null]
95        );
96    }
97}

Create a Controller

To work with draft entities, you must use the controller for the actions of non-draft entities.

For more details on how to create a controller and navigation, see the following guides:

  1<?php
  2
  3namespace ACME\Bundle\CMSBundle\Controller;
  4
  5use ACME\Bundle\CMSBundle\Entity\Block;
  6use ACME\Bundle\CMSBundle\Form\Type\BlockType;
  7use Oro\Bundle\FormBundle\Model\UpdateHandler;
  8use Oro\Bundle\SecurityBundle\Annotation\Acl;
  9use Oro\Bundle\SecurityBundle\Annotation\AclAncestor;
 10use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
 11use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 12use Symfony\Component\HttpFoundation\RedirectResponse;
 13use Symfony\Component\Routing\Annotation\Route;
 14use Symfony\Contracts\Translation\TranslatorInterface;
 15
 16/**
 17 * ACME CMS Block controller
 18 */
 19class BlockController extends AbstractController
 20{
 21    /**
 22     * @Route("/", name="acme_cms_block_index")
 23     * @Template
 24     * @AclAncestor("acme_cms_block_view")
 25     *
 26     * @return array
 27     */
 28    public function indexAction()
 29    {
 30        return ['entity_class' => Block::class];
 31    }
 32
 33    /**
 34     * @Route("/view/{id}", name="acme_cms_block_view", requirements={"id"="\d+"})
 35     * @Template
 36     * @Acl(
 37     *      id="acme_cms_block_view",
 38     *      type="entity",
 39     *      class="ACMECMSBundle:Block",
 40     *      permission="VIEW"
 41     * )
 42     * @param Block $block
 43     * @return array
 44     */
 45    public function viewAction(Block $block)
 46    {
 47        return ['entity' => $block];
 48    }
 49
 50    /**
 51     * @Route("/create", name="acme_cms_block_create")
 52     * @Template("ACMECMSBundle:Block:update.html.twig")
 53     * @Acl(
 54     *      id="acme_cms_block_create",
 55     *      type="entity",
 56     *      class="ACMECMSBundle:Block",
 57     *      permission="CREATE"
 58     * )
 59     *
 60     * @return array|RedirectResponse
 61     */
 62    public function createAction()
 63    {
 64        $block = new Block();
 65
 66        return $this->update($block);
 67    }
 68
 69    /**
 70     * @Route("/update/{id}", name="acme_cms_block_update", requirements={"id"="\d+"})
 71     * @Template
 72     * @Acl(
 73     *      id="acme_cms_block_update",
 74     *      type="entity",
 75     *      class="ACMECMSBundle:Block",
 76     *      permission="EDIT"
 77     * )
 78     * @param Block $block
 79     * @return array|RedirectResponse
 80     */
 81    public function updateAction(Block $block)
 82    {
 83        return $this->update($block);
 84    }
 85
 86    /**
 87     * @param Block $block
 88     * @return array|RedirectResponse
 89     */
 90    protected function update(Block $block)
 91    {
 92        return $this->get(UpdateHandler::class)->handleUpdate(
 93            $block,
 94            $this->createForm(BlockType::class, $block),
 95            function (Block $block) {
 96                return [
 97                    'route' => 'acme_cms_block_update',
 98                    'parameters' => ['id' => $block->getId()]
 99                ];
100            },
101            function (Block $block) {
102                return [
103                    'route' => 'acme_cms_block_view',
104                    'parameters' => ['id' => $block->getId()]
105                ];
106            },
107            $this->get(TranslatorInterface::class)->trans('acme.cms.controller.saved.message')
108        );
109    }
110
111    /**
112     * {@inheritdoc}
113     */
114    public static function getSubscribedServices(): array
115    {
116        return array_merge(
117            parent::getSubscribedServices(),
118            [
119                UpdateHandler::class,
120                TranslatorInterface::class,
121            ]
122        );
123    }
124}

Note

A draft realizes the DraftKernelListener class. Listener receives the controller’s original argument, replaces it with a draft, and injects the argument to the controller again. It allows us to use a single entry point for all entities.

Add a Draft Grid

To manage the entity, we need to create a grid to display original and draft entities. Follow the instructions provided in the Configure grid topic for more details.

  1. Create a grid for entities

 1    acme-cms-block-grid:
 2        acl_resource: acme_cms_block_view
 3        options:
 4            entity_pagination: true
 5            entityHint: acme.cms.block.entity_label
 6        source:
 7            type: orm
 8            query:
 9                select:
10                    - acme_cms_block.id
11                    - acme_cms_block.title
12                from:
13                    - { table: 'ACMECMSBundle:Block', alias: acme_cms_block }
14        columns:
15            id:
16                label: acme.cms.block.id.label
17            title:
18                label: acme.cms.block.title.label
19        properties:
20            id: ~
21            view_link:
22                type:   url
23                route:  acme_cms_block_view
24                params: [ id ]
25        actions:
26            view:
27                type:          navigate
28                label:         oro.grid.action.view
29                link:          view_link
30                icon:          eye
31                acl_resource:  acme_cms_block_view
32                rowAction:     true
  1. Create a grid for draft entities

    acme-cms-block-drafts-grid:
        extends: acme-cms-block-grid
        options:
            entity_pagination: true
            entityHint: acme.cms.block.draft.entity_plural_label
            showDrafts: true
        source:
            query:
                select:
                    - CONCAT(draftOwner.firstName, ' ', draftOwner.lastName) as ownerName
                join:
                    left:
                        - { join: acme_cms_block.draftOwner, alias: draftOwner }
                where:
                    and:
                        - acme_cms_block.draftSource = :draft_source_id
                        - acme_cms_block.draftUuid IS NOT NULL
            bind_parameters:
                - draft_source_id
        columns:
            ownerName:
                label: acme.cms.block.draft.owner.label

Important

Use the showDrafts option in the grid configuration. This parameter is responsible for the enable/disable draft filter. This configuration changes the behavior of the grid, and the grid only reflects draft entities.

  1. Add grid operations to the grid with draft entities

Operations for the draft entities must be configured separately in action.yml.

Follow the instructions provided in the Work with Operations topic.

    ACME_BLOCK_CREATE_DRAFT:
        extends: CREATE_DRAFT
        entities:
            - ACME\Bundle\CMSBundle\Entity\Block

    ACME_BLOCK_PUBLISH_DRAFT:
        extends: PUBLISH_DRAFT
        entities:
            - ACME\Bundle\CMSBundle\Entity\Block
        datagrids:
            - acme-cms-block-drafts-grid

    ACME_BLOCK_DUPLICATE_DRAFT:
        extends: DUPLICATE_DRAFT
        entities:
            - ACME\Bundle\CMSBundle\Entity\Block
        datagrids:
            - acme-cms-block-drafts-grid

    ACME_BLOCK_UPDATE_DRAFT:
        extends: UPDATE_DRAFT
        datagrids:
            - acme-cms-block-drafts-grid

    ACME_BLOCK_DELETE_DRAFT:
        extends: DELETE_DRAFT
        datagrids:
            - acme-cms-block-drafts-grid

Create a Form Type

Create a form type to handle the draft entity.

Follow the instructions provided in the The Form Type topic for more details.

 1<?php
 2
 3namespace ACME\Bundle\CMSBundle\Form\Type;
 4
 5use ACME\Bundle\CMSBundle\Entity\Block;
 6use Symfony\Component\Form\AbstractType;
 7use Symfony\Component\Form\Extension\Core\Type\TextareaType;
 8use Symfony\Component\Form\Extension\Core\Type\TextType;
 9use Symfony\Component\Form\FormBuilderInterface;
10use Symfony\Component\OptionsResolver\OptionsResolver;
11
12/**
13 * Block form type
14 */
15class BlockType extends AbstractType
16{
17    public const NAME = 'acme_cms_block';
18
19    /**
20     * @param FormBuilderInterface $builder
21     * @param array $options
22     */
23    public function buildForm(FormBuilderInterface $builder, array $options)
24    {
25        $builder
26            ->add(
27                'title',
28                TextType::class,
29                [
30                    'label' => 'Title',
31                    'required' => true,
32                ]
33            )
34            ->add(
35                'content',
36                TextareaType::class,
37                [
38                    'label' => 'Content',
39                    'required' => true,
40                ]
41            );
42    }
43
44    /**
45     * {@inheritDoc}
46     */
47    public function configureOptions(OptionsResolver $resolver)
48    {
49        $resolver->setDefaults([
50            'data_class' => Block::class,
51            'csrf_token_id' => 'acme_cms_block',
52            'ownership_disabled' => true,
53        ]);
54    }
55
56    /**
57     * @return string
58     */
59    public function getName()
60    {
61        return $this->getBlockPrefix();
62    }
63
64    /**
65     * {@inheritdoc}
66     */
67    public function getBlockPrefix()
68    {
69        return self::NAME;
70    }
71}

Use the Extension

Using the extension, you can change the property value when creating the draft entity. Follow the instructions provided in the How to use draft extension topic.

Take the following three steps to create an extension:

  1. Create a filter

  2. Create a matcher

  3. Create an extension

Create a Filter

Filter is a class that can modify the property value and be used in conjunction with Matcher.

 1<?php
 2
 3namespace ACME\Bundle\CMSBundle\Duplicator\Filter;
 4
 5use ACME\Bundle\CMSBundle\Entity\Block;
 6use Oro\Component\Duplicator\Filter\Filter;
 7
 8/**
 9 * Modifies the value of the title field
10 */
11class UniqueTitleFilter implements Filter
12{
13    /**
14     * @param Block $object
15     * @param string $property
16     * @param callable $objectCopier
17     */
18    public function apply($object, $property, $objectCopier): void
19    {
20        $resolvedField = sprintf('%s_%s', $object->getTitle(), uniqid());
21        $object->setTitle($resolvedField);
22    }
23}

Create a Matcher

Matcher is a class that points to the field that Filter can work on. Matcher checks whether the specified field meets the criteria and applies the filter to this field.

 1<?php
 2
 3namespace ACME\Bundle\CMSBundle\Duplicator\Matcher;
 4
 5use ACME\Bundle\CMSBundle\Entity\Block;
 6use DeepCopy\Matcher\Matcher;
 7
 8/**
 9 * Determines that a filter can be applied to the title property only
10 */
11class BlockTitleMatcher implements Matcher
12{
13    /**
14     * @param Block $object
15     * @param string $property
16     *
17     * @return bool
18     */
19    public function matches($object, $property): bool
20    {
21        return 'title' === $property;
22    }
23}

Create an Extension

Extension is a class that combines the logic of Filter and Matcher.

 1<?php
 2
 3namespace ACME\Bundle\CMSBundle\Duplicator\Extension;
 4
 5use ACME\Bundle\CMSBundle\Duplicator\Filter\UniqueTitleFilter;
 6use ACME\Bundle\CMSBundle\Duplicator\Matcher\BlockTitleMatcher;
 7use ACME\Bundle\CMSBundle\Entity\Block;
 8use DeepCopy\Filter\Filter;
 9use DeepCopy\Matcher\Matcher;
10use Oro\Bundle\DraftBundle\Duplicator\Extension\AbstractDuplicatorExtension;
11use Oro\Bundle\DraftBundle\Entity\DraftableInterface;
12use Oro\Bundle\DraftBundle\Manager\DraftManager;
13
14/**
15 * Responsible for copying behavior of Block type parameter.
16 */
17class BlockTitleExtension extends AbstractDuplicatorExtension
18{
19    /**
20     * @return Filter
21     */
22    public function getFilter(): Filter
23    {
24        return new UniqueTitleFilter();
25    }
26
27    /**
28     * @return Matcher
29     */
30    public function getMatcher(): Matcher
31    {
32        return new BlockTitleMatcher();
33    }
34
35    /**
36     * @param DraftableInterface $source
37     *
38     * @return bool
39     */
40    public function isSupport(DraftableInterface $source): bool
41    {
42        return
43            $this->getContext()->offsetGet('action') === DraftManager::ACTION_CREATE_DRAFT &&
44            $source instanceof Block;
45    }
46}

Create a Template

Draft entities use the default templates, so you can use them. To identify a draft entity, you can create a block that shows whether the entity is a draft entity.

Follow the instructions provided in the Template topic.

1{% block breadcrumbs %}
2    {% import 'OroUIBundle::macros.html.twig' as UI %}
3    {{ parent() }}
4    {% if entity.draftUuid %}
5        <span class="page-title-draft">
6            {{ UI.badge('oro.draft.label'|trans, 'tentatively') }}
7        </span>
8    {% endif %}
9{% endblock breadcrumbs %}