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.
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
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.
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:
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
.
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 %}