Important
You are browsing upcoming documentation for version 7.0 of OroCommerce, scheduled for release in 2026. Read the documentation for the latest LTS version 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:
<?php
namespace Acme\Bundle\CMSBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Oro\Bundle\DraftBundle\Entity\DraftableInterface;
use Oro\Bundle\DraftBundle\Entity\DraftableTrait;
use Oro\Bundle\EntityBundle\EntityProperty\DatesAwareInterface;
use Oro\Bundle\EntityBundle\EntityProperty\DatesAwareTrait;
use Oro\Bundle\EntityConfigBundle\Metadata\Attribute\Config;
use Oro\Bundle\EntityConfigBundle\Metadata\Attribute\ConfigField;
use Oro\Bundle\UserBundle\Entity\Ownership\UserAwareTrait;
/**
 * Represents Acme Block
 */
#[ORM\Entity]
#[ORM\Table(name: 'acme_cms_block')]
#[Config(
    routeName: 'acme_cms_block_index',
    routeView: 'acme_cms_block_view',
    routeUpdate: 'acme_cms_block_update',
    defaultValues: [
        'entity' => ['icon' => 'fa-book'],
        'ownership' => [
            'owner_type' => 'USER',
            'owner_field_name' => 'owner',
            'owner_column_name' => 'user_owner_id',
            'organization_field_name' => 'organization',
            'organization_column_name' => 'organization_id'
        ],
        'security' => ['type' => 'ACL', 'group_name' => '']
    ]
)]
class Block implements DraftableInterface, DatesAwareInterface
{
    use DraftableTrait;
    use UserAwareTrait;
    use DatesAwareTrait;
    /**
     * @var integer
     */
    #[ORM\Column(name: 'id', type: 'integer')]
    #[ORM\Id]
    #[ORM\GeneratedValue(strategy: 'AUTO')]
    protected $id;
    /**
     * @var string
     */
    #[ORM\Column(type: 'string', nullable: false, length: 255, unique: true)]
    #[ConfigField(defaultValues: ['draft' => ['draftable' => true]])]
    protected $title;
    /**
     * @var string
     */
    #[ORM\Column(type: 'text', nullable: true)]
    #[ConfigField(defaultValues: ['draft' => ['draftable' => true]])]
    protected $content;
    /**
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }
    /**
     * @return string
     */
    public function getTitle(): ?string
    {
        return $this->title;
    }
    public function setTitle(string $title): Block
    {
        $this->title = $title;
        return $this;
    }
    /**
     * @return string
     */
    public function getContent(): ?string
    {
        return $this->content;
    }
    /**
     * @param string|null $content
     *
     * @return $this
     */
    public function setContent(?string $content): Block
    {
        $this->content = $content;
        return $this;
    }
}
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.
    /**
     * @var string
     */
    #[ORM\Column(type: 'string', nullable: false, length: 255, unique: true)]
    #[ConfigField(defaultValues: ['draft' => ['draftable' => true]])]
    protected $title;
    /**
     * @var string
     */
    #[ORM\Column(type: 'text', nullable: true)]
    #[ConfigField(defaultValues: ['draft' => ['draftable' => true]])]
    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 migration 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:
<?php
namespace Acme\Bundle\CMSBundle\Migrations\Schema;
use Doctrine\DBAL\Schema\Schema;
use Oro\Bundle\MigrationBundle\Migration\Installation;
use Oro\Bundle\MigrationBundle\Migration\QueryBag;
/**
 * Creates all tables required for Acme CMSBundle.
 */
