*/ private array $lockNames = []; public static function tryAcquire(int $orderId): ?self { return self::tryAcquireResource(self::ORDER_RESOURCE, $orderId); } public static function tryAcquireInvoiceCollection(int $invoiceCollectionId): ?self { return self::tryAcquireResource(self::INVOICE_COLLECTION_RESOURCE, $invoiceCollectionId); } public static function tryAcquireOrderMutation(int $orderId): ?self { return self::tryAcquireOrderMutations([$orderId]); } /** * @param list $orderIds */ public static function tryAcquireOrderMutations(array $orderIds): ?self { $ids = array_values(array_unique(array_filter( array_map('intval', $orderIds), static fn(int $id): bool => $id > 0 ))); sort($ids, SORT_NUMERIC); $collectionByOrder = []; foreach ($ids as $id) { $collectionByOrder[$id] = self::invoiceCollectionIdForOrder($id); } $collectionIds = array_values(array_unique(array_filter( $collectionByOrder, static fn(int $id): bool => $id > 0 ))); sort($collectionIds, SORT_NUMERIC); $resources = array_map( static fn(int $id): string => self::resourceName(self::INVOICE_COLLECTION_RESOURCE, $id), $collectionIds ); array_push( $resources, ...array_map( static fn(int $id): string => self::resourceName(self::ORDER_RESOURCE, $id), $ids ) ); $lock = self::tryAcquireNames($resources); if ($lock !== null) { foreach ($collectionByOrder as $id => $invoiceCollectionId) { if (self::invoiceCollectionIdForOrder($id) !== $invoiceCollectionId) { $lock->release(); return null; } } } return $lock; } public static function tryAcquireReassignment(int $orderId, int $targetInvoiceCollectionId): ?self { $sourceInvoiceCollectionId = self::invoiceCollectionIdForOrder($orderId); $collectionIds = array_values(array_unique(array_filter([ $sourceInvoiceCollectionId, $targetInvoiceCollectionId, ], static fn(int $id): bool => $id > 0))); sort($collectionIds, SORT_NUMERIC); $resources = array_map( static fn(int $id): string => self::resourceName(self::INVOICE_COLLECTION_RESOURCE, $id), $collectionIds ); $resources[] = self::resourceName(self::ORDER_RESOURCE, $orderId); $lock = self::tryAcquireNames($resources); if ($lock !== null && self::invoiceCollectionIdForOrder($orderId) !== $sourceInvoiceCollectionId) { $lock->release(); return null; } return $lock; } /** * @param list $invoiceCollectionIds */ public static function tryAcquireInvoiceCollections(array $invoiceCollectionIds): ?self { $ids = array_values(array_unique(array_filter( array_map('intval', $invoiceCollectionIds), static fn(int $id): bool => $id > 0 ))); sort($ids, SORT_NUMERIC); return self::tryAcquireNames(array_map( static fn(int $id): string => self::resourceName(self::INVOICE_COLLECTION_RESOURCE, $id), $ids )); } public static function tryAcquireInvoiceCollectionWithOrders(int $invoiceCollectionId): ?self { $lock = self::tryAcquireInvoiceCollection($invoiceCollectionId); if ($lock === null) { return null; } try { $lock->acquireNames(array_map( static fn(int $id): string => self::resourceName(self::ORDER_RESOURCE, $id), self::orderIdsForInvoiceCollection($invoiceCollectionId) )); return $lock; } catch (UnexpectedValueException) { $lock->release(); return null; } } private static function tryAcquireResource(string $resource, int $resourceId): ?self { return self::tryAcquireNames([self::resourceName($resource, $resourceId)]); } /** * @param list $lockNames */ private static function tryAcquireNames(array $lockNames): ?self { try { return new self($lockNames); } catch (UnexpectedValueException) { return null; } } private static function resourceName(string $resource, int $resourceId): string { if ($resourceId <= 0) { throw new RuntimeException('A valid resource ID is required for the payment lock.'); } return $resource . ':' . $resourceId; } private static function invoiceCollectionIdForOrder(int $orderId): int { global $db; if ($orderId <= 0) { throw new RuntimeException('A valid order ID is required for the payment lock.'); } $result = $db->query( 'SELECT `invoice_collection_id` FROM `orders` WHERE `id` = ' . $orderId . ' LIMIT 1' ); $row = $result ? $result->fetch_assoc() : null; return (int)($row['invoice_collection_id'] ?? 0); } /** * @return list */ private static function orderIdsForInvoiceCollection(int $invoiceCollectionId): array { global $db; $result = $db->query( 'SELECT `id` FROM `orders` WHERE `invoice_collection_id` = ' . $invoiceCollectionId . ' ORDER BY `id` ASC' ); $ids = []; while ($result && ($row = $result->fetch_assoc())) { $ids[] = (int)$row['id']; } return $ids; } /** * @param list $lockNames */ private function __construct(array $lockNames) { $this->acquireNames($lockNames); } /** * @param list $lockNames */ private function acquireNames(array $lockNames): void { global $db; foreach ($lockNames as $lockName) { if (in_array($lockName, $this->lockNames, true)) { continue; } $statement = $db->prepare('SELECT GET_LOCK(?, ?) AS acquired'); if ($statement === false) { $this->release(); throw new RuntimeException('Unable to prepare the order payment lock.'); } $timeout = self::LOCK_TIMEOUT_SECONDS; $statement->bind_param('si', $lockName, $timeout); $statement->execute(); $result = $statement->get_result()->fetch_assoc(); $statement->close(); if ((int)($result['acquired'] ?? 0) !== 1) { $this->release(); throw new UnexpectedValueException( 'The order or invoice collection is currently being changed or paid. Try again.' ); } $this->lockNames[] = $lockName; } } public function release(): void { global $db; foreach (array_reverse($this->lockNames) as $lockName) { $statement = $db->prepare('SELECT RELEASE_LOCK(?)'); if ($statement !== false) { $statement->bind_param('s', $lockName); $statement->execute(); $statement->close(); } } $this->lockNames = []; } public function __destruct() { $this->release(); } }