diff --git a/src/Oro/Bundle/ActivityListBundle/Entity/ActivityList.php b/src/Oro/Bundle/ActivityListBundle/Entity/ActivityList.php index 3f8861ed726..444b544e096 100644 --- a/src/Oro/Bundle/ActivityListBundle/Entity/ActivityList.php +++ b/src/Oro/Bundle/ActivityListBundle/Entity/ActivityList.php @@ -324,7 +324,7 @@ public function getSubject() } /** - * Set a subject of the related record + * Set a subject of the related record. The subject cutes to 250 symbols. * * @param string $subject * @@ -332,7 +332,7 @@ public function getSubject() */ public function setSubject($subject) { - $this->subject = substr($subject, 0, 255); + $this->subject = mb_substr($subject, 0, 250, mb_detect_encoding($subject)); return $this; } diff --git a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Entity/ActivityListTest.php b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Entity/ActivityListTest.php index 644b432f99e..ce53bc5e468 100644 --- a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Entity/ActivityListTest.php +++ b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Entity/ActivityListTest.php @@ -159,4 +159,20 @@ public function testIsNotUpdatedFlags() $this->assertFalse($activityList->isUpdatedBySet()); $this->assertFalse($activityList->isUpdatedAtSet()); } + + public function testSetSubjectOnLongString() + { + $activityList = new ActivityList(); + $activityList->setSubject( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eget elementum velit, ac tempor orci. ' + . 'Cras aliquet massa id dignissim bibendum. Interdum et malesuada fames ac ante ipsum primis in faucibus.' + .' Aenean ac libero magna. Proin eu tristiqäe est. Donec convallis pretium congue. Nullam sed.' + ); + self::assertEquals( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eget elementum velit, ac tempor orci. ' + . 'Cras aliquet massa id dignissim bibendum. Interdum et malesuada fames ac ante ipsum primis in faucibus.' + . ' Aenean ac libero magna. Proin eu tristiqä', + $activityList->getSubject() + ); + } } diff --git a/src/Oro/Bundle/CronBundle/Command/CronCommandInterface.php b/src/Oro/Bundle/CronBundle/Command/CronCommandInterface.php index d5a8b7cca6d..25e317d8ba7 100644 --- a/src/Oro/Bundle/CronBundle/Command/CronCommandInterface.php +++ b/src/Oro/Bundle/CronBundle/Command/CronCommandInterface.php @@ -8,7 +8,7 @@ interface CronCommandInterface * Define default cron schedule definition for a command. * Example: "5 * * * *" * - * @see Oro\Bundle\CronBundle\Entity\Schedule::setDefinition() + * @see \Oro\Bundle\CronBundle\Entity\Schedule::setDefinition() * @return string */ public function getDefaultDefinition(); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/doc/backend/extensions.md b/src/Oro/Bundle/DataGridBundle/Resources/doc/backend/extensions.md index b10e72c1147..fe85c3047ed 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/doc/backend/extensions.md +++ b/src/Oro/Bundle/DataGridBundle/Resources/doc/backend/extensions.md @@ -18,6 +18,7 @@ Here's list of already implemented extensions: - [Export](extensions/export.md) - responsible for export grid data - [Field ACL](extensions/field_acl.md) - allow to protect entity fields with ACL - [Board](extensions/board.md) - responsible for adding Kanban board views for datagrids +- [Filter](http://github.com/orocrm/platform/blob/master/src/Oro/Bundle/FilterBundle/Resources/doc/reference/grid_extension.md) - responsible for adding filtering and filter widgets to grid Customization ------------- diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/components/column-manager-component.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/components/column-manager-component.js index 82051aef784..d9d7cbd4ca9 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/components/column-manager-component.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/components/column-manager-component.js @@ -52,7 +52,7 @@ define(function(require) { this.columnFilterModel = new ColumnFilterModel(); - this._createViews(options); + this.createViews = _.bind(this._createViews, this, options); this._applyState(this.grid.collection, this.grid.collection.state); @@ -79,7 +79,6 @@ define(function(require) { */ delegateListeners: function() { this.listenTo(this.grid.collection, 'updateState', this._applyState); - this.listenTo(this.columnManagerCollectionView, 'reordered', this._pushState); this.listenTo(this.managedColumns, 'change:renderable', this._pushState); this.listenTo(this.managedColumns, 'sort', function() { this.columns.sort(); @@ -112,9 +111,13 @@ define(function(require) { filterModel: this.columnFilterModel, orderShift: orderShift }); + this.listenTo(this.columnManagerCollectionView, 'reordered', this._pushState); }, updateViews: function() { + if (!this.columnManagerCollectionView) { + this.createViews(); + } this.columnManagerCollectionView.updateView(); }, diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action-component-dropdown-launcher.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action-component-dropdown-launcher.js index ea0fdf0b488..b74fa0b4222 100755 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action-component-dropdown-launcher.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action-component-dropdown-launcher.js @@ -56,10 +56,12 @@ define(function(require) { /** * @inheritDoc */ - initComponent: function() { + render: function() { + ActionComponentDropdownLauncher.__super__.render.call(this); this.componentOptions._sourceElement = this.$('.dropdown-menu'); var Component = this.componentConstructor; this.component = new Component(this.componentOptions); + return this; }, /** @@ -92,8 +94,8 @@ define(function(require) { * Handles dropdown menu open and sets max-width for the element */ onOpen: function() { - if (!this.component) { - this.initComponent(); + if (_.isFunction(this.component.updateViews)) { + this.component.updateViews(); } var $dropdownMenu = this.$('>.dropdown-menu'); if ($dropdownMenu.length) { @@ -105,9 +107,6 @@ define(function(require) { var $elem = this.$('.dropdown-menu'); // focus input after Bootstrap opened dropdown menu $elem.focusFirstInput(); - if (_.isFunction(this.component.updateViews)) { - this.component.updateViews(); - } }, /** diff --git a/src/Oro/Bundle/EmailBundle/Builder/EmailBodyBuilder.php b/src/Oro/Bundle/EmailBundle/Builder/EmailBodyBuilder.php index 1f2bbf95cc0..5a0c42c34b5 100644 --- a/src/Oro/Bundle/EmailBundle/Builder/EmailBodyBuilder.php +++ b/src/Oro/Bundle/EmailBundle/Builder/EmailBodyBuilder.php @@ -7,6 +7,7 @@ use Oro\Bundle\EmailBundle\Entity\EmailAttachment; use Oro\Bundle\EmailBundle\Entity\EmailAttachmentContent; use Oro\Bundle\ConfigBundle\Config\ConfigManager; +use Oro\Bundle\EmailBundle\Tools\EmailBodyHelper; /** * A helper class allows you to easy build EmailBody entity @@ -24,6 +25,9 @@ class EmailBodyBuilder /** @var ConfigManager */ protected $configManager; + /** @var EmailBodyHelper */ + private $emailBodyHelper; + /** * @param ConfigManager $configManager */ @@ -58,7 +62,8 @@ public function setEmailBody($content, $bodyIsText) $this->emailBody = new EmailBody(); $this->emailBody ->setBodyContent($content) - ->setBodyIsText($bodyIsText); + ->setBodyIsText($bodyIsText) + ->setTextBody($this->getEmailBodyHelper()->getTrimmedClearText($content)); } /** @@ -159,4 +164,16 @@ protected function checkContentSizeValue($content, $contentSize, $contentTransfe return $contentSize; } + + /** + * @return EmailBodyHelper + */ + protected function getEmailBodyHelper() + { + if (!$this->emailBodyHelper) { + $this->emailBodyHelper = new EmailBodyHelper(); + } + + return $this->emailBodyHelper; + } } diff --git a/src/Oro/Bundle/EmailBundle/Builder/EmailEntityBuilder.php b/src/Oro/Bundle/EmailBundle/Builder/EmailEntityBuilder.php index d9e49b02531..9725a63c31a 100644 --- a/src/Oro/Bundle/EmailBundle/Builder/EmailEntityBuilder.php +++ b/src/Oro/Bundle/EmailBundle/Builder/EmailEntityBuilder.php @@ -15,6 +15,7 @@ use Oro\Bundle\EmailBundle\Exception\UnexpectedTypeException; use Oro\Bundle\EmailBundle\Model\FolderType; use Oro\Bundle\EmailBundle\Tools\EmailAddressHelper; +use Oro\Bundle\EmailBundle\Tools\EmailBodyHelper; use Oro\Bundle\OrganizationBundle\Entity\OrganizationInterface; use Oro\Bundle\UserBundle\Entity\User; @@ -35,6 +36,9 @@ class EmailEntityBuilder */ private $emailAddressHelper; + /** @var EmailBodyHelper */ + private $emailBodyHelper; + /** * Constructor * @@ -250,7 +254,8 @@ public function body($content, $isHtml, $persistent = false) $result ->setBodyContent($content) ->setBodyIsText(!$isHtml) - ->setPersistent($persistent); + ->setPersistent($persistent) + ->setTextBody($this->getEmailBodyHelper()->getTrimmedClearText($content, !$isHtml)); return $result; } @@ -476,4 +481,16 @@ public function setObject($obj) ); } } + + /** + * @return EmailBodyHelper + */ + protected function getEmailBodyHelper() + { + if (!$this->emailBodyHelper) { + $this->emailBodyHelper = new EmailBodyHelper(); + } + + return $this->emailBodyHelper; + } } diff --git a/src/Oro/Bundle/EmailBundle/Command/ConvertEmailBodyToTextBody.php b/src/Oro/Bundle/EmailBundle/Command/ConvertEmailBodyToTextBody.php new file mode 100644 index 00000000000..3c84deaf51d --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Command/ConvertEmailBodyToTextBody.php @@ -0,0 +1,77 @@ +setName(static::COMMAND_NAME) + ->setDescription('Converts emails body. Generates and stores textual email body representation.'); + + parent::configure(); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Conversion of emails body is started.'); + + /** @var Connection $connection */ + $connection = $this->getContainer()->get('doctrine')->getConnection(); + + $tableName = $this->queryHelper->getTableName('Oro\Bundle\EmailBundle\Entity\EmailBody'); + $selectQuery = 'select id, body from ' . $tableName . ' where body is not null and text_body is null ' + . 'order by created desc limit :limit offset :offset'; + $pageNumber = 0; + $emailBodyHelper = new EmailBodyHelper(); + while (true) { + $output->writeln(sprintf('Process page %s.', $pageNumber + 1)); + $data = $connection->fetchAll( + $selectQuery, + ['limit' => self::BATCH_SIZE, 'offset' => self::BATCH_SIZE * $pageNumber], + ['limit' => 'integer', 'offset' => 'integer'] + ); + + // exit if we have no data anymore + if (count($data) === 0) { + break; + } + + foreach ($data as $dataArray) { + $connection->update( + $tableName, + ['text_body' => $emailBodyHelper->getTrimmedClearText($dataArray['body'])], + ['id' => $dataArray['id']], + ['textBody' => 'string'] + ); + } + + $pageNumber++; + } + + $output->writeln('Job complete.'); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/Email.php b/src/Oro/Bundle/EmailBundle/Entity/Email.php index 9e87244d380..f9b0c0a860c 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/Email.php +++ b/src/Oro/Bundle/EmailBundle/Entity/Email.php @@ -301,7 +301,7 @@ public function getSubject() */ public function setSubject($subject) { - $this->subject = $subject; + $this->subject = mb_substr($subject, 0, 998, mb_detect_encoding($subject)); return $this; } diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php b/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php index 49113a73b9e..7f0c471402e 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php @@ -54,6 +54,13 @@ class EmailBody */ protected $bodyIsText; + /** + * @var string + * + * @ORM\Column(name="text_body", type="text", nullable=true) + */ + protected $textBody; + /** * @var bool * @@ -271,4 +278,23 @@ public function __toString() { return (string)$this->getId(); } + + /** + * @return string + */ + public function getTextBody() + { + return $this->textBody; + } + + /** + * @param string $textBody + * @return $this + */ + public function setTextBody($textBody) + { + $this->textBody = $textBody; + + return $this; + } } diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php b/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php index c468aa19f07..6ab22aa2425 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php @@ -149,7 +149,7 @@ class EmailFolder /** * @var integer * - * @ORM\Column(name="failed_count", type="integer", nullable=false) + * @ORM\Column(name="failed_count", type="integer", nullable=false, options={"default" = "0"}) */ protected $failedCount = 0; diff --git a/src/Oro/Bundle/EmailBundle/Manager/EmailNotificationManager.php b/src/Oro/Bundle/EmailBundle/Manager/EmailNotificationManager.php index 27aa05101e4..003d6ba86d2 100644 --- a/src/Oro/Bundle/EmailBundle/Manager/EmailNotificationManager.php +++ b/src/Oro/Bundle/EmailBundle/Manager/EmailNotificationManager.php @@ -8,7 +8,6 @@ use Symfony\Component\Routing\Exception\RouteNotFoundException; use Oro\Bundle\EmailBundle\Entity\Email; -use Oro\Bundle\EmailBundle\Tools\EmailBodyHelper; use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; use Oro\Bundle\OrganizationBundle\Entity\Organization; use Oro\Bundle\UIBundle\Tools\HtmlTagHelper; @@ -23,9 +22,6 @@ class EmailNotificationManager /** @var HtmlTagHelper */ protected $htmlTagHelper; - /** @var EmailBodyHelper */ - protected $emailBodyHelper; - /** @var Router */ protected $router; @@ -40,20 +36,17 @@ class EmailNotificationManager * @param HtmlTagHelper $htmlTagHelper * @param Router $router * @param ConfigManager $configManager - * @param EmailBodyHelper $emailBodyHelper */ public function __construct( EntityManager $entityManager, HtmlTagHelper $htmlTagHelper, Router $router, - ConfigManager $configManager, - EmailBodyHelper $emailBodyHelper + ConfigManager $configManager ) { $this->em = $entityManager; $this->htmlTagHelper = $htmlTagHelper; $this->router = $router; $this->configManager = $configManager; - $this->emailBodyHelper = $emailBodyHelper; } /** @@ -80,9 +73,7 @@ public function getEmails(User $user, Organization $organization, $maxEmailsDisp $bodyContent = ''; $emailBody = $email->getEmailBody(); if ($emailBody) { - $bodyContent = $this->htmlTagHelper->shorten( - $this->emailBodyHelper->getClearBody($emailBody->getBodyContent()) - ); + $bodyContent = $emailBody->getTextBody(); } $emailId = $email->getId(); diff --git a/src/Oro/Bundle/EmailBundle/Migrations/Data/ORM/CollectEmailBodyJobFixture.php b/src/Oro/Bundle/EmailBundle/Migrations/Data/ORM/CollectEmailBodyJobFixture.php new file mode 100644 index 00000000000..8c41f35d7b7 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Migrations/Data/ORM/CollectEmailBodyJobFixture.php @@ -0,0 +1,27 @@ +persist($job); + $manager->flush($job); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Migrations/Schema/OroEmailBundleInstaller.php b/src/Oro/Bundle/EmailBundle/Migrations/Schema/OroEmailBundleInstaller.php index 5aa409ee8f0..c374bf1afd9 100644 --- a/src/Oro/Bundle/EmailBundle/Migrations/Schema/OroEmailBundleInstaller.php +++ b/src/Oro/Bundle/EmailBundle/Migrations/Schema/OroEmailBundleInstaller.php @@ -29,6 +29,8 @@ use Oro\Bundle\EmailBundle\Migrations\Schema\v1_24\OroEmailBundle as OroEmailBundle124; use Oro\Bundle\EmailBundle\Migrations\Schema\v1_26\OroEmailBundle as OroEmailBundle126; use Oro\Bundle\EmailBundle\Migrations\Schema\v1_27\OroEmailBundle as OroEmailBundle127; +use Oro\Bundle\EmailBundle\Migrations\Schema\v1_28\OroEmailBundle as OroEmailBundle128; +use Oro\Bundle\EmailBundle\Migrations\Schema\v1_29\OroEmailBundle as OroEmailBundle129; /** * Class OroEmailBundleInstaller @@ -43,7 +45,7 @@ class OroEmailBundleInstaller implements Installation */ public function getMigrationVersion() { - return 'v1_27'; + return 'v1_28'; } /** @@ -56,6 +58,7 @@ public function up(Schema $schema, QueryBag $queries) OroEmailBundle::oroEmailAttachmentTable($schema); OroEmailBundle::oroEmailAttachmentContentTable($schema); OroEmailBundle::oroEmailBodyTable($schema); + OroEmailBundle129::addTextBodyFieldToEmailBodyTable($schema); OroEmailBundle::oroEmailFolderTable($schema); OroEmailBundle::oroEmailOriginTable($schema); OroEmailBundle::oroEmailRecipientTable($schema); @@ -120,5 +123,7 @@ public function up(Schema $schema, QueryBag $queries) OroEmailBundle126::addEmailUserMailboxOwnerSeenIndex($schema); OroEmailBundle127::oroEmailFolderTable($schema); + + OroEmailBundle128::oroEmailFolderChangeColumn($schema); } } diff --git a/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_27/OroEmailBundle.php b/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_27/OroEmailBundle.php index 2c44d756e2e..ce5a8270033 100644 --- a/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_27/OroEmailBundle.php +++ b/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_27/OroEmailBundle.php @@ -24,7 +24,7 @@ public static function oroEmailFolderTable(Schema $schema) { $table = $schema->getTable('oro_email_folder'); if (!$table->hasColumn('failed_count')) { - $table->addColumn('failed_count', 'integer', ['notnull' => true]); + $table->addColumn('failed_count', 'integer', ['notnull' => true, 'default' => '0']); } } } diff --git a/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_28/OroEmailBundle.php b/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_28/OroEmailBundle.php new file mode 100644 index 00000000000..9dc02fb0322 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_28/OroEmailBundle.php @@ -0,0 +1,30 @@ +getTable('oro_email_folder'); + if ($table->hasColumn('failed_count')) { + $table->getColumn('failed_count')->setDefault("0"); + } + } +} diff --git a/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_29/OroEmailBundle.php b/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_29/OroEmailBundle.php new file mode 100644 index 00000000000..22114cc030f --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Migrations/Schema/v1_29/OroEmailBundle.php @@ -0,0 +1,48 @@ +container = $container; + } + + /** + * {@inheritdoc} + */ + public function up(Schema $schema, QueryBag $queries) + { + // We should not do anything if text_body field already exists. + // This field could be added during update from old versions. + if ($schema->getTable('oro_email_body')->hasColumn('text_body')) { + return; + } + + static::addTextBodyFieldToEmailBodyTable($schema); + } + + /** + * @param Schema $schema + */ + public static function addTextBodyFieldToEmailBodyTable(Schema $schema) + { + $table = $schema->getTable('oro_email_body'); + $table->addColumn('text_body', 'text', ['notnull' => false]); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Provider/EmailActivityListProvider.php b/src/Oro/Bundle/EmailBundle/Provider/EmailActivityListProvider.php index 0e6d3be643f..ae771239ed9 100644 --- a/src/Oro/Bundle/EmailBundle/Provider/EmailActivityListProvider.php +++ b/src/Oro/Bundle/EmailBundle/Provider/EmailActivityListProvider.php @@ -22,7 +22,6 @@ use Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface; use Oro\Bundle\EmailBundle\Entity\EmailUser; use Oro\Bundle\EmailBundle\Entity\Provider\EmailThreadProvider; -use Oro\Bundle\EmailBundle\Tools\EmailBodyHelper; use Oro\Bundle\EntityBundle\ORM\DoctrineHelper; use Oro\Bundle\EntityBundle\Provider\EntityNameResolver; use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; @@ -84,9 +83,6 @@ class EmailActivityListProvider implements /** @var CommentAssociationHelper */ protected $commentAssociationHelper; - /** @var EmailBodyHelper */ - protected $emailBodyHelper; - /** * @param DoctrineHelper $doctrineHelper * @param ServiceLink $doctrineRegistryLink @@ -99,7 +95,6 @@ class EmailActivityListProvider implements * @param ServiceLink $mailboxProcessStorageLink * @param ActivityAssociationHelper $activityAssociationHelper * @param CommentAssociationHelper $commentAssociationHelper - * @param EmailBodyHelper $emailBodyHelper * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -114,8 +109,7 @@ public function __construct( ServiceLink $securityFacadeLink, ServiceLink $mailboxProcessStorageLink, ActivityAssociationHelper $activityAssociationHelper, - CommentAssociationHelper $commentAssociationHelper, - EmailBodyHelper $emailBodyHelper + CommentAssociationHelper $commentAssociationHelper ) { $this->doctrineHelper = $doctrineHelper; $this->doctrineRegistryLink = $doctrineRegistryLink; @@ -128,7 +122,6 @@ public function __construct( $this->mailboxProcessStorageLink = $mailboxProcessStorageLink; $this->activityAssociationHelper = $activityAssociationHelper; $this->commentAssociationHelper = $commentAssociationHelper; - $this->emailBodyHelper = $emailBodyHelper; } /** @@ -194,11 +187,7 @@ public function getDescription($entity) { /** @var $entity Email */ if ($entity->getEmailBody()) { - $body = $entity->getEmailBody()->getBodyContent(); - $content = $this->emailBodyHelper->getClearBody($body); - $content = $this->htmlTagHelper->shorten($content); - - return $content; + return $entity->getEmailBody()->getTextBody(); } return null; diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml index c7daf39d4b4..8a1b2237d6c 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml @@ -100,7 +100,7 @@ datagrid: select: - partial eu.{id, email} - e - - eb.bodyContent AS body_content + - eb.textBody AS body_content - > (SELECT COUNT(_ec.id) FROM OroEmailBundle:Email _ec diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/services.yml b/src/Oro/Bundle/EmailBundle/Resources/config/services.yml index d1fc93d7acc..4aabfe6b73f 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/services.yml @@ -646,7 +646,6 @@ services: - '@oro_email.mailbox.process_storage' - '@oro_security.security_facade' - '@oro_email.related_emails.provider' - - '@oro_email.tools.email_body_helper' tags: - { name: twig.extension } @@ -677,7 +676,6 @@ services: - '@oro_email.mailbox.process_storage.link' - '@oro_activity.association_helper' - '@oro_comment.association_helper' - - '@oro_email.tools.email_body_helper' calls: - [ setSecurityContextLink, ['@security.context.link'] ] tags: @@ -861,7 +859,6 @@ services: - '@oro_ui.html_tag_helper' - '@router' - '@oro_entity_config.config_manager' - - '@oro_email.tools.email_body_helper' oro_email.datagrid.origin_folder.provider: class: %oro_email.datagrid.origin_folder.provider.class% @@ -1056,8 +1053,3 @@ services: - '@oro_email.email.activity.manager' - '@oro_email.provider.emailowners.provider' - '@oro_email.email.manager' - - oro_email.tools.email_body_helper: - class: Oro\Bundle\EmailBundle\Tools\EmailBodyHelper - arguments: - - '@oro_ui.html_tag_helper' diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/subject.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/subject.html.twig index c3917d2b6bf..35d37b6e6ad 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/subject.html.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/Datagrid/Property/subject.html.twig @@ -1,5 +1,5 @@ {% import 'OroEmailBundle::macros.html.twig' as EA %} -{% set emailBody = {bodyContent: record.getValue('body_content')} %} +{% set emailBody = {textBody: record.getValue('body_content')} %} {% set isNew = record.getValue('is_new') %} {% set valueToShow = value ? value : 'oro.email.subject.no_subject.label'|trans %} @@ -10,7 +10,7 @@ {% else %} {{ valueToShow }} {% endif %} - {% if emailBody.bodyContent is defined %} + {% if emailBody.textBody is defined %}
{{ EA.email_short_body(emailBody) }}
{% endif %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig index 98ab67d2fb3..cbf9b596cc7 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig @@ -576,13 +576,13 @@ {% endmacro %} {# - Strips email body from HTML-tags, removes sequence of '-' chars and cuts to fit the length + Removes sequence of '-' chars and cuts to fit the length Parameters: emailBody - email body entity Oro\Bundle\EmailBundle\Entity\EmailBody #} {% macro email_short_body(emailBody, length) %} {%- set length = length|default(150) -%} - {{ emailBody.bodyContent|oro_cleanup_email_body|oro_preg_replace('/\-{2,}/', '--')[:length]|replace({'--': '—'})|raw }} + {{ emailBody.textBody|oro_preg_replace('/\-{2,}/', '--')[:length]|replace({'--': '—'})|raw }} {% endmacro %} {# diff --git a/src/Oro/Bundle/EmailBundle/Sync/AbstractEmailSynchronizer.php b/src/Oro/Bundle/EmailBundle/Sync/AbstractEmailSynchronizer.php index d78cf9571a4..dd7ab609fce 100644 --- a/src/Oro/Bundle/EmailBundle/Sync/AbstractEmailSynchronizer.php +++ b/src/Oro/Bundle/EmailBundle/Sync/AbstractEmailSynchronizer.php @@ -4,6 +4,7 @@ use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ORM\EntityManager; +use Doctrine\ORM\ORMException; use JMS\JobQueueBundle\Entity\Job; @@ -31,6 +32,7 @@ abstract class AbstractEmailSynchronizer implements LoggerAwareInterface const SYNC_CODE_IN_PROCESS = 1; const SYNC_CODE_FAILURE = 2; const SYNC_CODE_SUCCESS = 3; + const SYNC_CODE_IN_PROCESS_FORCE = 4; const ID_OPTION = 'id'; @@ -55,6 +57,9 @@ abstract class AbstractEmailSynchronizer implements LoggerAwareInterface /** @var JobManager */ private $jobManager; + /** @var string */ + protected $clearInterval = 'P1D'; + /** * Constructor * @@ -126,6 +131,9 @@ public function sync($maxConcurrentTasks, $minExecIntervalInMin, $maxExecTimeInM } $startTime = $this->getCurrentUtcDateTime(); + if ($maxExecTimeInMin > 5) { + $this->clearInterval = 'PT' . ($maxExecTimeInMin * 5) . 'M'; + } $this->resetHangedOrigins(); $maxExecTimeout = $maxExecTimeInMin > 0 @@ -158,6 +166,9 @@ public function sync($maxConcurrentTasks, $minExecIntervalInMin, $maxExecTimeInM $this->doSyncOrigin($origin, new SynchronizationProcessorSettings()); } catch (SyncFolderTimeoutException $ex) { break; + } catch (ORMException $ex) { + $failedOriginIds[] = $origin->getId(); + break; } catch (\Exception $ex) { $failedOriginIds[] = $origin->getId(); } @@ -283,7 +294,9 @@ protected function doSyncOrigin(EmailOrigin $origin, SynchronizationProcessorSet } try { - if ($this->changeOriginSyncState($origin, self::SYNC_CODE_IN_PROCESS)) { + $inProcessCode = $settings && $settings->isForceMode() + ? self::SYNC_CODE_IN_PROCESS_FORCE : self::SYNC_CODE_IN_PROCESS; + if ($this->changeOriginSyncState($origin, $inProcessCode)) { $syncStartTime = $this->getCurrentUtcDateTime(); if ($settings) { $processor->setSettings($settings); @@ -396,7 +409,7 @@ protected function changeOriginSyncState(EmailOrigin $origin, $syncCode, $synchr ->setParameter('synchronized', $synchronizedAt); } - if ($syncCode === self::SYNC_CODE_IN_PROCESS) { + if ($syncCode === self::SYNC_CODE_IN_PROCESS || $syncCode === self::SYNC_CODE_IN_PROCESS_FORCE) { $qb->andWhere('(o.syncCode IS NULL OR o.syncCode <> :code)'); } @@ -447,6 +460,9 @@ protected function findOriginToSync($maxConcurrentTasks, $minExecIntervalInMin) $min = clone $now; $min->sub(new \DateInterval('P1Y')); + // time shift in minutes for fails origins + $timeShift = 30; + // rules: // - items with earlier sync code modification dates have higher priority // - previously failed items are shifted at 30 minutes back (it means that if sync failed @@ -456,19 +472,20 @@ protected function findOriginToSync($maxConcurrentTasks, $minExecIntervalInMin) $query = $repo->createQueryBuilder('o') ->select( 'o' - . ', CASE WHEN o.syncCode = :inProcess THEN 0 ELSE 1 END AS HIDDEN p1' - . ', (COALESCE(o.syncCode, 1000) * 30' - . ' + TIMESTAMPDIFF(MINUTE, COALESCE(o.syncCodeUpdatedAt, :min), :now)' - . ' / (CASE o.syncCode WHEN :success THEN 100 ELSE 1 END)) AS HIDDEN p2' + . ', CASE WHEN o.syncCode = :inProcess OR o.syncCode = :inProcessForce THEN 0 ELSE 1 END AS HIDDEN p1' + . ', (TIMESTAMPDIFF(MINUTE, COALESCE(o.syncCodeUpdatedAt, :min), :now)' + . ' - (CASE o.syncCode WHEN :success THEN 0 ELSE :timeShift END)) AS HIDDEN p2' ) ->where('o.isActive = :isActive AND (o.syncCodeUpdatedAt IS NULL OR o.syncCodeUpdatedAt <= :border)') ->orderBy('p1, p2 DESC, o.syncCodeUpdatedAt') ->setParameter('inProcess', self::SYNC_CODE_IN_PROCESS) + ->setParameter('inProcessForce', self::SYNC_CODE_IN_PROCESS_FORCE) ->setParameter('success', self::SYNC_CODE_SUCCESS) ->setParameter('isActive', true) ->setParameter('now', $now) ->setParameter('min', $min) ->setParameter('border', $border) + ->setParameter('timeShift', $timeShift) ->setMaxResults($maxConcurrentTasks + 1) ->getQuery(); @@ -476,7 +493,8 @@ protected function findOriginToSync($maxConcurrentTasks, $minExecIntervalInMin) $origins = $query->getResult(); $result = null; foreach ($origins as $origin) { - if ($origin->getSyncCode() !== self::SYNC_CODE_IN_PROCESS) { + $syncCode = $origin->getSyncCode(); + if ($syncCode !== self::SYNC_CODE_IN_PROCESS && $syncCode !== self::SYNC_CODE_IN_PROCESS_FORCE) { $result = $origin; break; } @@ -534,7 +552,7 @@ protected function resetHangedOrigins() $now = $this->getCurrentUtcDateTime(); $border = clone $now; - $border->sub(new \DateInterval('P1D')); + $border->sub(new \DateInterval($this->clearInterval)); $repo = $this->getEntityManager()->getRepository($this->getEmailOriginClass()); $query = $repo->createQueryBuilder('o') diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailBodyBuilderTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailBodyBuilderTest.php index c778afdbf66..79bb75f12fe 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailBodyBuilderTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailBodyBuilderTest.php @@ -32,6 +32,7 @@ public function testCreateEmailBody() $body = $this->emailBodyBuilder->getEmailBody(); $this->assertEquals(true, $body->getBodyIsText()); $this->assertEquals('test', $body->getBodyContent()); + $this->assertEquals('test', $body->getTextBody()); } /** diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailEntityBuilderTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailEntityBuilderTest.php index 87870f02963..c3aba477ec2 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailEntityBuilderTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailEntityBuilderTest.php @@ -175,6 +175,7 @@ public function testBody() $body = $this->builder->body('testContent', true, true); $this->assertEquals('testContent', $body->getBodyContent()); + $this->assertEquals('testContent', $body->getTextBody()); $this->assertFalse($body->getBodyIsText()); $this->assertTrue($body->getPersistent()); } diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php index 6db262eab3f..3f6a272ac79 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php @@ -68,4 +68,12 @@ public function testBeforeSave() $this->assertEquals(false, $entity->getPersistent()); $this->assertGreaterThanOrEqual($createdAt, $entity->getCreated()); } + + public function testTextBodyGetterAndSetter() + { + $entity = new EmailBody(); + self::assertNull($entity->getTextBody()); + $entity->setTextBody('some text'); + self::assertEquals('some text', $entity->getTextBody()); + } } diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php index c7f89d88e3d..677ae55266d 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php @@ -171,4 +171,34 @@ public function refsDataProvider() [' ref2', ['']], ]; } + + public function testSetSubjectOnLongString() + { + $activityList = new Email(); + $activityList->setSubject( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ut sem cursus ligula consectetur iaculis. ' + . 'Sed ac viverra mi, in auctor tortor. Aliquam id est laoreet, ultricies lectus a, aliquam lectus. Aenean' + . ' ac tristique eros. Integer vestibulum volutpat lacus, eu lobortis sapien condimentum in. Pellentesque ' + . 'a venenatis risus, id placerat nisi. Donec egestas maximus convallis. Cras eleifend leo quis neque ' + . 'rutrum suscipit. Nulla facilisi. Integer vel enim at tellus ornare condimentum. Nunc rhoncus urna nec ' + . 'scelerisque elementum. Pellentesque id ante sapien. Phasellus luctus facilisis massa, eu condimentum ' + . 'justo ultrices at. Curabitur purus diam, aliquet sit amet ante a, aliquet faucibus metus. Nam efficitur' + . ' tincidunt urna tincidunt tincidunt. Maecenas et dictum enim. Maecenas pellentesque purus et sapien ' + . 'vulputate efficitur. Curabitur egestas gravida venenatis. Nullam efficitur nulla eu augue vestibulum, ' + . 'ut imperdiet nibh pellentesque. Cras ultrices luctus magna vel sodales. Curabituä eget nullam.' + ); + self::assertEquals( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ut sem cursus ligula consectetur iaculis. ' + . 'Sed ac viverra mi, in auctor tortor. Aliquam id est laoreet, ultricies lectus a, aliquam lectus. Aenean' + . ' ac tristique eros. Integer vestibulum volutpat lacus, eu lobortis sapien condimentum in. Pellentesque ' + . 'a venenatis risus, id placerat nisi. Donec egestas maximus convallis. Cras eleifend leo quis neque ' + . 'rutrum suscipit. Nulla facilisi. Integer vel enim at tellus ornare condimentum. Nunc rhoncus urna nec ' + . 'scelerisque elementum. Pellentesque id ante sapien. Phasellus luctus facilisis massa, eu condimentum ' + . 'justo ultrices at. Curabitur purus diam, aliquet sit amet ante a, aliquet faucibus metus. Nam efficitur' + . ' tincidunt urna tincidunt tincidunt. Maecenas et dictum enim. Maecenas pellentesque purus et sapien ' + . 'vulputate efficitur. Curabitur egestas gravida venenatis. Nullam efficitur nulla eu augue vestibulum, ' + . 'ut imperdiet nibh pellentesque. Cras ultrices luctus magna vel sodales. Curabituä', + $activityList->getSubject() + ); + } } diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Manager/EmailNotificationManagerTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Manager/EmailNotificationManagerTest.php index f4b27e28475..93a01726f14 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Manager/EmailNotificationManagerTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Manager/EmailNotificationManagerTest.php @@ -98,34 +98,6 @@ public function getEmails() { $user = $this->getMockBuilder('Oro\Bundle\UserBundle\Entity\User')->disableOriginalConstructor()->getMock(); - $htmlBody = << - - - - - - - -

Lorem ipsum

-dolor sit amet, consectetur adipiscing elit. - - - - - - - - - -
Integersagittis
ornaredo
- - -EMAILBODY; - $emails = [ $this->prepareEmailUser( [ @@ -141,7 +113,7 @@ public function getEmails() [ 'getId' => 2, 'getSubject' => 'subject_1', - 'getBodyContent' => $htmlBody, + 'getBodyContent' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer', 'getFromName' => 'fromName_1', ], $user, @@ -168,7 +140,7 @@ public function getEmails() 'id' => 2, 'seen' => 1, 'subject' => 'subject_1', - 'bodyContent' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer sagittis ornare do', + 'bodyContent' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer', 'fromName' => 'fromName_1', 'linkFromName' => 'oro_email_email_reply', ] @@ -198,7 +170,7 @@ public function testGetCountNewEmails() protected function prepareEmailUser($values, $user, $seen) { $emailBody = new EmailBody(); - $emailBody->setBodyContent($values['getBodyContent']); + $emailBody->setTextBody($values['getBodyContent']); $email = new Email(); $email->setId($values['getId']); diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Sync/AbstractEmailSynchronizerTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Sync/AbstractEmailSynchronizerTest.php index 32f8d80c368..ba3bbf964e1 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Sync/AbstractEmailSynchronizerTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Sync/AbstractEmailSynchronizerTest.php @@ -2,6 +2,8 @@ namespace Oro\Bundle\EmailBundle\Tests\Unit\Sync; +use Doctrine\ORM\ORMException; + use Oro\Bundle\EmailBundle\Sync\AbstractEmailSynchronizer; use Oro\Bundle\EmailBundle\Tests\Unit\Fixtures\Entity\TestEmailOrigin; use Oro\Bundle\EmailBundle\Tests\Unit\Sync\Fixtures\TestEmailSynchronizer; @@ -101,6 +103,50 @@ public function testSyncNoOrigin() $sync->sync($maxConcurrentTasks, $minExecPeriodInMin); } + /** + * @expectedException \Exception + */ + public function testSyncOriginWithDoctrineError() + { + $now = new \DateTime('now', new \DateTimeZone('UTC')); + $maxConcurrentTasks = 3; + $minExecPeriodInMin = 1; + $origin = new TestEmailOrigin(123); + + $sync = $this->getMockBuilder('Oro\Bundle\EmailBundle\Tests\Unit\Sync\Fixtures\TestEmailSynchronizer') + ->disableOriginalConstructor() + ->setMethods( + [ + 'resetHangedOrigins', + 'findOriginToSync', + 'createSynchronizationProcessor', + 'changeOriginSyncState', + 'getCurrentUtcDateTime', + 'doSyncOrigin' + ] + ) + ->getMock(); + $sync->setLogger($this->logger); + + $sync->expects($this->once()) + ->method('getCurrentUtcDateTime') + ->will($this->returnValue($now)); + $sync->expects($this->once()) + ->method('resetHangedOrigins'); + $sync->expects($this->once()) + ->method('findOriginToSync') + ->with($maxConcurrentTasks, $minExecPeriodInMin) + ->will($this->returnValue($origin)); + $sync->expects($this->once()) + ->method('doSyncOrigin') + ->with($origin, $this->isInstanceOf('Oro\Bundle\EmailBundle\Sync\Model\SynchronizationProcessorSettings')) + ->will($this->throwException(new ORMException())); + $sync->expects($this->never()) + ->method('createSynchronizationProcessor'); + + $sync->sync($maxConcurrentTasks, $minExecPeriodInMin); + } + public function testDoSyncOrigin() { $now = new \DateTime('now', new \DateTimeZone('UTC')); @@ -389,6 +435,7 @@ public function testFindOriginToSync() $maxConcurrentTasks = 2; $minExecPeriodInMin = 1; + $timeShift = 30; $now = new \DateTime('now', new \DateTimeZone('UTC')); $border = clone $now; if ($minExecPeriodInMin > 0) { @@ -418,10 +465,9 @@ public function testFindOriginToSync() ->method('select') ->with( 'o' - . ', CASE WHEN o.syncCode = :inProcess THEN 0 ELSE 1 END AS HIDDEN p1' - . ', (COALESCE(o.syncCode, 1000) * 30' - . ' + TIMESTAMPDIFF(MINUTE, COALESCE(o.syncCodeUpdatedAt, :min), :now)' - . ' / (CASE o.syncCode WHEN :success THEN 100 ELSE 1 END)) AS HIDDEN p2' + . ', CASE WHEN o.syncCode = :inProcess OR o.syncCode = :inProcessForce THEN 0 ELSE 1 END AS HIDDEN p1' + . ', (TIMESTAMPDIFF(MINUTE, COALESCE(o.syncCodeUpdatedAt, :min), :now)' + . ' - (CASE o.syncCode WHEN :success THEN 0 ELSE :timeShift END)) AS HIDDEN p2' ) ->will($this->returnValue($qb)); $qb->expects($this->at($index++)) @@ -436,6 +482,10 @@ public function testFindOriginToSync() ->method('setParameter') ->with('inProcess', AbstractEmailSynchronizer::SYNC_CODE_IN_PROCESS) ->will($this->returnValue($qb)); + $qb->expects($this->at($index++)) + ->method('setParameter') + ->with('inProcessForce', AbstractEmailSynchronizer::SYNC_CODE_IN_PROCESS_FORCE) + ->will($this->returnValue($qb)); $qb->expects($this->at($index++)) ->method('setParameter') ->with('success', AbstractEmailSynchronizer::SYNC_CODE_SUCCESS) @@ -456,6 +506,10 @@ public function testFindOriginToSync() ->method('setParameter') ->with('border', $this->equalTo($border)) ->will($this->returnValue($qb)); + $qb->expects($this->at($index++)) + ->method('setParameter') + ->with('timeShift', $this->equalTo($timeShift)) + ->will($this->returnValue($qb)); $qb->expects($this->at($index++)) ->method('setMaxResults') ->with($maxConcurrentTasks + 1) diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Tools/EmailBodyHelperTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Tools/EmailBodyHelperTest.php index 0e754fc8c2a..c9a3091f3b9 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Tools/EmailBodyHelperTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Tools/EmailBodyHelperTest.php @@ -26,7 +26,7 @@ protected function setUp() */ public function testGetClearBody($bodyText, $expectedResult) { - $this->assertEquals($expectedResult, $this->bodyHelper->getClearBody($bodyText)); + $this->assertEquals($expectedResult, $this->bodyHelper->getTrimmedClearText($bodyText)); } public function bodyData() diff --git a/src/Oro/Bundle/EmailBundle/Tools/EmailBodyHelper.php b/src/Oro/Bundle/EmailBundle/Tools/EmailBodyHelper.php index f14bea08120..ebae9f96e50 100644 --- a/src/Oro/Bundle/EmailBundle/Tools/EmailBodyHelper.php +++ b/src/Oro/Bundle/EmailBundle/Tools/EmailBodyHelper.php @@ -2,22 +2,9 @@ namespace Oro\Bundle\EmailBundle\Tools; -use Oro\Bundle\UIBundle\Tools\HtmlTagHelper; - class EmailBodyHelper { - /** @var HtmlTagHelper */ - protected $htmlTagHelper; - - /** - * EmailBodyHelper constructor. - * - * @param HtmlTagHelper $htmlTagHelper - */ - public function __construct(HtmlTagHelper $htmlTagHelper) - { - $this->htmlTagHelper = $htmlTagHelper; - } + const MAX_STRING_LENGTH = 500; /** * Returns the plain text representation of email body @@ -26,11 +13,8 @@ public function __construct(HtmlTagHelper $htmlTagHelper) * * @return string */ - public function getClearBody($bodyContent) + public function getTrimmedClearText($bodyContent) { - /** - * @todo: Should be refactored or deleted in scope of BAP-11622 - */ if (extension_loaded('tidy')) { $config = [ 'show-body-only' => true, @@ -38,7 +22,7 @@ public function getClearBody($bodyContent) 'hide-comments' => true ]; $tidy = new \tidy(); - $body = $tidy->repairString($bodyContent, $config, 'UTF8'); + $body = $tidy->repairString($bodyContent, $config, 'utf8'); } else { $body = $bodyContent; // get `body` content in case of html text @@ -46,10 +30,22 @@ public function getClearBody($bodyContent) $body = $bodyText[1]; } } + // Clear style and script tags + $body = strip_tags(html_entity_decode(preg_replace('/<(style|script).*?>.*?<\/\1>/su', '', $body))); + // Clear non printed symbols + $body = preg_replace('/(?>[\x00-\x1F]|\xC2[\x80-\x9F]|\xE2[\x80-\x8F]{2}|' + . '\xE2\x80[\xA4-\xA8]|\xE2\x81[\x9F-\xAF])/u', ' ', $body); - // clear `script` and `style` tags from content - $body = preg_replace('/<(style|script).*?>.*?<\/\1>/s', '', $body); + $body = trim(preg_replace('/(\s\s+|\n+)/u', ' ', $body)); + // trim the text content + if (strlen($body) > self::MAX_STRING_LENGTH) { + $body = substr($body, 0, self::MAX_STRING_LENGTH); + $lastOccurrencePos = strrpos($body, ' ', null); + if ($lastOccurrencePos !== false) { + $body = substr($body, 0, $lastOccurrencePos); + } + } - return preg_replace('/(\s\s+|\n+|[^[:print:]])/', ' ', $this->htmlTagHelper->stripTags($body)); + return $body; } } diff --git a/src/Oro/Bundle/EmailBundle/Twig/EmailExtension.php b/src/Oro/Bundle/EmailBundle/Twig/EmailExtension.php index 1974095575d..b77c57d55b8 100644 --- a/src/Oro/Bundle/EmailBundle/Twig/EmailExtension.php +++ b/src/Oro/Bundle/EmailBundle/Twig/EmailExtension.php @@ -17,7 +17,6 @@ use Oro\Bundle\EmailBundle\Model\WebSocket\WebSocketSendProcessor; use Oro\Bundle\EmailBundle\Provider\RelatedEmailsProvider; use Oro\Bundle\EmailBundle\Tools\EmailAddressHelper; -use Oro\Bundle\EmailBundle\Tools\EmailBodyHelper; use Oro\Bundle\EmailBundle\Tools\EmailHolderHelper; use Oro\Bundle\SecurityBundle\SecurityFacade; @@ -46,9 +45,6 @@ class EmailExtension extends Twig_Extension /** @var RelatedEmailsProvider */ protected $relatedEmailsProvider; - /** @var EmailBodyHelper */ - protected $emailBodyHelper; - /** * @param EmailHolderHelper $emailHolderHelper * @param EmailAddressHelper $emailAddressHelper @@ -57,7 +53,6 @@ class EmailExtension extends Twig_Extension * @param MailboxProcessStorage $mailboxProcessStorage * @param SecurityFacade $securityFacade * @param RelatedEmailsProvider $relatedEmailsProvider - * @param EmailBodyHelper $emailBodyHelper */ public function __construct( EmailHolderHelper $emailHolderHelper, @@ -66,8 +61,7 @@ public function __construct( EntityManager $em, MailboxProcessStorage $mailboxProcessStorage, SecurityFacade $securityFacade, - RelatedEmailsProvider $relatedEmailsProvider, - EmailBodyHelper $emailBodyHelper + RelatedEmailsProvider $relatedEmailsProvider ) { $this->emailHolderHelper = $emailHolderHelper; $this->emailAddressHelper = $emailAddressHelper; @@ -76,7 +70,6 @@ public function __construct( $this->mailboxProcessStorage = $mailboxProcessStorage; $this->securityFacade = $securityFacade; $this->relatedEmailsProvider = $relatedEmailsProvider; - $this->emailBodyHelper = $emailBodyHelper; } /** @@ -97,28 +90,6 @@ public function getFunctions() ]; } - /** - * {@inheritDoc} - */ - public function getFilters() - { - return [ - new \Twig_SimpleFilter('oro_cleanup_email_body', [$this, 'getCleanEmailBody']) - ]; - } - - /** - * Returns clean text representation without tags - * - * @param string $emailBodyText - * - * @return string - */ - public function getCleanEmailBody($emailBodyText) - { - return $this->emailBodyHelper->getClearBody($emailBodyText); - } - /** * Gets the email address of the given object * diff --git a/src/Oro/Bundle/EntityExtendBundle/OroEntityExtendBundle.php b/src/Oro/Bundle/EntityExtendBundle/OroEntityExtendBundle.php index 8ba8846935b..270f071e8eb 100644 --- a/src/Oro/Bundle/EntityExtendBundle/OroEntityExtendBundle.php +++ b/src/Oro/Bundle/EntityExtendBundle/OroEntityExtendBundle.php @@ -122,7 +122,12 @@ private function checkConfigs() $process = $pb->getProcess(); $exitStatusCode = $process->run(); if ($exitStatusCode) { - throw new \RuntimeException($process->getErrorOutput()); + $output = $process->getErrorOutput(); + + if (empty($output)) { + $output = $process->getOutput(); + } + throw new \RuntimeException($output); } return; diff --git a/src/Oro/Bundle/FormBundle/Form/Extension/JsValidationExtension.php b/src/Oro/Bundle/FormBundle/Form/Extension/JsValidationExtension.php index 4a32e6ec82b..0e3bc09e941 100644 --- a/src/Oro/Bundle/FormBundle/Form/Extension/JsValidationExtension.php +++ b/src/Oro/Bundle/FormBundle/Form/Extension/JsValidationExtension.php @@ -32,19 +32,19 @@ public function __construct(ConstraintsProvider $constraintsProvider) */ public function finishView(FormView $view, FormInterface $form, array $options) { - $this->addDataValidationOptionalGroupAttribute($view, $options); + $this->addDataValidationOptionalGroupAttributes($view, $options); $this->addDataValidationAttribute($view, $form); } /** - * Adds "data-validation-optional-group" attribute to embedded form. + * Adds "data-validation-optional-group" attributes to embedded form. * * Validation will run only if one of the children is filled in. * * @param FormView $view * @param array $options */ - protected function addDataValidationOptionalGroupAttribute(FormView $view, array $options) + protected function addDataValidationOptionalGroupAttributes(FormView $view, array $options) { if ($this->isOptionalEmbeddedFormView($view, $options)) { $view->vars['attr']['data-validation-optional-group'] = null; diff --git a/src/Oro/Bundle/FormBundle/Form/Type/CollectionType.php b/src/Oro/Bundle/FormBundle/Form/Type/CollectionType.php index 2b67baad282..5640fa285b9 100644 --- a/src/Oro/Bundle/FormBundle/Form/Type/CollectionType.php +++ b/src/Oro/Bundle/FormBundle/Form/Type/CollectionType.php @@ -7,7 +7,7 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\Options; -use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; use Oro\Bundle\FormBundle\Form\EventListener\CollectionTypeSubscriber; @@ -47,7 +47,7 @@ public function buildView(FormView $view, FormInterface $form, array $options) /** * {@inheritdoc} */ - public function setDefaultOptions(OptionsResolverInterface $resolver) + public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults( [ @@ -66,13 +66,9 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) ] ); $resolver->setRequired(['type']); - $resolver->setNormalizers( - [ - 'show_form_when_empty' => function (Options $options, $value) { - return !$options['allow_add'] ? false : $value; - } - ] - ); + $resolver->setNormalizer('show_form_when_empty', function (Options $options, $value) { + return !$options['allow_add'] ? false : $value; + }); } /** diff --git a/src/Oro/Bundle/FormBundle/Resources/config/requirejs.yml b/src/Oro/Bundle/FormBundle/Resources/config/requirejs.yml index 46dbc5a5974..423c9c12286 100644 --- a/src/Oro/Bundle/FormBundle/Resources/config/requirejs.yml +++ b/src/Oro/Bundle/FormBundle/Resources/config/requirejs.yml @@ -53,6 +53,7 @@ config: 'oroform/js/validator/type': 'bundles/oroform/js/validator/type.js' 'oroform/js/validator/url': 'bundles/oroform/js/validator/url.js' 'oroform/js/optional-validation-handler': 'bundles/oroform/js/optional-validation-handler.js' + 'oroform/js/optional-validation-groups-handler': 'bundles/oroform/js/optional-validation-groups-handler.js' #inline editing 'oroform/js/tools/frontend-type-map': 'bundles/oroform/js/tools/frontend-type-map.js' diff --git a/src/Oro/Bundle/FormBundle/Resources/doc/reference/js_validation.md b/src/Oro/Bundle/FormBundle/Resources/doc/reference/js_validation.md index 7d01c3ba967..68e6e21cd96 100644 --- a/src/Oro/Bundle/FormBundle/Resources/doc/reference/js_validation.md +++ b/src/Oro/Bundle/FormBundle/Resources/doc/reference/js_validation.md @@ -85,6 +85,50 @@ In case you have one form which saves several different entities at once (e.g. c ``` After that, validation for sub-entinty works only if some of fields is not blank. Otherwise it ignores all validation rules for fields elements of sub-entity. +###Override of optional validation logic +In case if you want to customize "optional validation group" behaviour you can override a handler which is responsible for +handle field changes in specific optional validation group. In this case you need: +1) add custom handler to requirejs.yml +``` +config: + paths: + example/js/custom-handler: 'bundles/example/js/custom-handler.js' +``` + +Custom optional validation handler should have two methods: initialize and handle. +Method "Initialise" is responsible for update validation state for "optional validation group" after it will be loaded to the page. +Method "Handle" is responsible for update "optional validation group" validation state after the descendant field will be changed. + +You can have any level of "optional validation group" inheritance in your page. In case if your field has more than one "optional validation group" ancestor, +all "optional validation group" handlers will be called from closest ancestor to root by default. +This behaviour is configurable, you can simply return `true` or `false` in your custom "Handle" method. + +2) add data attribute to validation group +``` + +--
+ +-- + +-- + +-- +``` +3) all custom handlers should be preloaded to avoid situation when form was loaded but handler was not. To avoid such +situations you should add custom application module +``` +requirejs.yml: + paths: + example/js/custom-handler: 'bundles/example/js/custom-handler.js' + example/js/custom-module: 'bundles/example/js/custom-module.js' + config: + appmodules: + - example/js/custom-module + +custom-handler.js: + define([ + 'oroui/js/app/controllers/base/controller' + ], function(BaseController) { + BaseController.loadBeforeAction(['example/js/custom-handler'], function() {}); + }); +``` + ## Ignore validation section There are cases when developer need to suppress validation for some field or group of fields. It can be done over `data-validation-ignore` attribute of container element. It works the same way as with `data-validation-optional-group` attribute, except validator omit these fields even if they have some value. ``` diff --git a/src/Oro/Bundle/FormBundle/Resources/public/js/extend/validate.js b/src/Oro/Bundle/FormBundle/Resources/public/js/extend/validate.js index 606426ad001..336b80d5652 100644 --- a/src/Oro/Bundle/FormBundle/Resources/public/js/extend/validate.js +++ b/src/Oro/Bundle/FormBundle/Resources/public/js/extend/validate.js @@ -4,7 +4,7 @@ define([ 'orotranslation/js/translator', 'oroui/js/tools', 'oroui/js/tools/logger', - 'oroform/js/optional-validation-handler', + 'oroform/js/optional-validation-groups-handler', 'jquery.validate' ], function($, _, __, tools, logger, validationHandler) { 'use strict'; diff --git a/src/Oro/Bundle/FormBundle/Resources/public/js/optional-validation-groups-handler.js b/src/Oro/Bundle/FormBundle/Resources/public/js/optional-validation-groups-handler.js new file mode 100644 index 00000000000..a3b832434e8 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Resources/public/js/optional-validation-groups-handler.js @@ -0,0 +1,90 @@ +define(['jquery', 'oroform/js/optional-validation-handler'], function($, defaultOptionalValidationHandler) { + 'use strict'; + + return { + /** + * @constructor + */ + initialize: function(formElement) { + var self = this; + + formElement.on( + 'change', + 'input, select, textarea', + function() { + $(this).trigger('validation-optional-group-value-changed'); + } + ); + + /** + * Custom event used to not interrupt default change event + */ + formElement.on( + 'validation-optional-group-value-changed', + '[data-validation-optional-group]', + function(event) { + var shouldBeBubbled = self.handleFormChanges($(this), $(event.target)); + if (!shouldBeBubbled) { + event.stopPropagation(); + } + } + ); + + self.initializeOptionalValidationGroupHandlers(formElement); + }, + + /** + * @param {jQuery} $group Optional validation elements group + * @param {jQuery} $element Current Element + * + * @return {boolean} + */ + handleFormChanges: function($group, $element) { + var optionalValidationHandler = this.getHandler($group); + + return optionalValidationHandler.handle($group, $element); + }, + + /** + * @param {jQuery} $formElement + * + * @return {boolean} + */ + initializeOptionalValidationGroupHandlers: function($formElement) { + var self = this; + + var rootOptionalValidationGroups = this.getRootLevelOptionalValidationGroups($formElement); + rootOptionalValidationGroups.each(function(index, group) { + var $group = $(group); + self.initializeOptionalValidationGroupHandlers($group); + + var optionalValidationHandler = self.getHandler($group); + optionalValidationHandler.initialize($group); + }); + }, + + /** + * @param {jQuery} $element + * + * @return {boolean} + */ + getRootLevelOptionalValidationGroups: function($element) { + return $element.find('[data-validation-optional-group]') + .not('[data-validation-optional-group] [data-validation-optional-group]'); + }, + + /** + * @param {jQuery} $group Optional validation elements group + * + * @return {OptionalValidationHandler} + */ + getHandler: function($group) { + /** + * Handlers should be preloaded using Controller::loadBeforeAction + */ + var handler = $group.data('validation-optional-group-handler'); + + return handler ? require(handler) : defaultOptionalValidationHandler; + } + }; +}); diff --git a/src/Oro/Bundle/FormBundle/Resources/public/js/optional-validation-handler.js b/src/Oro/Bundle/FormBundle/Resources/public/js/optional-validation-handler.js index 5a819551dd6..75b3dd526ca 100644 --- a/src/Oro/Bundle/FormBundle/Resources/public/js/optional-validation-handler.js +++ b/src/Oro/Bundle/FormBundle/Resources/public/js/optional-validation-handler.js @@ -3,18 +3,47 @@ define(['jquery'], function($) { /** * @export oroform/js/optional-validation-handler - * @class oroform.optionalValidationHandler + * @class OptionalValidationHandler */ return { /** - * @param {jQuery} group + * @param {jQuery} $group Optional validation elements group + */ + initialize: function($group) { + var self = this; + + var labels = this.getGroupElements($group, $group.find('label[data-required]')); + labels.addClass('required'); + + var labelAsterisk = labels.find('em'); + labelAsterisk.hide().html('*'); + if (self.hasNotEmptyDescendantGroup($group) || + self.hasNotEmptyInput($group) || + self.hasNotEmptySelect($group)) { + labelAsterisk.show(); + + $group.data('group-validation-required', true); + } + }, + + /** + * @param {jQuery} $group + * @returns {boolean} + */ + hasNotEmptyDescendantGroup: function($group) { + return $group.find('[data-validation-optional-group][data-group-validation-required]').length > 0; + }, + + /** + * @param {jQuery} $group * @returns {boolean} */ - hasNotEmptyInput: function(group) { - var elementsSelector = 'input[type!="checkbox"][type!="radio"][type!="button"][data-required],' + + hasNotEmptyInput: function($group) { + var elementsSelector = 'textarea, input[type!="checkbox"][type!="radio"][type!="button"][data-required],' + ' input[type="radio"][data-required]:checked,' + ' input[type="checkbox"][data-required]:checked'; - var checkedElements = group.find(elementsSelector); + + var checkedElements = this.getGroupElements($group, $group.find(elementsSelector)); for (var i = 0; i < checkedElements.length; i++) { if (!this.isValueEmpty($(checkedElements[i]).val())) { return true; @@ -25,11 +54,11 @@ define(['jquery'], function($) { }, /** - * @param {jQuery} group + * @param {jQuery} $group * @returns {boolean} */ - hasNotEmptySelect: function(group) { - var elements = group.find('select[data-required]'); + hasNotEmptySelect: function($group) { + var elements = this.getGroupElements($group, $group.find('select')); for (var i = 0; i < elements.length; i++) { if (!this.isValueEmpty($(elements[i]).find('option:selected').val())) { return true; @@ -40,69 +69,95 @@ define(['jquery'], function($) { }, /** - * @param {string|undefined} value - * @returns {boolean} + * @param {jQuery} $group Optional validation elements group + * @param {jQuery} $element Changed Element + * + * @return {boolean} Should parent OptionalValidationHandler be called */ - isValueEmpty: function(value) { - value = value ? $.trim(value) : ''; - return !value; + handle: function($group, $element) { + var tagName = $element.prop('tagName').toLowerCase(); + + switch (tagName) { + case 'select': + this.selectHandler($group, $element); + break; + case 'textarea': + case 'input': + this.inputHandler($group, $element); + break; + } + + return true; }, /** - * @param {jQuery} element - * @param {string|undefined} value + * @param {jQuery} $group Optional validation elements group + * @param {jQuery} $element Changed Element + */ + inputHandler: function($group, $element) { + this.handleGroupRequire($group, $element.val()); + }, + + /** + * @param {jQuery} $group Optional validation elements group + * @param {jQuery} $element Changed Element */ - handleGroupRequire: function(element, value) { - var group = element.parents('[data-validation-optional-group]'); + selectHandler: function($group, $element) { + this.handleGroupRequire($group, $element.find('option:selected').val()); + }, + /** + * @param {jQuery} $group Optional validation elements group + * @param {string|undefined} value Changed Element value + */ + handleGroupRequire: function($group, value) { if (this.isValueEmpty(value)) { - if (!this.hasNotEmptyInput(group) && !this.hasNotEmptySelect(group)) { - group.find('label[data-required] em').hide(); - } + $group.find('label[data-required] em').hide(); + this.clearValidationErrorsAndDisableValidation($group); + $group.data('group-validation-required', false); } else { - group.find('label[data-required] em').show(); + $group.find('label[data-required] em').show(); + var inputs = this.getGroupElements($group, $group.find('input, select, textarea')); + inputs.data('ignore-validation', false); + $group.data('group-validation-required', true); } }, /** - * @param {jQuery} element + * @param {string|undefined} value + * @returns {boolean} */ - inputHandler: function(element) { - this.handleGroupRequire(element, element.val()); + isValueEmpty: function(value) { + value = value ? $.trim(value) : ''; + return !value; }, /** - * @param {jQuery} element + * @param {jQuery} $group */ - selectHandler: function(element) { - this.handleGroupRequire(element, element.find('option:selected').val()); + clearValidationErrorsAndDisableValidation: function($group) { + var validator = $group.validate(); + var inputs = this.getGroupElements($group, $group.find('input, select, textarea')); + inputs.data('ignore-validation', true); + inputs.each( + function(key, element) { + validator.hideElementErrors($(element)); + } + ); }, /** - * @constructor + * @param {jQuery} $group + * @param {jQuery} $elements + * + * @return {jQuery} */ - initialize: function(formElement) { - var self = this; - - var groups = formElement.find('[data-validation-optional-group]'); - var labels = groups.find('label[data-required]'); - - labels.find('em').hide().html('*'); - labels.addClass('required'); - - groups.on('change', 'input', function() { - self.inputHandler($(this)); - }); - groups.on('change', 'select', function() { - self.selectHandler($(this)); + getGroupElements: function($group, $elements) { + $elements.filter(function(key, element) { + return $(element).closest('[data-validation-optional-group]').get(0) === $group.get(0); }); - groups.each(function(index, group) { - group = $(group); - if (self.hasNotEmptyInput(group) || self.hasNotEmptySelect(group)) { - group.find('label[data-required] em').show(); - } - }); + return $elements; } }; }); diff --git a/src/Oro/Bundle/FormBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/FormBundle/Resources/views/Form/fields.html.twig index 45842d54764..74e4c7cda7f 100644 --- a/src/Oro/Bundle/FormBundle/Resources/views/Form/fields.html.twig +++ b/src/Oro/Bundle/FormBundle/Resources/views/Form/fields.html.twig @@ -314,7 +314,13 @@ {% set allow_delete = allow_delete and widget.vars.allow_delete %} {% endif %} {% endif %} -
+
{{ form_widget(form, {disabled: disabled}) }} {% if allow_delete %} diff --git a/src/Oro/Bundle/ImapBundle/Connector/ImapMessageIterator.php b/src/Oro/Bundle/ImapBundle/Connector/ImapMessageIterator.php index f4d51ea7b3f..653a670cd08 100644 --- a/src/Oro/Bundle/ImapBundle/Connector/ImapMessageIterator.php +++ b/src/Oro/Bundle/ImapBundle/Connector/ImapMessageIterator.php @@ -3,6 +3,7 @@ namespace Oro\Bundle\ImapBundle\Connector; +use Oro\Bundle\ImapBundle\Mail\Protocol\Exception\InvalidEmailFormatException; use Oro\Bundle\ImapBundle\Mail\Storage\Imap; use Oro\Bundle\ImapBundle\Mail\Storage\Message; @@ -131,7 +132,10 @@ public function next() } $messages = $this->imap->getMessages(array_values($ids)); foreach ($ids as $pos => $id) { - $this->batch[$pos] = isset($messages[$id]) ? $messages[$id] : null; + if (!isset($messages[$id])) { + throw new InvalidEmailFormatException('Invalid email format'); + } + $this->batch[$pos] = $messages[$id]; } } else { $this->batch[$this->iterationPos] = $this->imap->getMessage( diff --git a/src/Oro/Bundle/ImapBundle/Mail/Storage/Imap.php b/src/Oro/Bundle/ImapBundle/Mail/Storage/Imap.php index dce445d5d29..13fb0ca4d34 100644 --- a/src/Oro/Bundle/ImapBundle/Mail/Storage/Imap.php +++ b/src/Oro/Bundle/ImapBundle/Mail/Storage/Imap.php @@ -311,6 +311,10 @@ public function uidSearch(array $criteria) } $response = $this->protocol->requestAndResponse('UID SEARCH', $criteria); + if (!is_array($response)) { + throw new BaseException\RuntimeException('Cannot search messages.'); + } + foreach ($response as $ids) { if ($ids[0] === 'SEARCH') { array_shift($ids); @@ -319,10 +323,6 @@ public function uidSearch(array $criteria) } } - if (!is_array($response)) { - throw new BaseException\RuntimeException('Cannot search messages.'); - } - return $response; } diff --git a/src/Oro/Bundle/ImapBundle/Sync/ImapEmailSynchronizationProcessor.php b/src/Oro/Bundle/ImapBundle/Sync/ImapEmailSynchronizationProcessor.php index 99072d3fc28..8f65db9ae9b 100644 --- a/src/Oro/Bundle/ImapBundle/Sync/ImapEmailSynchronizationProcessor.php +++ b/src/Oro/Bundle/ImapBundle/Sync/ImapEmailSynchronizationProcessor.php @@ -46,6 +46,9 @@ class ImapEmailSynchronizationProcessor extends AbstractEmailSynchronizationProc /** @var ImapEmailRemoveManager */ protected $removeManager; + /** @var int */ + private $processStartTime; + /** * Constructor * @@ -76,7 +79,7 @@ public function process(EmailOrigin $origin, $syncStartTime) $this->emailEntityBuilder->clear(); $this->initEnv($origin); - $processStartTime = time(); + $this->processStartTime = time(); // iterate through all folders enabled for sync and do a synchronization of emails for each one $imapFolders = $this->getSyncEnabledImapFolders($origin, true); foreach ($imapFolders as $imapFolder) { @@ -117,9 +120,7 @@ public function process(EmailOrigin $origin, $syncStartTime) $this->cleanUp(true, $imapFolder->getFolder()); - $processSpentTime = time() - $processStartTime; - - if (false === $this->getSettings()->isForceMode() && $processSpentTime > self::MAX_ORIGIN_SYNC_TIME) { + if ($this->isTimeout()) { break; } } @@ -166,7 +167,7 @@ protected function syncEmails(EmailOrigin $origin, ImapEmailFolder $imapFolder) $lastSynchronizedAt = $folder->getSynchronizedAt(); $emails = $this->getEmailIterator($origin, $imapFolder, $folder); $count = $processed = $invalid = $totalInvalid = 0; - $emails->setIterationOrder(true); + $emails->setIterationOrder(false); $emails->setBatchSize(self::READ_BATCH_SIZE); $emails->setConvertErrorCallback( function (\Exception $e) use (&$invalid) { @@ -211,6 +212,9 @@ function (\Exception $e) use (&$invalid) { $count = 0; $batch = []; } + if ($this->isTimeout()) { + break; + } } if ($count > 0) { $this->saveEmails( @@ -598,4 +602,16 @@ protected function processUnselectableFolderException(EmailFolder $folder) ); } } + + /** + * Check timeout for done work with current origin + * Exclude force mode + * + * @return bool + */ + protected function isTimeout() + { + return false === $this->getSettings()->isForceMode() + && time() - $this->processStartTime > self::MAX_ORIGIN_SYNC_TIME; + } } diff --git a/src/Oro/Bundle/SearchBundle/Command/ReindexCommand.php b/src/Oro/Bundle/SearchBundle/Command/ReindexCommand.php index ac3a2775a1c..eee88a76b2a 100644 --- a/src/Oro/Bundle/SearchBundle/Command/ReindexCommand.php +++ b/src/Oro/Bundle/SearchBundle/Command/ReindexCommand.php @@ -7,7 +7,8 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Oro\Bundle\SearchBundle\Engine\EngineInterface; +use Oro\Component\Log\ConsoleProgressLogger; +use Oro\Component\Log\ProgressLoggerAwareInterface; /** * Update and reindex (automatically) fulltext-indexed table(s). @@ -72,7 +73,11 @@ protected function execute(InputInterface $input, OutputInterface $output) /** @var $searchEngine EngineInterface */ $searchEngine = $this->getContainer()->get('oro_search.search.engine'); - + if ($output->getVerbosity() === OutputInterface::VERBOSITY_DEBUG && + $searchEngine instanceof ProgressLoggerAwareInterface + ) { + $searchEngine->setProgressLogger(new ConsoleProgressLogger($output)); + } $recordsCount = $searchEngine->reindex($class, $offset, $limit); $output->writeln(sprintf('Total indexed items: %u', $recordsCount)); diff --git a/src/Oro/Bundle/SearchBundle/Engine/AbstractEngine.php b/src/Oro/Bundle/SearchBundle/Engine/AbstractEngine.php index 040163a6579..0b5470aea2d 100644 --- a/src/Oro/Bundle/SearchBundle/Engine/AbstractEngine.php +++ b/src/Oro/Bundle/SearchBundle/Engine/AbstractEngine.php @@ -45,6 +45,9 @@ abstract class AbstractEngine implements EngineInterface /** @var bool */ protected $logQueries = false; + /** @var \Iterator[]|\Countable[] */ + protected $iteratorCache = []; + /** * @param ManagerRegistry $registry * @param EventDispatcherInterface $eventDispatcher @@ -176,56 +179,105 @@ protected function scheduleIndexation($entity) * @param string $entityName * @param integer|null $offset * @param integer|null $limit + * * @return int */ protected function reindexSingleEntity($entityName, $offset = null, $limit = null) { - /** @var EntityManager $entityManager */ - $entityManager = $this->registry->getManagerForClass($entityName); - $entityManager->getConnection()->getConfiguration()->setSQLLogger(null); - - $pk = $entityManager->getClassMetadata($entityName)->getIdentifier(); + $iterator = $this->createIterator($entityName, $offset, $limit); - $orderingsExpr = new OrderBy(); - foreach ($pk as $fieldName) { - $orderingsExpr->add('entity.' . $fieldName); + $itemsCount = 0; + foreach ($iterator as $entity) { + $this->recordProcessed(); + $itemsCount++; } - $queryBuilder = $entityManager->getRepository($entityName) - ->createQueryBuilder('entity') - ->orderBy($orderingsExpr); + return $itemsCount; + } - if (null !== $offset) { - $queryBuilder->setFirstResult($offset); - } - if (null !== $limit) { - $queryBuilder->setMaxResults($limit); - } + /** + * Called each time record was processed to keep track of progress + */ + protected function recordProcessed() + { + } - $iterator = new BufferedQueryResultIterator($queryBuilder); - $iterator->setBufferSize(static::BATCH_SIZE); + /** + * @return int + */ + protected function getNumberOfRecordsToReindex($entityName, $offset = null, $limit = null) + { + return count($this->createIterator($entityName, $offset, $limit, true)); + } - $itemsCount = 0; - $entities = []; + /** + * @param string $entityName + * @param integer|null $offset + * @param integer|null $limit + * @param bool $cache + * + * @return \Iterator|\Countable + */ + protected function createIterator($entityName, $offset = null, $limit = null, $cache = false) + { + $key = $this->createIteratorCacheKey($entityName, $offset, $limit); + if (!isset($this->iteratorCache[$key])) { + /** @var EntityManager $entityManager */ + $entityManager = $this->registry->getManagerForClass($entityName); + $entityManager->getConnection()->getConfiguration()->setSQLLogger(null); - foreach ($iterator as $entity) { - $entities[] = $entity; - $itemsCount++; + $pk = $entityManager->getClassMetadata($entityName)->getIdentifier(); + + $orderingsExpr = new OrderBy(); + foreach ($pk as $fieldName) { + $orderingsExpr->add('entity.' . $fieldName); + } + + $queryBuilder = $entityManager->getRepository($entityName) + ->createQueryBuilder('entity') + ->orderBy($orderingsExpr); + + if (null !== $offset) { + $queryBuilder->setFirstResult($offset); + } + if (null !== $limit) { + $queryBuilder->setMaxResults($limit); + } - if (0 == $itemsCount % static::BATCH_SIZE) { + $iterator = new BufferedQueryResultIterator($queryBuilder); + $iterator->setBufferSize(static::BATCH_SIZE); + $iterator->setPageLoadedCallback(function (array $entities) use ($entityManager) { $this->save($entities, true); $entityManager->clear(); - $entities = []; gc_collect_cycles(); + + return $entities; + }); + + if ($cache) { + $this->iteratorCache[$key] = $iterator; } + } else { + $iterator = $this->iteratorCache[$key]; } - if ($itemsCount % static::BATCH_SIZE > 0) { - $this->save($entities, true); - $entityManager->clear(); + if (!$cache) { + unset($this->iteratorCache[$key]); } - return $itemsCount; + return $iterator; + } + + /** + * @param string $entityName + * @param integer|null $offset + * @param integer|null $limit + * + * @return string + */ + protected function createIteratorCacheKey($entityName, $offset = null, $limit = null) + { + return sprintf('%d.%d.%d', $entityName, $offset, $limit); } /** diff --git a/src/Oro/Bundle/SearchBundle/Engine/AbstractMapper.php b/src/Oro/Bundle/SearchBundle/Engine/AbstractMapper.php index a181e4ed971..0d9f6eff142 100644 --- a/src/Oro/Bundle/SearchBundle/Engine/AbstractMapper.php +++ b/src/Oro/Bundle/SearchBundle/Engine/AbstractMapper.php @@ -3,7 +3,7 @@ namespace Oro\Bundle\SearchBundle\Engine; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Oro\Bundle\SearchBundle\Provider\SearchMappingProvider; use Oro\Bundle\SearchBundle\Query\Query; @@ -26,6 +26,11 @@ abstract class AbstractMapper */ protected $mappingProvider; + /** + * @var PropertyAccessorInterface + */ + protected $propertyAccessor; + /** * @param SearchMappingProvider $mappingProvider */ @@ -34,6 +39,14 @@ public function setMappingProvider(SearchMappingProvider $mappingProvider) $this->mappingProvider = $mappingProvider; } + /** + * @param PropertyAccessorInterface $propertyAccessor + */ + public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor) + { + $this->propertyAccessor = $propertyAccessor; + } + /** * Get object field value * @@ -44,10 +57,15 @@ public function setMappingProvider(SearchMappingProvider $mappingProvider) */ public function getFieldValue($objectOrArray, $fieldName) { - $propertyAccessor = PropertyAccess::createPropertyAccessor(); + if (is_object($objectOrArray)) { + $getter = sprintf('get%s', $fieldName); + if (method_exists($objectOrArray, $getter)) { + return $objectOrArray->$getter(); + } + } try { - return $propertyAccessor->getValue($objectOrArray, $fieldName); + return $this->propertyAccessor->getValue($objectOrArray, $fieldName); } catch (\Exception $e) { return null; } diff --git a/src/Oro/Bundle/SearchBundle/Engine/Orm.php b/src/Oro/Bundle/SearchBundle/Engine/Orm.php index 9ed01b3cccc..a998f0a4bd3 100644 --- a/src/Oro/Bundle/SearchBundle/Engine/Orm.php +++ b/src/Oro/Bundle/SearchBundle/Engine/Orm.php @@ -2,18 +2,30 @@ namespace Oro\Bundle\SearchBundle\Engine; -use JMS\JobQueueBundle\Entity\Job; +use Doctrine\Common\Persistence\ManagerRegistry; -use Oro\Bundle\EntityBundle\ORM\OroEntityManager; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Oro\Bundle\EntityBundle\ORM\DoctrineHelper; +use Oro\Bundle\EntityBundle\ORM\OroEntityManager; +use Oro\Bundle\SearchBundle\Engine\Orm\DbalStorer; use Oro\Bundle\SearchBundle\Entity\Item; use Oro\Bundle\SearchBundle\Entity\Repository\SearchIndexRepository; use Oro\Bundle\SearchBundle\Query\Mode; use Oro\Bundle\SearchBundle\Query\Query; use Oro\Bundle\SearchBundle\Query\Result\Item as ResultItem; - -class Orm extends AbstractEngine +use Oro\Bundle\SearchBundle\Resolver\EntityTitleResolverInterface; +use Oro\Component\Log\NullProgressLogger; +use Oro\Component\Log\ProgressLoggerAwareInterface; +use Oro\Component\Log\ProgressLoggerAwareTrait; + +/** + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + */ +class Orm extends AbstractEngine implements ProgressLoggerAwareInterface { + use ProgressLoggerAwareTrait; + /** @var SearchIndexRepository */ private $indexRepository; @@ -29,6 +41,23 @@ class Orm extends AbstractEngine /** @var bool */ protected $needFlush = true; + /** @var DbalStorer */ + protected $dbalStorer; + + /** + * {@inheritdoc} + */ + public function __construct( + ManagerRegistry $registry, + EventDispatcherInterface $eventDispatcher, + DoctrineHelper $doctrineHelper, + ObjectMapper $mapper, + EntityTitleResolverInterface $entityTitleResolver + ) { + parent::__construct($registry, $eventDispatcher, $doctrineHelper, $mapper, $entityTitleResolver); + $this->progressLogger = new NullProgressLogger(); + } + /** * @param array $drivers */ @@ -37,6 +66,14 @@ public function setDrivers(array $drivers) $this->drivers = $drivers; } + /** + * @param DbalStorer $dbalStorer + */ + public function setDbalStorer(DbalStorer $dbalStorer) + { + $this->dbalStorer = $dbalStorer; + } + /** * {@inheritdoc} */ @@ -61,13 +98,20 @@ public function reindex($class = null, $offset = null, $limit = null) } } + $totalRecords = 0; + foreach ($entityNames as $entityName) { + $totalRecords += $this->getNumberOfRecordsToReindex($entityName, $offset, $limit); + } + // index data by mapping config $recordsCount = 0; + $this->progressLogger->logSteps($totalRecords); while ($class = array_shift($entityNames)) { $itemsCount = $this->reindexSingleEntity($class, $offset, $limit); $recordsCount += $itemsCount; } + $this->progressLogger->logFinish(); return $recordsCount; } @@ -123,12 +167,23 @@ public function save($entity, $realTime = true) $hasSavedEntities = $this->saveItemData($entities); if ($hasSavedEntities && $this->needFlush) { - $this->flush(); + $this->getIndexManager()->getConnection()->transactional(function () { + $this->dbalStorer->store(); + $this->getIndexManager()->clear(); + }); } return $hasSavedEntities; } + /** + * {@inheritdoc} + */ + protected function recordProcessed() + { + $this->progressLogger->logAdvance(1); + } + /** * @param array $entities * @return bool @@ -166,7 +221,7 @@ protected function saveItemData(array $entities) ->setChanged(false) ->saveItemData($data); - $this->getIndexManager()->persist($item); + $this->dbalStorer->addItem($item); $hasSavedEntities = true; } diff --git a/src/Oro/Bundle/SearchBundle/Engine/Orm/DbalStorer.php b/src/Oro/Bundle/SearchBundle/Engine/Orm/DbalStorer.php new file mode 100644 index 00000000000..4e5191c3b0a --- /dev/null +++ b/src/Oro/Bundle/SearchBundle/Engine/Orm/DbalStorer.php @@ -0,0 +1,392 @@ + [ + * 'id' => id of the item, + 'entity' => class of the entity, + 'alias' => alias of the entity, + 'record_id' => id of the entity, + 'title' => title of the entity, + 'changed' => changed attribute, + 'created_at' => when the Item was created, + 'updated_at' => when the Item was updated, + * ], + * ... + * ] + */ + protected $itemData = []; + + /** + * Doctrine types for columns in $itemData + * + * @var array + * [ + * 'insert' => [ + * 'integer', + * ... + * ], + * 'update' => [ + * 'string', + * ... + * ], + * ] + */ + protected $itemTypes = []; + + /** + * We need this to prevent reuse of object hashs + * + * @var Item[] + */ + protected $items = []; + + /** + * @var array + * [ + * 'table_name' => [ + * 'columns' => array of Index column names to insert + * 'types' => array of doctrine types of columns to insert for all values + * 'data' => [ + * [ + * 'itemRef' => string reference (object hash) to item so we can retrieve Item::id after insert + * 'values' => values for 'columns' for one record to insert + * ], + * ... + * ], + * ], + * ... + * ] + */ + protected $indexInsertData = []; + + /** + * @var array + * [ + * 'table_name' => [ + * 'types' => array of doctrine types of columns to insert for one record + * 'data' => [ + * [ + * 'itemRef' => string reference (object hash) to item so we can retrieve Item::id after insert + * 'values' => values for 'columns' for one record to update + * ], + * ... + * ], + * ], + * ... + * ] + */ + protected $indexUpdateData = []; + + /** + * Map of types of 'value' column for table + * + * @var array + */ + protected $indexValueTypes = [ + 'oro_search_index_integer' => Type::INTEGER, + 'oro_search_index_text' => Type::TEXT, + 'oro_search_index_decimal' => Type::DECIMAL, + 'oro_search_index_datetime' => Type::DATETIME, + ]; + + /** + * @param DoctrineHelper $doctrineHelper + */ + public function __construct(DoctrineHelper $doctrineHelper) + { + $this->doctrineHelper = $doctrineHelper; + } + + /** + * Stores all data taken from Items given by 'addItem' method + */ + public function store() + { + $connection = $this->getConnection(); + + $this->processItems($connection); + $multiInsertQueryData = []; + $this->fillQueryData($connection, $multiInsertQueryData); + + $this->runMultiInserts($connection, $multiInsertQueryData); + $this->runUpdates($connection, $this->indexUpdateData); + + $this->items = []; + $this->itemData = []; + $this->indexInsertData = []; + $this->indexUpdateData = []; + } + + /** + * Adds Item of which data will be stored when 'store' method is called + * + * @param Item $item + */ + public function addItem(Item $item) + { + $this->populateItem($item); + $this->populateIndex($item->getIntegerFields(), $item, 'oro_search_index_integer'); + $this->populateIndex($item->getTextFields(), $item, 'oro_search_index_text'); + $this->populateIndex($item->getDecimalFields(), $item, 'oro_search_index_decimal'); + $this->populateIndex($item->getDatetimeFields(), $item, 'oro_search_index_datetime'); + } + + /** + * Prepares index data for queries to be stored + * + * @param Connection $connection + * @param array $multiInsertQueryData + */ + protected function fillQueryData(Connection $connection, array &$multiInsertQueryData) + { + foreach ($this->indexInsertData as $table => $data) { + $insertValues = []; + foreach ($data['data'] as $record) { + $record['values']['item_id'] = $this->itemData[$record['itemRef']]['id']; + foreach ($record['values'] as $value) { + array_push($insertValues, $value); + } + } + + if ($insertValues) { + $multiInsertQueryData[$table] = [ + 'query' => sprintf( + 'INSERT INTO %s (%s) VALUES %s', + $connection->quoteIdentifier($table), + implode(', ', array_map([$connection, 'quoteIdentifier'], $data['columns'])), + implode( + ', ', + array_fill( + 0, + count($data['data']), + sprintf('(%s)', implode(', ', array_fill(0, count($data['columns']), '?'))) + ) + ) + ), + 'values' => $insertValues, + 'types' => $data['types'], + ]; + } + } + + foreach ($this->indexUpdateData as $table => $data) { + foreach ($data['data'] as $record) { + $record['values']['item_id'] = $this->itemData[$record['itemRef']]['id']; + } + } + } + + /** + * Runs multi inserts taken from $multiInsertQueryData argument + * + * @param Connection $connection + * @param array $multiInsertQueryData + */ + protected function runMultiInserts(Connection $connection, array $multiInsertQueryData) + { + foreach ($multiInsertQueryData as $data) { + $connection->executeQuery($data['query'], $data['values'], $data['types']); + } + } + + /** + * Runs updates taken from $updateQueryData argument + * + * @param Connection $connection + * @param array $updateQueryData + */ + protected function runUpdates(Connection $connection, array $updateQueryData) + { + foreach ($updateQueryData as $table => $data) { + foreach ($data['data'] as $record) { + $connection->update( + $connection->quoteIdentifier($table), + $record['values'], + ['id' => $record['values']['id']], + $data['types'] + ); + } + } + } + + /** + * Stores items from $this->itemData and updates their ids + * + * @param Connection $connection + */ + protected function processItems(Connection $connection) + { + $now = Carbon::now(); + $table = $connection->quoteIdentifier('oro_search_item'); + foreach ($this->itemData as &$data) { + $data['updated_at'] = $now; + if (isset($data['id'])) { + $connection->update( + $table, + $data, + ['id' => $data['id']], + $this->itemTypes['update'] + ); + } else { + $data['created_at'] = $now; + $connection->insert( + $table, + $data, + $this->itemTypes['insert'] + ); + $data['id'] = $connection->lastInsertId( + $connection->getDatabasePlatform() instanceof PostgreSqlPlatform ? 'oro_search_item_id_seq' : null + ); + } + } + } + + /** + * Converts $item into array and stores the result in the DbalStorer object + * + * @param Item $item + */ + protected function populateItem(Item $item) + { + $this->items[spl_object_hash($item)] = $item; + + if (!$this->itemTypes) { + $this->itemTypes = [ + 'insert' => [ + Type::STRING, + Type::STRING, + Type::INTEGER, + Type::STRING, + Type::BOOLEAN, + Type::DATETIME, + Type::DATETIME, + ], + 'update' => [ + Type::INTEGER, + Type::STRING, + Type::STRING, + Type::INTEGER, + Type::STRING, + Type::BOOLEAN, + Type::DATETIME, + Type::DATETIME, + ], + ]; + } + + if ($item->getId()) { + $this->itemData[spl_object_hash($item)] = [ + 'id' => $item->getId(), + 'entity' => $item->getEntity(), + 'alias' => $item->getAlias(), + 'record_id' => $item->getRecordId(), + 'title' => $item->getTitle(), + 'changed' => $item->getChanged(), + 'created_at' => $item->getCreatedAt(), + 'updated_at' => $item->getUpdatedAt(), + ]; + } else { + $this->itemData[spl_object_hash($item)] = [ + 'entity' => $item->getEntity(), + 'alias' => $item->getAlias(), + 'record_id' => $item->getRecordId(), + 'title' => $item->getTitle(), + 'changed' => $item->getChanged(), + 'created_at' => $item->getCreatedAt(), + 'updated_at' => $item->getUpdatedAt(), + ]; + } + } + + /** + * Converts indexes of $item into objects and stores them in the DbalStorer object + * + * @param Collection $fields + * @param Item $item + * @param string $table + */ + protected function populateIndex(Collection $fields, Item $item, $table) + { + if ($fields->isEmpty()) { + return; + } + + if (!isset($this->indexUpdateData[$table])) { + $this->indexUpdateData[$table] = [ + 'data' => [], + 'types' => [ + Type::INTEGER, + Type::STRING, + $this->indexValueTypes[$table], + Type::INTEGER, + ], + ]; + $this->indexInsertData[$table] = [ + 'columns' => ['field', 'value', 'item_id'], + 'data' => [], + 'types' => [], + ]; + } + + foreach ($fields as $field) { + if ($field->getId()) { + $this->indexUpdateData[$table]['data'][] = [ + 'itemRef' => spl_object_hash($item), + 'values' => [ + 'id' => $field->getId(), + 'field' => $field->getField(), + 'value' => $field->getValue(), + 'item_id' => $item->getId(), + ], + ]; + } else { + $this->indexInsertData[$table]['data'][] = [ + 'itemRef' => spl_object_hash($item), + 'values' => [ + 'field' => $field->getField(), + 'value' => $field->getValue(), + 'item_id' => $item->getId(), + ], + ]; + + array_push( + $this->indexInsertData[$table]['types'], + Type::STRING, + $this->indexValueTypes[$table], + Type::INTEGER + ); + } + } + $fields->clear(); + } + + /** + * @return Connection + */ + protected function getConnection() + { + return $this->doctrineHelper->getEntityManager('OroSearchBundle:Item') + ->getConnection(); + } +} diff --git a/src/Oro/Bundle/SearchBundle/Resources/config/oro/search_engine/orm.yml b/src/Oro/Bundle/SearchBundle/Resources/config/oro/search_engine/orm.yml index ba34ab76f41..af1158d6c14 100644 --- a/src/Oro/Bundle/SearchBundle/Resources/config/oro/search_engine/orm.yml +++ b/src/Oro/Bundle/SearchBundle/Resources/config/oro/search_engine/orm.yml @@ -2,6 +2,12 @@ parameters: oro_search.engine.class: Oro\Bundle\SearchBundle\Engine\Orm services: + oro_search.search.engine.storer: + class: Oro\Bundle\SearchBundle\Engine\Orm\DbalStorer + arguments: + - '@oro_entity.doctrine_helper' + public: false + oro_search.search.engine: class: %oro_search.engine.class% arguments: @@ -13,3 +19,4 @@ services: calls: - [setLogQueries, [%oro_search.log_queries%]] - [setDrivers, [%oro_search.drivers%]] + - [setDbalStorer, ['@oro_search.search.engine.storer']] diff --git a/src/Oro/Bundle/SearchBundle/Resources/config/services.yml b/src/Oro/Bundle/SearchBundle/Resources/config/services.yml index b382add3dee..43370cc6a49 100644 --- a/src/Oro/Bundle/SearchBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/SearchBundle/Resources/config/services.yml @@ -77,6 +77,7 @@ services: arguments: ['@event_dispatcher', %oro_search.entities_config%] calls: - [setMappingProvider, ['@oro_search.provider.search_mapping']] + - [setPropertyAccessor, ['@property_accessor']] oro_search.provider.result_statistics_provider: class: %oro_search.provider.result_statistics_provider.class% diff --git a/src/Oro/Bundle/SearchBundle/Tests/Unit/Engine/Orm/DbalStorerTest.php b/src/Oro/Bundle/SearchBundle/Tests/Unit/Engine/Orm/DbalStorerTest.php new file mode 100644 index 00000000000..72bab1582dd --- /dev/null +++ b/src/Oro/Bundle/SearchBundle/Tests/Unit/Engine/Orm/DbalStorerTest.php @@ -0,0 +1,270 @@ +connection = $this->getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + $this->connection->expects($this->any()) + ->method('quoteIdentifier') + ->will($this->returnCallback(function ($identifier) { + return $identifier; + })); + + $em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + $em->expects($this->any()) + ->method('getConnection') + ->will($this->returnValue($this->connection)); + + $doctrineHelper = $this->getMockBuilder('Oro\Bundle\EntityBundle\ORM\DoctrineHelper') + ->disableOriginalConstructor() + ->getMock(); + $doctrineHelper->expects($this->any()) + ->method('getEntityManager') + ->with('OroSearchBundle:Item') + ->will($this->returnValue($em)); + + $this->dbalStorer = new DbalStorer($doctrineHelper); + } + + public function tearDown() + { + Carbon::setTestNow(); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testStore() + { + $item1 = (new Item()) + ->setAlias('alias1') + ->setChanged(true) + ->setEntity('StdClass') + ->setRecordId(1) + ->addIntegerField( + (new IndexInteger()) + ->setField('intField') + ->setValue(3) + ) + ->addIntegerField( + (new IndexInteger()) + ->setField('intField2') + ->setValue(5) + ) + ->setTitle('item1'); + + $item2 = (new Item()) + ->setAlias('alias2') + ->setChanged(true) + ->setEntity('StdClass') + ->setRecordId(1) + ->setTitle('item2') + ->addDatetimeField( + (new IndexDatetime()) + ->setField('dateTimeField') + ->setValue(new \DateTime('2016-01-01 13:55:16')) + ) + ->addTextField( + (new IndexText()) + ->setField('text') + ->setValue('val') + ); + + $item3 = (new Item()) + ->setId(3) + ->setAlias('alias3') + ->setChanged(true) + ->setEntity('StdClass') + ->setRecordId(1) + ->setTitle('item3') + ->setCreatedAt(new \DateTime('2016-03-01 13:52:12')) + ->addDecimalField( + (new IndexDecimal()) + ->setField('decimalField') + ->setValue(0.3) + ); + + $this->dbalStorer->addItem($item1); + $this->dbalStorer->addItem($item2); + $this->dbalStorer->addItem($item3); + + $now = Carbon::now(); + Carbon::setTestNow($now); + + $this->connection->expects($this->exactly(2)) + ->method('insert') + ->withConsecutive( + [ + 'oro_search_item', + $this->itemInsertData($item1), + $this->itemInsertTypes(), + ], + [ + 'oro_search_item', + $this->itemInsertData($item2), + $this->itemInsertTypes(), + ] + ); + + $this->connection->expects($this->once()) + ->method('update') + ->with( + 'oro_search_item', + $this->itemUpdateData($item3), + ['id' => $item3->getId()], + $this->itemUpdateTypes() + ); + + $this->connection->expects($this->exactly(2)) + ->method('lastInsertId') + ->will($this->onConsecutiveCalls(1, 2)); + + $this->connection->expects($this->exactly(4)) + ->method('executeQuery') + ->withConsecutive( + [ + 'INSERT INTO oro_search_index_integer (field, value, item_id) VALUES (?, ?, ?), (?, ?, ?)', + ['intField', 3, 1, 'intField2', 5, 1], + $this->indexInsertTypes(Type::INTEGER, 2), + ], + [ + 'INSERT INTO oro_search_index_text (field, value, item_id) VALUES (?, ?, ?)', + ['text', 'val', 2], + $this->indexInsertTypes(Type::TEXT), + ], + [ + 'INSERT INTO oro_search_index_datetime (field, value, item_id) VALUES (?, ?, ?)', + ['dateTimeField', new \DateTime('2016-01-01 13:55:16'), 2], + $this->indexInsertTypes(Type::DATETIME), + ], + [ + 'INSERT INTO oro_search_index_decimal (field, value, item_id) VALUES (?, ?, ?)', + ['decimalField', 0.3, 3], + $this->indexInsertTypes(Type::DECIMAL), + ] + ); + + $this->connection->expects($this->exactly(1)) + ->method('update') + ->with('oro_search_item', $this->itemUpdateData($item3), ['id' => 3], $this->itemUpdateTypes()); + + $this->dbalStorer->store(); + } + + /** + * @param Item $item + * + * @return array + */ + protected function itemInsertData(Item $item) + { + return [ + 'entity' => $item->getEntity(), + 'alias' => $item->getAlias(), + 'record_id' => $item->getRecordId(), + 'title' => $item->getTitle(), + 'changed' => $item->getChanged(), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ]; + } + + /** + * @return array + */ + protected function itemInsertTypes() + { + return [ + Type::STRING, + Type::STRING, + Type::INTEGER, + Type::STRING, + Type::BOOLEAN, + Type::DATETIME, + Type::DATETIME, + ]; + } + + /** + * @param Item $item + * + * @return array + */ + protected function itemUpdateData(Item $item) + { + return [ + 'id' => $item->getId(), + 'entity' => $item->getEntity(), + 'alias' => $item->getAlias(), + 'record_id' => $item->getRecordId(), + 'title' => $item->getTitle(), + 'changed' => $item->getChanged(), + 'created_at' => $item->getCreatedAt(), + 'updated_at' => Carbon::now(), + ]; + } + + /** + * @return array + */ + protected function itemUpdateTypes() + { + return [ + Type::INTEGER, + Type::STRING, + Type::STRING, + Type::INTEGER, + Type::STRING, + Type::BOOLEAN, + Type::DATETIME, + Type::DATETIME, + ]; + } + + /** + * @param string $valueType + * @param int $n + * + * @return array + */ + protected function indexInsertTypes($valueType, $n = 1) + { + return call_user_func_array( + 'array_merge', + array_fill( + 0, + $n, + [ + Type::STRING, + $valueType, + Type::INTEGER, + ] + ) + ); + } +} diff --git a/src/Oro/Bundle/SearchBundle/Tests/Unit/Engine/Orm/ObjectMapperTest.php b/src/Oro/Bundle/SearchBundle/Tests/Unit/Engine/Orm/ObjectMapperTest.php index 1bf983fa78e..c3b9fe40a6e 100644 --- a/src/Oro/Bundle/SearchBundle/Tests/Unit/Engine/Orm/ObjectMapperTest.php +++ b/src/Oro/Bundle/SearchBundle/Tests/Unit/Engine/Orm/ObjectMapperTest.php @@ -1,6 +1,8 @@ mapper = new ObjectMapper($this->dispatcher, $this->mappingConfig); $this->mapper->setMappingProvider($mapperProvider); + $this->mapper->setPropertyAccessor(PropertyAccess::createPropertyAccessor()); } /** diff --git a/src/Oro/Bundle/SearchBundle/Tests/Unit/Engine/OrmTest.php b/src/Oro/Bundle/SearchBundle/Tests/Unit/Engine/OrmTest.php index 3a1567b2433..1f3a95f93e3 100644 --- a/src/Oro/Bundle/SearchBundle/Tests/Unit/Engine/OrmTest.php +++ b/src/Oro/Bundle/SearchBundle/Tests/Unit/Engine/OrmTest.php @@ -150,7 +150,12 @@ protected function getEngineMock() return $this->getMockBuilder('Oro\Bundle\SearchBundle\Engine\Orm') ->setConstructorArgs($arguments) - ->setMethods(['clearAllSearchIndexes', 'clearSearchIndexForEntity', 'reindexSingleEntity']) + ->setMethods([ + 'clearAllSearchIndexes', + 'clearSearchIndexForEntity', + 'reindexSingleEntity', + 'getNumberOfRecordsToReindex' + ]) ->getMock(); } } diff --git a/src/Oro/Bundle/SearchBundle/Tests/Unit/Fixture/Entity/Item.php b/src/Oro/Bundle/SearchBundle/Tests/Unit/Fixture/Entity/Item.php new file mode 100644 index 00000000000..b1c118a2066 --- /dev/null +++ b/src/Oro/Bundle/SearchBundle/Tests/Unit/Fixture/Entity/Item.php @@ -0,0 +1,20 @@ +id = $id; + + return $this; + } +} diff --git a/src/Oro/Bundle/TranslationBundle/Resources/config/services.yml b/src/Oro/Bundle/TranslationBundle/Resources/config/services.yml index 9079e6cbfe9..9ae199e22d8 100644 --- a/src/Oro/Bundle/TranslationBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/TranslationBundle/Resources/config/services.yml @@ -99,7 +99,8 @@ services: oro_translation.database_translation.persister: class: %oro_translation.database_translation.persister.class% arguments: - - '@doctrine.orm.entity_manager' + - '@doctrine' + - '@oro_entity.orm.native_query_executor_helper' - '@oro_translation.database_translation.metadata.cache' oro_translation.database_translation.loader: diff --git a/src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/DatabasePersisterTest.php b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/DatabasePersisterTest.php index 5c5f51393aa..53431807283 100644 --- a/src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/DatabasePersisterTest.php +++ b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/DatabasePersisterTest.php @@ -2,22 +2,13 @@ namespace Oro\Bundle\TranslationBundle\Tests\Unit\Translation; -use Oro\Bundle\TranslationBundle\Entity\Translation; +use Oro\Bundle\EntityBundle\ORM\NativeQueryExecutorHelper; use Oro\Bundle\TranslationBundle\Translation\DatabasePersister; class DatabasePersisterTest extends \PHPUnit_Framework_TestCase { - /** @var DatabasePersister */ - protected $persister; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $em; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $repo; - - /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $metadataCache; + /** @var NativeQueryExecutorHelper */ + private $nativeQueryExecutorHelper; /** @var array */ protected $testData = [ @@ -35,80 +26,92 @@ class DatabasePersisterTest extends \PHPUnit_Framework_TestCase /** @var string */ protected $testLocale = 'en'; + /** + * {@inheritdoc} + */ protected function setUp() { - $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager') - ->disableOriginalConstructor()->getMock(); - $this->repo = $this->getMockBuilder( - 'Oro\Bundle\TranslationBundle\Entity\Repository\TranslationRepository' - ) - ->disableOriginalConstructor()->getMock(); - $this->metadataCache = $this - ->getMockBuilder('Oro\Bundle\TranslationBundle\Translation\DynamicTranslationMetadataCache') - ->disableOriginalConstructor()->getMock(); - - - $this->em->expects($this->any())->method('getRepository')->with($this->equalTo(Translation::ENTITY_NAME)) - ->will($this->returnValue($this->repo)); - $this->persister = new DatabasePersister($this->em, $this->metadataCache); - - // set batch size to 2 - $reflection = new \ReflectionProperty(get_class($this->persister), 'batchSize'); - $reflection->setAccessible(true); - $reflection->setValue($this->persister, 2); + $this->nativeQueryExecutorHelper = + $this->getMockBuilder('Oro\Bundle\EntityBundle\ORM\NativeQueryExecutorHelper') + ->disableOriginalConstructor() + ->getMock(); + + $this->nativeQueryExecutorHelper->expects($this->once()) + ->method('getTableName') + ->willReturn('oro_test_table'); } protected function tearDown() { - unset($this->em, $this->persister, $this->repo); + unset($this->nativeQueryExecutorHelper); } - public function testPersist() + public function testAllNewTranslationsInserted() { - $this->em->expects($this->once())->method('beginTransaction'); - $this->em->expects($this->exactly(5))->method('persist'); - $this->em->expects($this->exactly(3))->method('flush'); - $this->em->expects($this->exactly(3))->method('clear'); - $this->em->expects($this->once())->method('commit'); - $this->em->expects($this->never())->method('rollback'); - - $this->metadataCache->expects($this->once())->method('updateTimestamp')->with($this->testLocale); + $connection = $this->getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + $connection->expects($this->once())->method('beginTransaction'); + $connection->expects($this->exactly(2))->method('fetchAll')->willReturn([]); + $connection->expects($this->exactly(5))->method('insert'); + $connection->expects($this->never())->method('update'); + $connection->expects($this->once())->method('commit'); + $connection->expects($this->never())->method('rollback'); + + $doctrine = $this->getMockBuilder('Doctrine\Bundle\DoctrineBundle\Registry') + ->disableOriginalConstructor() + ->getMock(); + $doctrine->expects($this->once()) + ->method('getConnection') + ->willReturn($connection); + + $metadataCache = $this + ->getMockBuilder('Oro\Bundle\TranslationBundle\Translation\DynamicTranslationMetadataCache') + ->disableOriginalConstructor() + ->getMock(); + $metadataCache->expects($this->once())->method('updateTimestamp')->with($this->testLocale); - $this->persister->persist($this->testLocale, $this->testData); + $persister = new DatabasePersister($doctrine, $this->nativeQueryExecutorHelper, $metadataCache); + $persister->persist($this->testLocale, $this->testData); } - public function testPersistUpdateScenario() + public function testInsertAndUpdateScenario() { - $testValue = 'some Value'; - $existsTranslation = new Translation(); - $existsTranslation->setValue($testValue); - - $this->repo->expects($this->any())->method('findValue') - ->will( - $this->returnValueMap( - [ - ['key_1', $this->testLocale, 'messages', Translation::SCOPE_SYSTEM, null], - ['key_2', $this->testLocale, 'messages', Translation::SCOPE_SYSTEM, null], - ['key_3', $this->testLocale, 'messages', Translation::SCOPE_SYSTEM, null], - ['key_1', $this->testLocale, 'validators', Translation::SCOPE_SYSTEM, $existsTranslation], - ['key_2', $this->testLocale, 'validators', Translation::SCOPE_SYSTEM, null], - ] - ) - ); - - $this->em->expects($this->once())->method('beginTransaction'); - $this->em->expects($this->exactly(5))->method('persist'); - $this->em->expects($this->exactly(3))->method('flush'); - $this->em->expects($this->exactly(3))->method('clear'); - $this->em->expects($this->once())->method('commit'); - $this->em->expects($this->never())->method('rollback'); - - $this->metadataCache->expects($this->once())->method('updateTimestamp')->with($this->testLocale); - - $this->persister->persist($this->testLocale, $this->testData); - - $this->assertSame($this->testData['validators']['key_1'], $existsTranslation->getValue()); - $this->assertNotSame($testValue, $existsTranslation->getValue()); + $connection = $this->getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + $connection->expects($this->once())->method('beginTransaction'); + $connection->expects($this->exactly(2))->method('fetchAll')->willReturnOnConsecutiveCalls( + [ + ['id' => 1, 'key' => 'key_1', 'value' => 'value_1'], //existing translation, to be skipped + ['id' => 2, 'key' => 'key_2', 'value' => 'value_02'], //existing with different value, to be updated + ], + [ + ['id' => 4, 'key' => 'key_1', 'value' => 'value_1'], //existing translation, to be skipped + ['id' => 5, 'key' => 'key_2', 'value' => 'value_02'], //existing with different value, to be updated + ] + ); + $connection->expects($this->exactly(1))->method('insert'); + $connection->expects($this->exactly(2))->method('update'); + + $connection->expects($this->once())->method('commit'); + $connection->expects($this->never())->method('rollback'); + + $doctrine = $this->getMockBuilder('Doctrine\Bundle\DoctrineBundle\Registry') + ->disableOriginalConstructor() + ->getMock(); + $doctrine->expects($this->once()) + ->method('getConnection') + ->willReturn($connection); + + $metadataCache = $this + ->getMockBuilder('Oro\Bundle\TranslationBundle\Translation\DynamicTranslationMetadataCache') + ->disableOriginalConstructor() + ->getMock(); + $metadataCache->expects($this->once())->method('updateTimestamp')->with($this->testLocale); + + $persister = new DatabasePersister($doctrine, $this->nativeQueryExecutorHelper, $metadataCache); + $persister->persist($this->testLocale, $this->testData); } public function testExceptionScenario() @@ -117,15 +120,30 @@ public function testExceptionScenario() $this->setExpectedException($exceptionClass); $exception = new $exceptionClass(); - $this->em->expects($this->once())->method('beginTransaction'); - $this->em->expects($this->exactly(5))->method('persist'); - $this->em->expects($this->exactly(3))->method('flush'); - $this->em->expects($this->exactly(3))->method('clear'); - $this->em->expects($this->once())->method('commit')->will($this->throwException($exception)); - $this->em->expects($this->once())->method('rollback'); - - $this->metadataCache->expects($this->never())->method('updateTimestamp'); + $connection = $this->getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + $connection->expects($this->once())->method('beginTransaction'); + $connection->expects($this->any())->method('fetchAll')->willReturn([]); + $connection->expects($this->exactly(5))->method('insert'); + $connection->expects($this->never())->method('update'); + $connection->expects($this->once())->method('commit')->will($this->throwException($exception)); + $connection->expects($this->once())->method('rollback'); + + $doctrine = $this->getMockBuilder('Doctrine\Bundle\DoctrineBundle\Registry') + ->disableOriginalConstructor() + ->getMock(); + $doctrine->expects($this->once()) + ->method('getConnection') + ->willReturn($connection); + + $metadataCache = $this + ->getMockBuilder('Oro\Bundle\TranslationBundle\Translation\DynamicTranslationMetadataCache') + ->disableOriginalConstructor() + ->getMock(); + $metadataCache->expects($this->never())->method('updateTimestamp'); - $this->persister->persist($this->testLocale, $this->testData); + $persister = new DatabasePersister($doctrine, $this->nativeQueryExecutorHelper, $metadataCache); + $persister->persist($this->testLocale, $this->testData); } } diff --git a/src/Oro/Bundle/TranslationBundle/Translation/DatabasePersister.php b/src/Oro/Bundle/TranslationBundle/Translation/DatabasePersister.php index 11f2be9bfbb..05537b4e348 100644 --- a/src/Oro/Bundle/TranslationBundle/Translation/DatabasePersister.php +++ b/src/Oro/Bundle/TranslationBundle/Translation/DatabasePersister.php @@ -2,38 +2,38 @@ namespace Oro\Bundle\TranslationBundle\Translation; +use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\DBAL\Connection; use Doctrine\DBAL\Platforms\MySqlPlatform; -use Doctrine\ORM\EntityManager; +use Doctrine\DBAL\Types\Type; +use Oro\Bundle\EntityBundle\ORM\NativeQueryExecutorHelper; use Oro\Bundle\TranslationBundle\Entity\Translation; -use Oro\Bundle\TranslationBundle\Entity\Repository\TranslationRepository; class DatabasePersister { - /** @var int */ - private $batchSize = 200; - - /** @var EntityManager */ - private $em; - /** @var DynamicTranslationMetadataCache */ private $metadataCache; - /** @var TranslationRepository */ - private $repository; + /** @var ManagerRegistry */ + private $doctrine; - /** @var array */ - private $toWrite = []; + /** @var NativeQueryExecutorHelper */ + private $nativeQueryExecutorHelper; /** - * @param EntityManager $em + * @param ManagerRegistry $doctrine + * @param NativeQueryExecutorHelper $nativeQueryExecutorHelper * @param DynamicTranslationMetadataCache $metadataCache */ - public function __construct(EntityManager $em, DynamicTranslationMetadataCache $metadataCache) - { - $this->em = $em; + public function __construct( + ManagerRegistry $doctrine, + NativeQueryExecutorHelper $nativeQueryExecutorHelper, + DynamicTranslationMetadataCache $metadataCache + ) { + $this->doctrine = $doctrine; + $this->nativeQueryExecutorHelper = $nativeQueryExecutorHelper; $this->metadataCache = $metadataCache; - $this->repository = $em->getRepository(Translation::ENTITY_NAME); } /** @@ -46,32 +46,79 @@ public function __construct(EntityManager $em, DynamicTranslationMetadataCache $ */ public function persist($locale, array $data) { + /** @var Connection $connection */ + $connection = $this->doctrine->getConnection(); $writeCount = 0; + try { - $this->em->beginTransaction(); + $connection->beginTransaction(); + $translationsTableName = $this->nativeQueryExecutorHelper->getTableName(Translation::ENTITY_NAME); + foreach ($data as $domain => $domainData) { + $fetchStatement = 'SELECT id, `key`, `value` FROM ' . $translationsTableName . + ' WHERE locale = :locale' . + ' AND domain = :domain' . + ' AND scope = :scope'; + $existings = $connection->fetchAll( + $fetchStatement, + [ + 'locale' => $locale, + 'domain' => $domain, + 'scope' => Translation::SCOPE_SYSTEM + ], + [ + Type::STRING, + Type::STRING, + Type::STRING + ] + ); + + $existingTranslationKeys = array_column($existings, 'id', 'key'); + $existingTranslationValues = array_column($existings, 'value', 'key'); + foreach ($domainData as $key => $translation) { if (strlen($key) > MySqlPlatform::LENGTH_LIMIT_TINYTEXT) { continue; } - $writeCount++; - $this->toWrite[] = $this->getTranslationObject($key, $locale, $domain, $translation); - if (0 === $writeCount % $this->batchSize) { - $this->write($this->toWrite); - - $this->toWrite = []; + $existingTranslationKey = array_key_exists($key, $existingTranslationKeys); + if (!$existingTranslationKey) { + $connection->insert( + $translationsTableName, + [ + $connection->quoteIdentifier('key') => $key, + $connection->quoteIdentifier('value') => $translation, + 'locale' => $locale, + 'domain' => $domain, + 'scope' => Translation::SCOPE_SYSTEM + ], + [ + Type::STRING, + Type::STRING, + Type::STRING, + Type::STRING, + Type::SMALLINT, + ] + ); + } elseif ($existingTranslationKey && $existingTranslationValues[$key] !== $translation) { + $connection->update( + $translationsTableName, + [$connection->quoteIdentifier('value') => $translation], + ['id' => $existingTranslationKeys[$key]] + ); + } else { + continue; } + + $writeCount++; } } - if (count($this->toWrite) > 0) { - $this->write($this->toWrite); + if ($writeCount) { + $connection->commit(); } - - $this->em->commit(); } catch (\Exception $exception) { - $this->em->rollback(); + $connection->rollBack(); throw $exception; } @@ -79,44 +126,4 @@ public function persist($locale, array $data) // update timestamp in case when persist succeed $this->metadataCache->updateTimestamp($locale); } - - /** - * Do persist into EntityManager - * - * @param array $items - */ - private function write(array $items) - { - foreach ($items as $item) { - $this->em->persist($item); - } - $this->em->flush(); - $this->em->clear(); - } - - /** - * Find existing translation in database - * - * @param string $key - * @param string $locale - * @param string $domain - * @param string $value - * - * @return Translation - */ - private function getTranslationObject($key, $locale, $domain, $value) - { - $object = $this->repository->findValue($key, $locale, $domain); - if (null === $object) { - $object = new Translation(); - $object->setScope(Translation::SCOPE_SYSTEM); - $object->setLocale($locale); - $object->setDomain($domain); - $object->setKey($key); - } - - $object->setValue($value); - - return $object; - } } diff --git a/src/Oro/Component/Log/ConsoleProgressLogger.php b/src/Oro/Component/Log/ConsoleProgressLogger.php new file mode 100644 index 00000000000..eb6593d2db2 --- /dev/null +++ b/src/Oro/Component/Log/ConsoleProgressLogger.php @@ -0,0 +1,59 @@ +output = $output; + } + + /** + * {@inheritdoc} + */ + public function logAdvance($step) + { + if (!$this->progressBar) { + throw new \RuntimeException('Trying to log advance without logging steps first.'); + } + + $this->progressBar->advance($step); + } + + /** + * {@inheritdoc} + */ + public function logFinish() + { + if (!$this->progressBar) { + throw new \RuntimeException('Trying to log finish without logging steps first.'); + } + + $this->progressBar->finish(); + $this->output->writeln(''); + } + + /** + * {@inheritdoc} + */ + public function logSteps($steps) + { + $this->progressBar = new ProgressBar($this->output, $steps); + } +} diff --git a/src/Oro/Component/Log/NullProgressLogger.php b/src/Oro/Component/Log/NullProgressLogger.php new file mode 100644 index 00000000000..cdf9ca3bcb1 --- /dev/null +++ b/src/Oro/Component/Log/NullProgressLogger.php @@ -0,0 +1,30 @@ +progressLogger = $progressLogger; + } +} diff --git a/src/Oro/Component/Log/ProgressLoggerInterface.php b/src/Oro/Component/Log/ProgressLoggerInterface.php new file mode 100644 index 00000000000..7ad5925ba76 --- /dev/null +++ b/src/Oro/Component/Log/ProgressLoggerInterface.php @@ -0,0 +1,21 @@ +