class AcmeCMSBundleInstaller implements Installation
{
    #[\Override]
    public function getMigrationVersion()
    {
        return 'v1_0';
    }
    #[\Override]
    public function up(Schema $schema, QueryBag $queries)
    {
        /** Tables updates **/
        $this->createAcmeCmsBlockColumns($schema);
        /** Foreign keys generation **/
        $this->addAcmeCmsBlockForeignKeys($schema);
    }
    private function createAcmeCmsBlockColumns(Schema $schema): void
    {
        $table = $schema->createTable('acme_cms_block');
        $table->addColumn('id', 'integer', ['autoincrement' => true]);
        $table->addColumn('title', 'string', ['length' => 255, 'notnull' => true]);
        $table->addColumn('content', 'text', ['notnull' => false]);
        $table->addColumn('created_at', 'datetime', ['comment' => '(DC2Type:datetime)']);
        $table->addColumn('updated_at', 'datetime', ['comment' => '(DC2Type:datetime)']);
        $table->addColumn('user_owner_id', 'integer', ['notnull' => false]);
        $table->addColumn('organization_id', 'integer', ['notnull' => false]);
        $table->addColumn('draft_project_id', 'integer', ['notnull' => false]);
        $table->addColumn('draft_source_id', 'integer', ['notnull' => false]);
        $table->addColumn('draft_uuid', 'guid', ['notnull' => false]);
        $table->addColumn('draft_owner_id', 'integer', ['notnull' => false]);
        $table->setPrimaryKey(['id']);
        $table->addIndex(['user_owner_id'], 'IDX_11767F6E9EB185F9');
        $table->addIndex(['organization_id'], 'IDX_11767F6E32C8A3DE');
        $table->addIndex(['draft_project_id'], 'IDX_11767F6E29386E37');
        $table->addIndex(['draft_source_id'], 'IDX_11767F6E754B6AC7');
        $table->addIndex(['draft_owner_id'], 'IDX_11767F6EDCA3D9F3');
        $table->addUniqueIndex(['title'], 'UNIQ_11767F6E2B36786B');
    }
    private function addAcmeCmsBlockForeignKeys(Schema $schema): void
    {
        $table = $schema->getTable('acme_cms_block');
        $table->addForeignKeyConstraint(
            $schema->getTable('acme_cms_block'),
            ['draft_source_id'],
            ['id'],
            ['onDelete' => 'CASCADE']
        );
        $table->addForeignKeyConstraint(
            $schema->getTable('oro_draft_project'),
            ['draft_project_id'],
            ['id'],
            ['onDelete' => 'CASCADE']
        );
        $table->addForeignKeyConstraint(
            $schema->getTable('oro_user'),
            ['draft_owner_id'],
            ['id'],
            ['onDelete' => 'CASCADE']
        );
        $table->addForeignKeyConstraint(
            $schema->getTable('oro_user'),
            ['user_owner_id'],
            ['id'],
            ['onDelete' => 'SET NULL', 'onUpdate' => null]
        );
        $table->addForeignKeyConstraint(
            $schema->getTable('oro_organization'),
            ['organization_id'],
            ['id'],
            ['onDelete' => 'SET NULL', 'onUpdate' => null]
        );
    }
}
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:
<?php
namespace Acme\Bundle\CMSBundle\Controller;
use Acme\Bundle\CMSBundle\Entity\Block;
use Acme\Bundle\CMSBundle\Form\Type\BlockType;
use Oro\Bundle\FormBundle\Model\UpdateHandlerFacade;
use Oro\Bundle\SecurityBundle\Attribute\Acl;
use Oro\Bundle\SecurityBundle\Attribute\AclAncestor;
use Symfony\Bridge\Twig\Attribute\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
 * Acme CMS Block controller
 */
class BlockController extends AbstractController
{
    #[Route(path: '/', name: 'acme_cms_block_index')]
    #[Template('@AcmeCMS/Block/index.html.twig')]
    #[AclAncestor('acme_cms_block_view')]
    public function indexAction(): array
    {
        return ['entity_class' => Block::class];
    }
    #[Route(path: '/view/{id}', name: 'acme_cms_block_view', requirements: ['id' => '\d+'])]
    #[Template('@AcmeCMS/Block/view.html.twig')]
    #[Acl(id: 'acme_cms_block_view', type: 'entity', class: 'Acme\Bundle\CMSBundle\Entity\Block', permission: 'VIEW')]
    public function viewAction(Block $block): array
    {
        return ['entity' => $block];
    }
    #[Route(path: '/create', name: 'acme_cms_block_create')]
    #[Template('@AcmeCMS/Block/update.html.twig')]
    #[Acl(
        id: 'acme_cms_block_create',
        type: 'entity',
        class: 'Acme\Bundle\CMSBundle\Entity\Block',
        permission: 'CREATE'
    )]
    public function createAction(): array|RedirectResponse
    {
        $block = new Block();
        return $this->update($block);
    }
    #[Route(path: '/update/{id}', name: 'acme_cms_block_update', requirements: ['id' => '\d+'])]
    #[Template('@AcmeCMS/Block/update.html.twig')]
    #[Acl(id: 'acme_cms_block_update', type: 'entity', class: 'Acme\Bundle\CMSBundle\Entity\Block', permission: 'EDIT')]
    public function updateAction(Block $block): array|RedirectResponse
    {
        return $this->update($block);
    }
    protected function update(Block $block): array|RedirectResponse
    {
        return $this->container->get(UpdateHandlerFacade::class)->handleUpdate(
            $block,
            $this->createForm(BlockType::class, $block),
            $this->container->get(TranslatorInterface::class)->trans('acme.cms.controller.saved.message')
        );
    }
    #[\Override]
    public static function getSubscribedServices(): array
    {
        return array_merge(
            parent::getSubscribedServices(),
            [
                UpdateHandlerFacade::class,
                TranslatorInterface::class,
            ]
        );
    }
}
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.
- Create a grid for entities 
    acme-cms-block-grid:
        acl_resource: acme_cms_block_view
        options:
            entity_pagination: true
            entityHint: acme.cms.block.entity_label
        source:
            type: orm
            query:
                select:
                    - acme_cms_block.id
                    - acme_cms_block.title
                from:
                    - { table: Acme\Bundle\CMSBundle\Entity\Block, alias: acme_cms_block }
        columns:
            id:
                label: acme.cms.block.id.label
            title:
                label: acme.cms.block.title.label
        properties:
            id: ~
            view_link:
                type:   url
                route:  acme_cms_block_view
                params: [ id ]
        actions:
            view:
                type:          navigate
                label:         oro.grid.action.view
                link:          view_link
                icon:          eye
                acl_resource:  acme_cms_block_view
                rowAction:     true
