-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
An optimistic locking mechanism for campaigns (#13659)
* An optimistic locking mechanism for campaigns * Rector fixes * Undefined event.deleted fixed --------- Co-authored-by: John Linhart <admin@escope.cz>
- Loading branch information
Showing
11 changed files
with
370 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
app/bundles/CampaignBundle/Tests/Functional/Controller/CampaignOptimisticLockTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Mautic\CampaignBundle\Tests\Functional\Controller; | ||
|
||
use Mautic\CampaignBundle\Entity\Campaign; | ||
use Mautic\CampaignBundle\Entity\Event; | ||
use Mautic\CoreBundle\Test\MauticMysqlTestCase; | ||
use Mautic\LeadBundle\Entity\Lead; | ||
use Mautic\LeadBundle\Entity\LeadList; | ||
use PHPUnit\Framework\Assert; | ||
use Symfony\Component\DomCrawler\Crawler; | ||
|
||
class CampaignOptimisticLockTest extends MauticMysqlTestCase | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
private const OPTIMISTIC_LOCK_ERROR = 'The record you are updating has been changed by someone else in the meantime. Please refresh the browser window and re-submit your changes.'; | ||
|
||
public function testOptimisticLock(): void | ||
{ | ||
$campaign = $this->setupCampaign(); | ||
$version = $campaign->getVersion(); | ||
|
||
// version should be incremented as campaign's "modified by user" is updated | ||
$this->refreshAndSubmitForm($campaign, ++$version); | ||
|
||
// version should not be incremented as there are no changes | ||
$this->refreshAndSubmitForm($campaign, $version); | ||
|
||
// version should be incremented as there are changes | ||
$this->refreshAndSubmitForm($campaign, ++$version, [ | ||
'campaign[allowRestart]' => '1', | ||
'campaign[isPublished]' => '1', | ||
]); | ||
|
||
// version should not be incremented as there are no changes | ||
$this->refreshAndSubmitForm($campaign, $version); | ||
|
||
// refresh the page | ||
$pageCrawler = $this->refreshPage($campaign); | ||
|
||
// we should not get an optimistic lock error as the page was refreshed, version should be incremented | ||
$crawler = $this->submitForm($pageCrawler, $campaign, ++$version, [ | ||
'campaign[allowRestart]' => '0', | ||
]); | ||
Assert::assertStringNotContainsString(self::OPTIMISTIC_LOCK_ERROR, $crawler->text()); | ||
|
||
// we should get an optimistic lock error as the page wasn't refreshed | ||
$crawler = $this->submitForm($pageCrawler, $campaign, $version, [ | ||
'campaign[isPublished]' => '1', | ||
]); | ||
Assert::assertStringContainsString(self::OPTIMISTIC_LOCK_ERROR, $crawler->text()); | ||
|
||
// we should get an optimistic lock error even if there is no change | ||
$crawler = $this->submitForm($pageCrawler, $campaign, $version); | ||
Assert::assertStringContainsString(self::OPTIMISTIC_LOCK_ERROR, $crawler->text()); | ||
} | ||
|
||
/** | ||
* @param array<string,string> $formValues | ||
*/ | ||
private function refreshAndSubmitForm(Campaign $campaign, int $expectedVersion, array $formValues = []): void | ||
{ | ||
$crawler = $this->refreshPage($campaign); | ||
$this->submitForm($crawler, $campaign, $expectedVersion, $formValues); | ||
} | ||
|
||
private function refreshPage(Campaign $campaign): Crawler | ||
{ | ||
$crawler = $this->client->request('GET', sprintf('/s/campaigns/edit/%s', $campaign->getId())); | ||
Assert::assertTrue($this->client->getResponse()->isOk()); | ||
Assert::assertStringContainsString('Edit Campaign', $crawler->text()); | ||
|
||
return $crawler; | ||
} | ||
|
||
/** | ||
* @param array<string,string> $formValues | ||
*/ | ||
private function submitForm(Crawler $crawler, Campaign $campaign, int $expectedVersion, array $formValues = []): Crawler | ||
{ | ||
$form = $crawler->selectButton('Save')->form(); | ||
$form->setValues($formValues); | ||
$newCrawler = $this->client->submit($form); | ||
Assert::assertTrue($this->client->getResponse()->isOk()); | ||
|
||
$this->em->clear(); | ||
$campaign = $this->em->find(Campaign::class, $campaign->getId()); | ||
Assert::assertSame($expectedVersion, $campaign->getVersion()); | ||
|
||
return $newCrawler; | ||
} | ||
|
||
private function setupCampaign(): Campaign | ||
{ | ||
$leadList = new LeadList(); | ||
$leadList->setName('Test list'); | ||
$leadList->setPublicName('Test list'); | ||
$leadList->setAlias('test-list'); | ||
$this->em->persist($leadList); | ||
|
||
$campaign = new Campaign(); | ||
$campaign->setName('Test campaign'); | ||
$campaign->addList($leadList); | ||
$this->em->persist($campaign); | ||
|
||
$lead = new Lead(); | ||
$lead->setFirstname('Test Lead'); | ||
$this->em->persist($lead); | ||
|
||
$campaignEvent = new Event(); | ||
$campaignEvent->setCampaign($campaign); | ||
$campaignEvent->setName('Send Email 1'); | ||
$campaignEvent->setType('email.send'); | ||
$campaignEvent->setEventType('action'); | ||
$campaignEvent->setProperties([]); | ||
$this->em->persist($campaignEvent); | ||
|
||
$this->em->flush(); | ||
$this->em->clear(); | ||
|
||
return $campaign; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Mautic\CoreBundle\Entity; | ||
|
||
/** | ||
* Optimistic locking is applied for entities that implements this interface. | ||
*/ | ||
interface OptimisticLockInterface | ||
{ | ||
/** | ||
* Returns the current version of the entity. | ||
*/ | ||
public function getVersion(): int; | ||
|
||
/** | ||
* Sets a new version of the entity and resets the mark for incrementing the version. | ||
*/ | ||
public function setVersion(int $version): void; | ||
|
||
/** | ||
* Returns true if the entity is marked for incrementing the version in a subsequent flush call. | ||
*/ | ||
public function isMarkedForVersionIncrement(): bool; | ||
|
||
/** | ||
* Mark the entity for incrementing the version in a subsequent flush call. | ||
*/ | ||
public function markForVersionIncrement(): void; | ||
|
||
/** | ||
* Returns the name of the version field. | ||
*/ | ||
public function getVersionField(): string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Mautic\CoreBundle\Entity; | ||
|
||
use Doctrine\DBAL\Types\Types; | ||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder; | ||
|
||
/** | ||
* This trait provides default implementation of OptimisticLockInterface. | ||
*/ | ||
trait OptimisticLockTrait | ||
{ | ||
private int $version = 1; | ||
private ?int $currentVersion; | ||
private bool $incrementVersion = false; | ||
|
||
public function getVersion(): int | ||
{ | ||
return $this->currentVersion ?? $this->version; | ||
} | ||
|
||
public function setVersion(int $version): void | ||
{ | ||
$this->currentVersion = $version; | ||
$this->incrementVersion = false; | ||
} | ||
|
||
public function isMarkedForVersionIncrement(): bool | ||
{ | ||
return $this->incrementVersion; | ||
} | ||
|
||
public function markForVersionIncrement(): void | ||
{ | ||
$this->incrementVersion = true; | ||
} | ||
|
||
public function getVersionField(): string | ||
{ | ||
return 'version'; | ||
} | ||
|
||
private static function addVersionField(ClassMetadataBuilder $builder): void | ||
{ | ||
$builder->createField('version', Types::INTEGER) | ||
->columnName('version') | ||
->option('default', 1) | ||
->option('unsigned', true) | ||
->build(); | ||
} | ||
} |
Oops, something went wrong.