diff --git a/Classes/Command/SequenceCommandController.php b/Classes/Command/SequenceCommandController.php index cf9dc7e..881d3c1 100644 --- a/Classes/Command/SequenceCommandController.php +++ b/Classes/Command/SequenceCommandController.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace DigiComp\Sequence\Command; -use DigiComp\Sequence\Domain\Model\Insert; -use DigiComp\Sequence\Service\Exception as DigiCompSequenceServiceException; +use DigiComp\Sequence\Domain\Model\SequenceEntry; +use DigiComp\Sequence\Service\Exception\InvalidSourceException; use DigiComp\Sequence\Service\SequenceGenerator; use Doctrine\DBAL\Driver\Exception as DoctrineDBALDriverException; use Doctrine\DBAL\Exception as DoctrineDBALException; @@ -14,8 +14,6 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; /** - * A database agnostic SequenceNumber generator - * * @Flow\Scope("singleton") */ class SequenceCommandController extends CommandController @@ -33,42 +31,53 @@ class SequenceCommandController extends CommandController protected $entityManager; /** - * Sets minimum number for sequence generator + * Set last number for sequence generator. * - * @param int $to * @param string $type - * @throws DigiCompSequenceServiceException + * @param int $number + * @throws DoctrineDBALDriverException + * @throws DoctrineDBALException + * @throws InvalidSourceException */ - public function advanceCommand(int $to, string $type): void + public function setLastNumberForCommand(string $type, int $number): void { - $this->sequenceGenerator->advanceTo($to, $type); + if ($this->sequenceGenerator->setLastNumberFor($type, $number)) { + $this->outputLine('Last number successfully set.'); + } else { + $this->outputLine('Failed to set last number.'); + } } /** - * @param string[] $typesToClean - * @throws DigiCompSequenceServiceException + * Clean up sequence table. + * + * @param string[] $types * @throws DoctrineDBALDriverException * @throws DoctrineDBALException + * @throws InvalidSourceException */ - public function cleanSequenceInsertsCommand(array $typesToClean = []) + public function cleanUpCommand(array $types = []): void { - $cleanArray = []; - if ($typesToClean === []) { - $results = $this->entityManager - ->createQuery('SELECT i.type, MAX(i.number) max_number FROM ' . Insert::class . ' i GROUP BY i.type') - ->getScalarResult(); - foreach ($results as $result) { - $cleanArray[$result['type']] = (int)$result['max_number']; - } - } else { - foreach ($typesToClean as $typeToClean) { - $cleanArray[$typeToClean] = $this->sequenceGenerator->getLastNumberFor($typeToClean); + if ($types === []) { + foreach ( + $this + ->entityManager + ->createQuery('SELECT DISTINCT(se.type) type FROM ' . SequenceEntry::class . ' se') + ->execute() + as $result + ) { + $types[] = $result['type']; } } - foreach ($cleanArray as $typeToClean => $number) { - $this->entityManager - ->createQuery('DELETE FROM ' . Insert::class . ' i WHERE i.type = ?0 AND i.number < ?1') - ->execute([$typeToClean, $number]); + + foreach ($types as $type) { + $rowCount = $this + ->entityManager + ->createQuery('DELETE FROM ' . SequenceEntry::class . ' se WHERE se.type = ?0 AND se.number < ?1') + ->execute([$type, $this->sequenceGenerator->getLastNumberFor($type)]); + + + $this->outputLine('Deleted ' . $rowCount . ' row(s) for type "' . $type . '".'); } } } diff --git a/Classes/Domain/Model/Insert.php b/Classes/Domain/Model/SequenceEntry.php similarity index 50% rename from Classes/Domain/Model/Insert.php rename to Classes/Domain/Model/SequenceEntry.php index 97b0cb8..d010331 100644 --- a/Classes/Domain/Model/Insert.php +++ b/Classes/Domain/Model/SequenceEntry.php @@ -8,52 +8,35 @@ use Doctrine\ORM\Mapping as ORM; use Neos\Flow\Annotations as Flow; /** - * SequenceInsert - * * @Flow\Entity - * @ORM\Table(indexes={ - * @ORM\Index(name="type_idx", columns={"type"}) - * }) + * @ORM\Table( + * indexes={ + * @ORM\Index(columns={"type"}) + * }, + * uniqueConstraints={ + * @ORM\UniqueConstraint(columns={"type", "number"}) + * } + * ) */ -class Insert +class SequenceEntry { /** - * @Flow\Identity - * @ORM\Id - * @var int - */ - protected int $number; - - /** - * @Flow\Identity - * @ORM\Id * @var string */ protected string $type; /** - * @param int $number - * @param string|object $type + * @var int */ - public function __construct(int $number, $type) - { - $this->setNumber($number); - $this->setType($type); - } - - /** - * @return int - */ - public function getNumber(): int - { - return $this->number; - } + protected int $number; /** + * @param string $type * @param int $number */ - public function setNumber(int $number): void + public function __construct(string $type, int $number) { + $this->type = $type; $this->number = $number; } @@ -66,13 +49,10 @@ class Insert } /** - * @param string|object $type + * @return int */ - public function setType($type): void + public function getNumber(): int { - if (\is_object($type)) { - $type = \get_class($type); - } - $this->type = $type; + return $this->number; } } diff --git a/Classes/Exception.php b/Classes/Exception.php new file mode 100644 index 0000000..383db6f --- /dev/null +++ b/Classes/Exception.php @@ -0,0 +1,11 @@ + 1 we could return new keys immediately for this * request, as we "reserved" the space between. @@ -35,98 +37,102 @@ class SequenceGenerator protected $logger; /** - * @param string|object $type + * @param string|object $source * @return int - * @throws Exception * @throws DoctrineDBALDriverException * @throws DoctrineDBALException + * @throws InvalidSourceException */ - public function getNextNumberFor($type): int + public function getNextNumberFor($source): int { - $type = $this->inferTypeFromSource($type); - $count = $this->getLastNumberFor($type); + $type = $this->inferTypeFromSource($source); + $number = $this->getLastNumberFor($type); - // TODO: Check for maximal tries, or similar - // TODO: Let increment be configurable per type + // TODO: Check for maximal tries, or similar? + // TODO: Let increment be configurable per type? do { - $count++; - } while (!$this->validateFreeNumber($count, $type)); + $number++; + } while (!$this->insertFor($type, $number)); - return $count; + return $number; } /** - * @param int $count * @param string $type + * @param int $number * @return bool */ - protected function validateFreeNumber(int $count, string $type): bool + protected function insertFor(string $type, int $number): bool { - $em = $this->entityManager; try { - $em->getConnection()->insert( - $em->getClassMetadata(Insert::class)->getTableName(), - ['number' => $count, 'type' => $type] + $this->entityManager->getConnection()->insert( + $this->entityManager->getClassMetadata(SequenceEntry::class)->getTableName(), + ['persistence_object_identifier' => Algorithms::generateUUID(), 'number' => $number, 'type' => $type] ); + return true; - } catch (\PDOException $e) { - return false; - } catch (DoctrineDBALException $e) { - if (!$e->getPrevious() instanceof \PDOException) { - $this->logger->critical('Exception occurred: ' . $e->getMessage()); + } catch (\PDOException $exception) { + } catch (DoctrineDBALException $exception) { + if (!$exception->getPrevious() instanceof \PDOException) { + $this->logger->critical('Exception occurred: ' . $exception->getMessage()); } - } catch (\Exception $e) { - $this->logger->critical('Exception occurred: ' . $e->getMessage()); + } catch (\Exception $exception) { + $this->logger->critical('Exception occurred: ' . $exception->getMessage()); } return false; } /** - * @param int $to - * @param string|object $type - * + * @param string|object $source + * @param int $number * @return bool - * @throws Exception + * @throws DoctrineDBALDriverException + * @throws DoctrineDBALException + * @throws InvalidSourceException */ - public function advanceTo(int $to, $type): bool + public function setLastNumberFor($source, int $number): bool { - $type = $this->inferTypeFromSource($type); + $type = $this->inferTypeFromSource($source); - return $this->validateFreeNumber($to, $type); + if ($this->getLastNumberFor($type) >= $number) { + return false; + } + + return $this->insertFor($type, $number); } /** - * @param string|object $type - * @return int - * @throws Exception + * @param string|object $source * @throws DoctrineDBALDriverException * @throws DoctrineDBALException + * @throws InvalidSourceException */ - public function getLastNumberFor($type): int + public function getLastNumberFor($source): int { return (int)$this->entityManager->getConnection()->executeQuery( 'SELECT MAX(number) FROM ' - . $this->entityManager->getClassMetadata(Insert::class)->getTableName() + . $this->entityManager->getClassMetadata(SequenceEntry::class)->getTableName() . ' WHERE type = :type', - ['type' => $this->inferTypeFromSource($type)] + ['type' => $this->inferTypeFromSource($source)] )->fetchOne(); } /** - * @param string|object $stringOrObject + * @param string|object $source * @return string - * @throws Exception + * @throws InvalidSourceException */ - protected function inferTypeFromSource($stringOrObject): string + protected function inferTypeFromSource($source): string { - if (\is_object($stringOrObject)) { - $stringOrObject = TypeHandling::getTypeForValue($stringOrObject); - } - if (!$stringOrObject) { - throw new Exception('No Type given'); + if (\is_string($source)) { + return $source; } - return $stringOrObject; + if (\is_object($source)) { + return TypeHandling::getTypeForValue($source); + } + + throw new InvalidSourceException('Could not infer type from source.', 1632216173); } } diff --git a/Configuration/Testing/Settings.yaml b/Configuration/Testing/Settings.yaml index 54d2680..5d0dfb7 100644 --- a/Configuration/Testing/Settings.yaml +++ b/Configuration/Testing/Settings.yaml @@ -2,5 +2,5 @@ Neos: Flow: persistence: backendOptions: - driver: 'pdo_sqlite' - path: '%FLOW_PATH_DATA%/Temporary/testing.db' + driver: "pdo_sqlite" + path: "%FLOW_PATH_DATA%/Temporary/testing.db" diff --git a/Migrations/Mysql/Version20210922110814.php b/Migrations/Mysql/Version20210922110814.php new file mode 100644 index 0000000..e74c465 --- /dev/null +++ b/Migrations/Mysql/Version20210922110814.php @@ -0,0 +1,39 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on "mysql".'); + + $this->addSql('CREATE TABLE digicomp_sequence_domain_model_sequenceentry (persistence_object_identifier VARCHAR(40) NOT NULL, type VARCHAR(255) NOT NULL, number INT NOT NULL, INDEX IDX_F6ADC8568CDE5729 (type), UNIQUE INDEX UNIQ_F6ADC8568CDE572996901F54 (type, number), PRIMARY KEY(persistence_object_identifier))'); + $this->addSql('INSERT INTO digicomp_sequence_domain_model_sequenceentry (persistence_object_identifier, type, number) SELECT UUID(), i.type, i.number FROM digicomp_sequence_domain_model_insert AS i'); + $this->addSql('DROP TABLE digicomp_sequence_domain_model_insert'); + } + + /** + * @param Schema $schema + * @throws AbortMigrationException + * @throws DoctrineDBALException + */ + public function down(Schema $schema): void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on "mysql".'); + + $this->addSql('CREATE TABLE digicomp_sequence_domain_model_insert (number INT NOT NULL, type VARCHAR(255) NOT NULL, INDEX type_idx (type), PRIMARY KEY(number, type))'); + $this->addSql('INSERT INTO digicomp_sequence_domain_model_insert (number, type) SELECT se.number, se.type FROM digicomp_sequence_domain_model_sequenceentry AS se'); + $this->addSql('DROP TABLE digicomp_sequence_domain_model_sequenceentry'); + } +} diff --git a/README.md b/README.md index 36ea7c5..e796736 100644 --- a/README.md +++ b/README.md @@ -16,5 +16,5 @@ public function __construct(SequenceNumberGenerator $sequenceNumberGenerator) `getNextNumberFor` allows you to give an object (which will be resolved to its FQCN) or a custom sequence name. -The `SequenceCommandController` helps you to advance the current sequence number, in case of migrations or similar. See -`./flow help sequence:advance` if interested. +The `SequenceCommandController` helps you to set the last sequence number, in case of migrations or similar. See +`./flow help sequence:setlastnumberfor` if interested. diff --git a/Tests/Functional/SequenceTest.php b/Tests/Functional/SequenceTest.php index b400c34..4fb3067 100644 --- a/Tests/Functional/SequenceTest.php +++ b/Tests/Functional/SequenceTest.php @@ -1,8 +1,10 @@ objectManager->get(SequenceGenerator::class); - $number = $sequenceGenerator->getLastNumberFor($sequenceGenerator); - $this->assertEquals(0, $number); + $this->assertEquals(0, $sequenceGenerator->getLastNumberFor($sequenceGenerator)); $this->assertEquals(1, $sequenceGenerator->getNextNumberFor($sequenceGenerator)); $pIds = []; for ($i = 0; $i < 10; $i++) { $pId = \pcntl_fork(); - if ($pId) { + if ($pId > 0) { $pIds[] = $pId; } else { for ($j = 0; $j < 10; $j++) { @@ -53,16 +54,17 @@ class SequenceTest extends FunctionalTestCase /** * @test - * @throws DigiCompSequenceServiceException * @throws DoctrineDBALDriverException * @throws DoctrineDBALException + * @throws InvalidSourceException */ - public function advanceTest() + public function setLastNumberForTest() { $sequenceGenerator = $this->objectManager->get(SequenceGenerator::class); - $sequenceGenerator->advanceTo(100, $sequenceGenerator); + $sequenceGenerator->setLastNumberFor($sequenceGenerator, 100); + $this->assertEquals(100, $sequenceGenerator->getLastNumberFor($sequenceGenerator)); - $this->assertEquals(0, $sequenceGenerator->getLastNumberFor('strangeOtherSequence')); + $this->assertEquals(0, $sequenceGenerator->getLastNumberFor('otherSequence')); } }