- 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.
- 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.
<?php
namespace Acme\Bundle\CMSBundle\Form\Type;
use Acme\Bundle\CMSBundle\Entity\Block;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
 * Block form type
 */
class BlockType extends AbstractType
{
    public const NAME = 'acme_cms_block';
    #[\Override]
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add(
                'title',
                TextType::class,
                [
                    'label' => 'Title',
                    'required' => true,
                ]
            )
            ->add(
                'content',
                TextareaType::class,
                [
                    'label' => 'Content',
                    'required' => true,
                ]
            );
    }
    #[\Override]
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Block::class,
            'csrf_token_id' => 'acme_cms_block',
            'ownership_disabled' => true,
        ]);
    }
    /**
     * @return string
     */
    public function getName()
    {
        return $this->getBlockPrefix();
    }
    #[\Override]
    public function getBlockPrefix()
    {
        return self::NAME;
    }
}
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:
- Create a filter 
- Create a matcher 
- Create an extension 
Create a Filter
Filter is a class that can modify the property value and be used in conjunction with Matcher.
<?php
namespace Acme\Bundle\CMSBundle\Duplicator\Filter;
use Acme\Bundle\CMSBundle\Entity\Block;
use Oro\Component\Duplicator\Filter\Filter;
/**
 * Modifies the value of the title field
 */
class UniqueTitleFilter implements Filter
{
    /**
     * @param Block $object
     * @param string $property
     * @param callable $objectCopier
     */
    #[\Override]
    public function apply($object, $property, $objectCopier): void
    {
        $resolvedField = sprintf('%s_%s', $object->getTitle(), uniqid());
        $object->setTitle($resolvedField);
    }
}
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.
<?php
namespace Acme\Bundle\CMSBundle\Duplicator\Matcher;
use Acme\Bundle\CMSBundle\Entity\Block;
use DeepCopy\Matcher\Matcher;
/**
 * Determines that a filter can be applied to the title property only
 */
class BlockTitleMatcher implements Matcher
{
    /**
     * @param Block $object
     * @param string $property
     *
     * @return bool
     */
    #[\Override]
    public function matches($object, $property): bool
    {
        return 'title' === $property;
    }
}
Create an Extension
Extension is a class that combines the logic of Filter and Matcher.
<?php
namespace Acme\Bundle\CMSBundle\Duplicator\Extension;
use Acme\Bundle\CMSBundle\Duplicator\Filter\UniqueTitleFilter;
use Acme\Bundle\CMSBundle\Duplicator\Matcher\BlockTitleMatcher;
use Acme\Bundle\CMSBundle\Entity\Block;
use DeepCopy\Filter\Filter;
use DeepCopy\Matcher\Matcher;
use Oro\Bundle\DraftBundle\Duplicator\Extension\AbstractDuplicatorExtension;
use Oro\Bundle\DraftBundle\Entity\DraftableInterface;
use Oro\Bundle\DraftBundle\Manager\DraftManager;
/**
 * Responsible for copying behavior of Block type parameter.
 */
class BlockTitleExtension extends AbstractDuplicatorExtension
{
    #[\Override]
    public function getFilter(): Filter
    {
        return new UniqueTitleFilter();
    }
    #[\Override]
    public function getMatcher(): Matcher
    {
        return new BlockTitleMatcher();
    }
    #[\Override]
    public function isSupport(DraftableInterface $source): bool
    {
        return
            $this->getContext()->offsetGet('action') === DraftManager::ACTION_CREATE_DRAFT &&
            $source instanceof Block;
    }
}
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.
{% block breadcrumbs %}
    {% import '@OroUI/macros.html.twig' as UI %}
    {{ parent() }}
    {% if entity.draftUuid %}
        <span class="page-title-draft">
            {{ UI.badge('oro.draft.label'|trans, 'tentatively') }}
        </span>
    {% endif %}
{% endblock breadcrumbs %}