diff --git a/Messenger/CommandMessage.php b/Messenger/CommandMessage.php index 9ea7cde..06b6efb 100644 --- a/Messenger/CommandMessage.php +++ b/Messenger/CommandMessage.php @@ -96,4 +96,18 @@ abstract class CommandMessage return $options; } + + /** + * For lockable message + * + * @return array + */ + public function __serialize(): array + { + return [ + 'command' => $this->getCommandName(), + 'arguments' => $this->getArguments(), + 'options' => $this->getOptions() + ]; + } } diff --git a/Messenger/Middleware/LockableMessage.php b/Messenger/Middleware/LockableMessage.php new file mode 100644 index 0000000..9550df3 --- /dev/null +++ b/Messenger/Middleware/LockableMessage.php @@ -0,0 +1,13 @@ +lockFactory = $lockFactory; + } + + /** + * @param Envelope $envelope + * @param StackInterface $stack + * + * @return Envelope + * + * @throws Throwable + */ + public function handle(Envelope $envelope, StackInterface $stack): Envelope + { + $message = $envelope->getMessage(); + + if ($envelope->all(ReceivedStamp::class) && $message instanceof LockableMessage) { + $lock = $this->lockFactory->createLock($this->objectHash($message), null); + if (!$lock->acquire()) { + return $envelope; + } + + try { + return $stack->next()->handle($envelope, $stack); + } catch (Throwable $exception) { + throw $exception; + } finally { + $lock->release(); + } + } + + return $stack->next()->handle($envelope, $stack); + } + + /** + * @param LockableMessage $message + * + * @return string + */ + private function objectHash(LockableMessage $message): string + { + return hash('crc32', serialize($message)); + } +} diff --git a/Tests/DataFixtures/TestMessage.php b/Tests/DataFixtures/TestMessage.php index b69877e..1884b1d 100644 --- a/Tests/DataFixtures/TestMessage.php +++ b/Tests/DataFixtures/TestMessage.php @@ -3,8 +3,9 @@ namespace RetailCrm\ServiceBundle\Tests\DataFixtures; use RetailCrm\ServiceBundle\Messenger\CommandMessage; +use RetailCrm\ServiceBundle\Messenger\Middleware\LockableMessage; -class TestMessage extends CommandMessage +class TestMessage extends CommandMessage implements LockableMessage { public function __construct() { diff --git a/Tests/Messenger/Middleware/LockableMessageMiddlewareTest.php b/Tests/Messenger/Middleware/LockableMessageMiddlewareTest.php new file mode 100644 index 0000000..ea74f78 --- /dev/null +++ b/Tests/Messenger/Middleware/LockableMessageMiddlewareTest.php @@ -0,0 +1,113 @@ +lockFactory = $this->createMock(LockFactory::class); + } + + public function testHandle(): void + { + $store = $this->createMock(PersistingStoreInterface::class); + $key = new Key(uniqid()); + $lock = new Lock($key, $store); + $this->lockFactory->expects(static::once())->method('createLock')->willReturn($lock); + $envelope = new Envelope(new TestMessage(), [new ReceivedStamp('test')]); + + $next = $this->createMock(MiddlewareInterface::class); + $next->method('handle')->willReturn($envelope); + $stack = $this->createMock(StackInterface::class); + $stack->method('next')->willReturn($next); + + $middleware = new LockableMessageMiddleware($this->lockFactory); + $result = $middleware->handle($envelope, $stack); + + static::assertInstanceOf(Envelope::class, $result); + } + + public function testLockHandle(): void + { + $store = $this->createMock(PersistingStoreInterface::class); + $store->method('save')->willThrowException(new LockConflictedException); + $key = new Key(uniqid()); + $lock = new Lock($key, $store); + $this->lockFactory->expects(static::once())->method('createLock')->willReturn($lock); + $envelope = new Envelope(new TestMessage(), [new ReceivedStamp('test')]); + + $next = $this->createMock(MiddlewareInterface::class); + $next->method('handle')->willReturn($envelope); + $stack = $this->createMock(StackInterface::class); + $stack->method('next')->willReturn($next); + + $middleware = new LockableMessageMiddleware($this->lockFactory); + $result = $middleware->handle($envelope, $stack); + + static::assertInstanceOf(Envelope::class, $result); + } + + public function testNonLockableHandle(): void + { + $store = $this->createMock(PersistingStoreInterface::class); + $store->method('save')->willThrowException(new LockConflictedException); + $key = new Key(uniqid()); + $lock = new Lock($key, $store); + $this->lockFactory->expects(static::never())->method('createLock')->willReturn($lock); + $envelope = new Envelope(new \stdClass(), [new ReceivedStamp('test')]); + + $next = $this->createMock(MiddlewareInterface::class); + $next->method('handle')->willReturn($envelope); + $stack = $this->createMock(StackInterface::class); + $stack->method('next')->willReturn($next); + + $middleware = new LockableMessageMiddleware($this->lockFactory); + $result = $middleware->handle($envelope, $stack); + + static::assertInstanceOf(Envelope::class, $result); + } + + public function testNonReceivedHandle(): void + { + $store = $this->createMock(PersistingStoreInterface::class); + $store->method('save')->willThrowException(new LockConflictedException); + $key = new Key(uniqid()); + $lock = new Lock($key, $store); + $this->lockFactory->expects(static::never())->method('createLock')->willReturn($lock); + $envelope = new Envelope(new TestMessage()); + + $next = $this->createMock(MiddlewareInterface::class); + $next->method('handle')->willReturn($envelope); + $stack = $this->createMock(StackInterface::class); + $stack->method('next')->willReturn($next); + + $middleware = new LockableMessageMiddleware($this->lockFactory); + $result = $middleware->handle($envelope, $stack); + + static::assertInstanceOf(Envelope::class, $result); + } +} diff --git a/composer.json b/composer.json index c90c1ab..9fbbf2e 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "symfony/console": "^5.2", "symfony/messenger": "^5.2", "symfony/process": "^5.2", - "symfony/event-dispatcher": "^5.2" + "symfony/event-dispatcher": "^5.2", + "symfony/lock": "^5.2" }, "autoload": { "psr-4": {