From 3ffda2b0b282f8f3ee9dde1edd052629c2b41d76 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Mon, 26 Dec 2022 19:20:27 +0000 Subject: [PATCH 01/69] Fix env CONNECTIONS_CONFIG_ENV_VAR fetching --- src/ConnectionManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConnectionManager.php b/src/ConnectionManager.php index 5ff27db..8f7c4c6 100644 --- a/src/ConnectionManager.php +++ b/src/ConnectionManager.php @@ -40,7 +40,7 @@ public static function getConnection(string $name, ?Configuration $config = null private static function getConnectionParams(string $name): array { if (self::$configs === null) { - $configFile = getenv(self::CONNECTIONS_CONFIG_ENV_VAR, true); + $configFile = getenv(self::CONNECTIONS_CONFIG_ENV_VAR, true) ?: ($_ENV[self::CONNECTIONS_CONFIG_ENV_VAR] ?? false); if (empty($configFile)) { throw new DbException(sprintf( 'ConnectionManager is not configured, please call ConnectionManager::configure() method or setup putenv(\'%s=/path/to/config/file.php\') variable', From 9fb532384ff254804d92357cdb3ec1fe40344d6b Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 6 May 2023 09:54:27 +0100 Subject: [PATCH 02/69] Soft delete improvements --- src/AbstractCachedTable.php | 5 ++++- src/AbstractTable.php | 31 +++++++++++++++++++++++++++---- src/TableConfig.php | 3 +++ tests/Table/AbstractTableTest.php | 6 +++--- tests/Table/TableConfigTest.php | 2 +- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index 9d3064e..1f0906f 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -77,7 +77,7 @@ public function delete(AbstractEntity &$entity): void */ public function deleteMany(array $entities): bool { - return $this->getConnection()->transactional(function() use ($entities) { + return (bool)$this->getConnection()->transactional(function() use ($entities) { $cacheKeys = []; foreach ($entities as $entity) { $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity)); @@ -244,6 +244,9 @@ protected function buildCacheKey(mixed ...$parts): string $formattedParts = []; foreach ($parts as $part) { if (is_array($part)) { + if ($this->config->isSoftDelete && array_key_exists('deleted_at', $part)) { + unset($part['deleted_at']); + } $string = json_encode($part); } else { $string = strval($part); diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 4d465f1..4a4c1c4 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -2,10 +2,12 @@ namespace Composite\DB; +use Composite\DB\Helpers\DateTimeHelper; use Composite\Entity\AbstractEntity; use Composite\DB\Exceptions\DbException; use Composite\Entity\Exceptions\EntityException; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Query\QueryBuilder; abstract class AbstractTable @@ -45,7 +47,7 @@ public function save(AbstractEntity &$entity): void $this->config->checkEntity($entity); if ($entity->isNew()) { $connection = $this->getConnection(); - $insertData = $entity->toArray(); + $insertData = $this->formatData($entity->toArray()); $this->getConnection()->insert($this->getTableName(), $insertData); if ($this->config->autoIncrementKey) { @@ -118,8 +120,13 @@ public function delete(AbstractEntity &$entity): void $this->config->checkEntity($entity); if ($this->config->isSoftDelete) { if (method_exists($entity, 'delete')) { + $condition = $this->getPkCondition($entity); + $this->getConnection()->update( + $this->getTableName(), + ['deleted_at' => DateTimeHelper::dateTimeToString(new \DateTime())], + $condition, + ); $entity->delete(); - $this->save($entity); } } else { $where = $this->getPkCondition($entity); @@ -133,7 +140,7 @@ public function delete(AbstractEntity &$entity): void */ public function deleteMany(array $entities): bool { - return $this->getConnection()->transactional(function() use ($entities) { + return (bool)$this->getConnection()->transactional(function() use ($entities) { foreach ($entities as $entity) { $this->delete($entity); } @@ -250,7 +257,11 @@ protected function getPkCondition(int|string|array|AbstractEntity $data): array } } else { foreach ($this->config->primaryKeys as $key) { - $condition[$key] = $data; + if ($this->config->isSoftDelete && $key === 'deleted_at') { + $condition['deleted_at'] = null; + } else { + $condition[$key] = $data; + } } } return $condition; @@ -288,4 +299,16 @@ private function buildWhere(QueryBuilder $query, array $where): void } } } + + private function formatData(array $data): array + { + foreach ($data as $columnName => $value) { + if ($this->getConnection()->getDatabasePlatform() instanceof AbstractMySQLPlatform) { + if (is_bool($value)) { + $data[$columnName] = $value ? 1 : 0; + } + } + } + return $data; + } } diff --git a/src/TableConfig.php b/src/TableConfig.php index c19c23e..ef2bacb 100644 --- a/src/TableConfig.php +++ b/src/TableConfig.php @@ -55,6 +55,9 @@ public static function fromEntitySchema(Schema $schema): TableConfig foreach (class_uses($schema->class) as $traitClass) { if ($traitClass === Traits\SoftDelete::class) { $isSoftDelete = true; + if (!\in_array('deleted_at', $primaryKeys)) { + $primaryKeys[] = 'deleted_at'; + } } elseif ($traitClass === Traits\OptimisticLock::class) { $isOptimisticLock = true; } diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php index 0a7e78c..5cbedcd 100644 --- a/tests/Table/AbstractTableTest.php +++ b/tests/Table/AbstractTableTest.php @@ -48,17 +48,17 @@ public function getPkCondition_dataProvider(): array [ new Tables\TestAutoincrementSdTable(), Entities\TestAutoincrementSdEntity::fromArray(['id' => 123, 'name' => 'John']), - ['id' => 123], + ['id' => 123, 'deleted_at' => null], ], [ new Tables\TestCompositeSdTable(), new Entities\TestCompositeSdEntity(user_id: 123, post_id: 456, message: 'Text'), - ['user_id' => 123, 'post_id' => 456], + ['user_id' => 123, 'post_id' => 456, 'deleted_at' => null], ], [ new Tables\TestUniqueSdTable(), new Entities\TestUniqueSdEntity(id: '123abc', name: 'John'), - ['id' => '123abc'], + ['id' => '123abc', 'deleted_at' => null], ], ]; } diff --git a/tests/Table/TableConfigTest.php b/tests/Table/TableConfigTest.php index b4c558e..528a835 100644 --- a/tests/Table/TableConfigTest.php +++ b/tests/Table/TableConfigTest.php @@ -31,7 +31,7 @@ public function __construct( $this->assertNotEmpty($tableConfig->connectionName); $this->assertNotEmpty($tableConfig->tableName); $this->assertTrue($tableConfig->isSoftDelete); - $this->assertCount(1, $tableConfig->primaryKeys); + $this->assertCount(2, $tableConfig->primaryKeys); $this->assertSame('id', $tableConfig->autoIncrementKey); } } \ No newline at end of file From 317462f8527fba12b5747fdf608d5d1301d2d9a2 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 6 May 2023 09:57:03 +0100 Subject: [PATCH 03/69] Move code generation to compositephp/sync repository and make db repo more clean and independent --- composer.json | 9 +- src/Attributes/Column.php | 15 - src/Attributes/Index.php | 13 - src/Attributes/Strict.php | 6 - src/Commands/CommandHelperTrait.php | 108 ------- src/Commands/GenerateEntityCommand.php | 90 ------ src/Commands/GenerateTableCommand.php | 95 ------- src/Generator/AbstractTableClassBuilder.php | 68 ----- src/Generator/CachedTableClassBuilder.php | 97 ------- src/Generator/EntityClassBuilder.php | 264 ------------------ src/Generator/EnumClassBuilder.php | 32 --- src/Generator/Schema/ColumnType.php | 15 - .../Schema/Parsers/MySQLSchemaParser.php | 122 -------- .../Schema/Parsers/PostgresSchemaParser.php | 221 --------------- .../Schema/Parsers/SQLiteSchemaParser.php | 247 ---------------- src/Generator/Schema/SQLColumn.php | 45 --- src/Generator/Schema/SQLEnum.php | 11 - src/Generator/Schema/SQLIndex.php | 13 - src/Generator/Schema/SQLSchema.php | 52 ---- src/Generator/TableClassBuilder.php | 77 ----- src/Generator/Templates/EntityTemplate.php | 41 --- src/Helpers/ClassHelper.php | 18 -- .../Entities/TestDiversityEntity.php | 1 - tests/TestStand/Entities/TestEntity.php | 1 - .../Entities/{Enums => }/TestSubEntity.php | 2 +- 25 files changed, 4 insertions(+), 1659 deletions(-) delete mode 100644 src/Attributes/Column.php delete mode 100644 src/Attributes/Index.php delete mode 100644 src/Attributes/Strict.php delete mode 100644 src/Commands/CommandHelperTrait.php delete mode 100644 src/Commands/GenerateEntityCommand.php delete mode 100644 src/Commands/GenerateTableCommand.php delete mode 100644 src/Generator/AbstractTableClassBuilder.php delete mode 100644 src/Generator/CachedTableClassBuilder.php delete mode 100644 src/Generator/EntityClassBuilder.php delete mode 100644 src/Generator/EnumClassBuilder.php delete mode 100644 src/Generator/Schema/ColumnType.php delete mode 100644 src/Generator/Schema/Parsers/MySQLSchemaParser.php delete mode 100644 src/Generator/Schema/Parsers/PostgresSchemaParser.php delete mode 100644 src/Generator/Schema/Parsers/SQLiteSchemaParser.php delete mode 100644 src/Generator/Schema/SQLColumn.php delete mode 100644 src/Generator/Schema/SQLEnum.php delete mode 100644 src/Generator/Schema/SQLIndex.php delete mode 100644 src/Generator/Schema/SQLSchema.php delete mode 100644 src/Generator/TableClassBuilder.php delete mode 100644 src/Generator/Templates/EntityTemplate.php delete mode 100644 src/Helpers/ClassHelper.php rename tests/TestStand/Entities/{Enums => }/TestSubEntity.php (80%) diff --git a/composer.json b/composer.json index bc13c29..5d8f909 100644 --- a/composer.json +++ b/composer.json @@ -13,13 +13,10 @@ ], "require": { "php": "^8.1", + "ext-pdo": "*", "psr/simple-cache": "1 - 3", - "compositephp/entity": "^0.1.2", - "doctrine/dbal": "^3.5", - "doctrine/inflector": "^2.0", - "iamcal/sql-parser": "^0.4.0", - "nette/php-generator": "^4.0", - "symfony/console": "2 - 6" + "compositephp/entity": "dev-master", + "doctrine/dbal": "^3.5" }, "require-dev": { "kodus/file-cache": "^2.0", diff --git a/src/Attributes/Column.php b/src/Attributes/Column.php deleted file mode 100644 index 11ac3ca..0000000 --- a/src/Attributes/Column.php +++ /dev/null @@ -1,15 +0,0 @@ -writeln("$text"); - return Command::SUCCESS; - } - - private function showAlert(OutputInterface $output, string $text): int - { - $output->writeln("$text"); - return Command::SUCCESS; - } - - private function showError(OutputInterface $output, string $text): int - { - $output->writeln("$text"); - return Command::INVALID; - } - - protected function ask(InputInterface $input, OutputInterface $output, Question $question): mixed - { - return (new QuestionHelper())->ask($input, $output, $question); - } - - private function saveClassToFile(InputInterface $input, OutputInterface $output, string $class, string $content): bool - { - if (!$filePath = $this->getClassFilePath($class)) { - return false; - } - $fileState = 'new'; - if (file_exists($filePath)) { - $fileState = 'overwrite'; - if (!$input->getOption('force') - && !$this->ask($input, $output, new ConfirmationQuestion("File `$filePath` is already exists, do you want to overwrite it?[y/n]: "))) { - return true; - } - } - if (file_put_contents($filePath, $content)) { - $this->showSuccess($output, "File `$filePath` was successfully generated ($fileState)"); - return true; - } else { - $this->showError($output, "Something went wrong can `$filePath` was successfully generated ($fileState)"); - return false; - } - } - - protected function getClassFilePath(string $class): ?string - { - $class = trim($class, '\\'); - $namespaceParts = explode('\\', $class); - - $loaders = ClassLoader::getRegisteredLoaders(); - $matchedPrefixes = $matchedDirs = []; - foreach ($loaders as $loader) { - foreach ($loader->getPrefixesPsr4() as $prefix => $dir) { - $prefixParts = explode('\\', trim($prefix, '\\')); - foreach ($namespaceParts as $i => $namespacePart) { - if (!isset($prefixParts[$i]) || $prefixParts[$i] !== $namespacePart) { - break; - } - if (!isset($matchedPrefixes[$prefix])) { - $matchedPrefixes[$prefix] = 0; - $matchedDirs[$prefix] = $dir; - } - $matchedPrefixes[$prefix] += 1; - } - } - } - if (empty($matchedPrefixes)) { - throw new \Exception("Failed to determine directory for class `$class` from psr4 autoloading"); - } - arsort($matchedPrefixes); - $prefix = key($matchedPrefixes); - $dirs = $matchedDirs[$prefix]; - - $namespaceParts = explode('\\', str_replace($prefix, '', $class)); - $filename = array_pop($namespaceParts) . '.php'; - - $relativeDir = implode( - DIRECTORY_SEPARATOR, - array_merge( - $dirs, - $namespaceParts, - ) - ); - if (!$realDir = realpath($relativeDir)) { - $dirCreateResult = mkdir($relativeDir, 0755, true); - if (!$dirCreateResult) { - throw new \Exception("Directory `$relativeDir` not exists and failed to create it, please create it manually."); - } - $realDir = realpath($relativeDir); - } - return $realDir . DIRECTORY_SEPARATOR . $filename; - } -} diff --git a/src/Commands/GenerateEntityCommand.php b/src/Commands/GenerateEntityCommand.php deleted file mode 100644 index 16c3cdd..0000000 --- a/src/Commands/GenerateEntityCommand.php +++ /dev/null @@ -1,90 +0,0 @@ -addArgument('connection', InputArgument::REQUIRED, 'Connection name') - ->addArgument('table', InputArgument::REQUIRED, 'Table name') - ->addArgument('entity', InputArgument::OPTIONAL, 'Entity full class name') - ->addOption('force', 'f', InputOption::VALUE_NONE, 'If existing file should be overwritten'); - } - - /** - * @throws \Exception - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - $connectionName = $input->getArgument('connection'); - $tableName = $input->getArgument('table'); - $connection = ConnectionManager::getConnection($connectionName); - - if (!$entityClass = $input->getArgument('entity')) { - $entityClass = $this->ask($input, $output, new Question('Enter entity full class name: ')); - } - $entityClass = str_replace('\\\\', '\\', $entityClass); - - $schema = SQLSchema::generate($connection, $tableName); - $enums = []; - foreach ($schema->enums as $columnName => $sqlEnum) { - if ($enumClass = $this->generateEnum($input, $output, $entityClass, $sqlEnum)) { - $enums[$columnName] = $enumClass; - } - } - $entityBuilder = new EntityClassBuilder($schema, $connectionName, $entityClass, $enums); - $content = $entityBuilder->getClassContent(); - - $this->saveClassToFile($input, $output, $entityClass, $content); - return Command::SUCCESS; - } - - private function generateEnum(InputInterface $input, OutputInterface $output, string $entityClass, SQLEnum $enum): ?string - { - $name = $enum->name; - $values = $enum->values; - $this->showAlert($output, "Found enum `$name` with values [" . implode(', ', $values) . "]"); - if (!$this->ask($input, $output, new ConfirmationQuestion('Do you want to generate Enum class?[y/n]: '))) { - return null; - } - $enumShortClassName = ucfirst((new InflectorFactory())->build()->camelize($name)); - $entityNamespace = ClassHelper::extractNamespace($entityClass); - $proposedClass = $entityNamespace . '\\Enums\\' . $enumShortClassName; - $enumClass = $this->ask( - $input, - $output, - new Question("Enter enum full class name [skip to use $proposedClass]: ") - ); - if (!$enumClass) { - $enumClass = $proposedClass; - } - $enumClassBuilder = new EnumClassBuilder($enumClass, $values); - - $content = $enumClassBuilder->getClassContent(); - if (!$this->saveClassToFile($input, $output, $enumClass, $content)) { - return null; - } - return $enumClass; - } -} \ No newline at end of file diff --git a/src/Commands/GenerateTableCommand.php b/src/Commands/GenerateTableCommand.php deleted file mode 100644 index fa14cbb..0000000 --- a/src/Commands/GenerateTableCommand.php +++ /dev/null @@ -1,95 +0,0 @@ -addArgument('entity', InputArgument::REQUIRED, 'Entity full class name') - ->addArgument('table', InputArgument::OPTIONAL, 'Table full class name') - ->addOption('cached', 'c', InputOption::VALUE_NONE, 'Generate cache version') - ->addOption('force', 'f', InputOption::VALUE_NONE, 'Overwrite existing table class file'); - } - - /** - * @throws \Exception - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - /** @var class-string $entityClass */ - $entityClass = $input->getArgument('entity'); - $reflection = new \ReflectionClass($entityClass); - - if (!$reflection->isSubclassOf(AbstractEntity::class)) { - return $this->showError($output, "Class `$entityClass` must be subclass of " . AbstractEntity::class); - } - $schema = $entityClass::schema(); - $tableConfig = TableConfig::fromEntitySchema($schema); - $tableName = $tableConfig->tableName; - - if (!$tableClass = $input->getArgument('table')) { - $proposedClass = preg_replace('/\w+$/', 'Tables', $reflection->getNamespaceName()) . "\\{$tableName}Table"; - $tableClass = $this->ask( - $input, - $output, - new Question("Enter table full class name [skip to use $proposedClass]: ") - ); - if (!$tableClass) { - $tableClass = $proposedClass; - } - } - if (str_starts_with($tableClass, '\\')) { - $tableClass = substr($tableClass, 1); - } - - if (!preg_match('/^(.+)\\\(\w+)$/', $tableClass)) { - return $this->showError($output, "Table class `$tableClass` is incorrect"); - } - if ($input->getOption('cached')) { - $template = new CachedTableClassBuilder( - tableClass: $tableClass, - schema: $schema, - tableConfig: $tableConfig, - ); - } else { - $template = new TableClassBuilder( - tableClass: $tableClass, - schema: $schema, - tableConfig: $tableConfig, - ); - } - $template->generate(); - $fileContent = $template->getFileContent(); - - $fileState = 'new'; - if (!$filePath = $this->getClassFilePath($tableClass)) { - return Command::FAILURE; - } - if (file_exists($filePath)) { - if (!$input->getOption('force')) { - return $this->showError($output, "File `$filePath` already exists, use --force flag to overwrite it"); - } - $fileState = 'overwrite'; - } - file_put_contents($filePath, $fileContent); - return $this->showSuccess($output, "File `$filePath` was successfully generated ($fileState)"); - } -} diff --git a/src/Generator/AbstractTableClassBuilder.php b/src/Generator/AbstractTableClassBuilder.php deleted file mode 100644 index f1e7063..0000000 --- a/src/Generator/AbstractTableClassBuilder.php +++ /dev/null @@ -1,68 +0,0 @@ -entityClassShortName = ClassHelper::extractShortName($this->schema->class); - $this->file = new PhpFile(); - } - - abstract public function getParentNamespace(): string; - abstract public function generate(): void; - - final public function getFileContent(): string - { - return (string)$this->file; - } - - protected function generateGetConfig(): Method - { - return (new Method('getConfig')) - ->setProtected() - ->setReturnType(TableConfig::class) - ->setBody('return TableConfig::fromEntitySchema(' . $this->entityClassShortName . '::schema());'); - } - - protected function buildVarsList(array $vars): string - { - if (count($vars) === 1) { - $var = current($vars); - return '$' . $var; - } - $vars = array_map( - fn ($var) => "'$var' => \$" . $var, - $vars - ); - return '[' . implode(', ', $vars) . ']'; - } - - /** - * @param AbstractColumn[] $columns - */ - protected function addMethodParameters(Method $method, array $columns): void - { - foreach ($columns as $column) { - $method - ->addParameter($column->name) - ->setType($column->type); - } - } -} \ No newline at end of file diff --git a/src/Generator/CachedTableClassBuilder.php b/src/Generator/CachedTableClassBuilder.php deleted file mode 100644 index 760ad44..0000000 --- a/src/Generator/CachedTableClassBuilder.php +++ /dev/null @@ -1,97 +0,0 @@ -file - ->addNamespace(ClassHelper::extractNamespace($this->tableClass)) - ->addUse(AbstractEntity::class) - ->addUse(AbstractCachedTable::class) - ->addUse(TableConfig::class) - ->addUse($this->schema->class) - ->addClass(ClassHelper::extractShortName($this->tableClass)) - ->setExtends(AbstractCachedTable::class) - ->setMethods($this->getMethods()); - } - - private function getMethods(): array - { - return array_filter([ - $this->generateGetConfig(), - $this->generateGetFlushCacheKeys(), - $this->generateFindOne(), - $this->generateFindAll(), - $this->generateCountAll(), - ]); - } - - protected function generateGetFlushCacheKeys(): Method - { - $method = (new Method('getFlushCacheKeys')) - ->setProtected() - ->setReturnType('array') - ->addBody('return [') - ->addBody(' $this->getListCacheKey(),') - ->addBody(' $this->getCountCacheKey(),') - ->addBody('];'); - - $type = $this->schema->class . '|' . AbstractEntity::class; - $method - ->addParameter('entity') - ->setType($type); - return $method; - } - - protected function generateFindOne(): ?Method - { - $primaryColumns = array_map( - fn(string $key): AbstractColumn => $this->schema->getColumn($key) ?? throw new \Exception("Primary key column `$key` not found in entity."), - $this->tableConfig->primaryKeys - ); - if (count($this->tableConfig->primaryKeys) === 1) { - $body = 'return $this->createEntity($this->findByPkCachedInternal(' . $this->buildVarsList($this->tableConfig->primaryKeys) . '));'; - } else { - $body = 'return $this->createEntity($this->findOneCachedInternal(' . $this->buildVarsList($this->tableConfig->primaryKeys) . '));'; - } - - $method = (new Method('findByPk')) - ->setPublic() - ->setReturnType($this->schema->class) - ->setReturnNullable() - ->setBody($body); - $this->addMethodParameters($method, $primaryColumns); - return $method; - } - - protected function generateFindAll(): Method - { - return (new Method('findAll')) - ->setPublic() - ->setComment('@return ' . $this->entityClassShortName . '[]') - ->setReturnType('array') - ->setBody('return $this->createEntities($this->findAllCachedInternal());'); - } - - protected function generateCountAll(): Method - { - return (new Method('countAll')) - ->setPublic() - ->setReturnType('int') - ->setBody('return $this->countAllCachedInternal();'); - } -} \ No newline at end of file diff --git a/src/Generator/EntityClassBuilder.php b/src/Generator/EntityClassBuilder.php deleted file mode 100644 index 96453bd..0000000 --- a/src/Generator/EntityClassBuilder.php +++ /dev/null @@ -1,264 +0,0 @@ -renderTemplate('EntityTemplate', $this->getVars()); - } - - /** - * @return array - * @throws \Exception - */ - private function getVars(): array - { - $traits = $properties = []; - $constructorParams = $this->getEntityProperties(); - if (!empty($this->schema->columns['deleted_at'])) { - $traits[] = 'Traits\SoftDelete'; - $this->useNamespaces[] = 'Composite\DB\Traits'; - unset($constructorParams['deleted_at']); - } - foreach ($constructorParams as $name => $constructorParam) { - if ($this->schema->columns[$name]->isAutoincrement) { - $properties[$name] = $constructorParam; - unset($constructorParams[$name]); - } - } - if (!preg_match('/^(.+)\\\(\w+)$/', $this->entityClass, $matches)) { - throw new \Exception("Entity class `$this->entityClass` is incorrect"); - } - - return [ - 'phpOpener' => ' $this->connectionName, - 'tableName' => $this->schema->tableName, - 'pkNames' => "'" . implode("', '", $this->schema->primaryKeys) . "'", - 'indexes' => $this->getIndexes(), - 'traits' => $traits, - 'entityNamespace' => $matches[1], - 'entityClassShortname' => $matches[2], - 'properties' => $properties, - 'constructorParams' => $constructorParams, - 'useNamespaces' => array_unique($this->useNamespaces), - 'useAttributes' => array_unique($this->useAttributes), - ]; - } - - private function getEntityProperties(): array - { - $noDefaultValue = $hasDefaultValue = []; - foreach ($this->schema->columns as $column) { - $attributes = []; - if ($this->schema->isPrimaryKey($column->name)) { - $this->useAttributes[] = 'PrimaryKey'; - $autoIncrement = $column->isAutoincrement ? '(autoIncrement: true)' : ''; - $attributes[] = '#[PrimaryKey' . $autoIncrement . ']'; - } - if ($columnAttributeProperties = $column->getColumnAttributeProperties()) { - $this->useAttributes[] = 'Column'; - $attributes[] = '#[Column(' . implode(', ', $columnAttributeProperties) . ')]'; - } - $propertyParts = [$this->getPropertyVisibility($column)]; - if ($this->isReadOnly($column)) { - $propertyParts[] = 'readonly'; - } - $propertyParts[] = $this->getColumnType($column); - $propertyParts[] = '$' . $column->name; - if ($column->hasDefaultValue) { - $defaultValue = $this->getDefaultValue($column); - $propertyParts[] = '= ' . $defaultValue; - $hasDefaultValue[$column->name] = [ - 'attributes' => $attributes, - 'var' => implode(' ', $propertyParts), - ]; - } else { - $noDefaultValue[$column->name] = [ - 'attributes' => $attributes, - 'var' => implode(' ', $propertyParts), - ]; - } - } - return array_merge($noDefaultValue, $hasDefaultValue); - } - - private function getPropertyVisibility(SQLColumn $column): string - { - return 'public'; - } - - private function isReadOnly(SQLColumn $column): bool - { - if ($column->isAutoincrement) { - return true; - } - $readOnlyColumns = array_merge( - $this->schema->primaryKeys, - [ - 'created_at', - 'createdAt', - ] - ); - return in_array($column->name, $readOnlyColumns); - } - - private function getColumnType(SQLColumn $column): string - { - if ($column->type === ColumnType::Enum) { - if (!$type = $this->getEnumName($column->name)) { - $type = 'string'; - } - } else { - $type = $column->type->value; - } - if ($column->isNullable) { - $type = '?' . $type; - } - return $type; - } - - public function getDefaultValue(SQLColumn $column): mixed - { - $defaultValue = $column->defaultValue; - if ($defaultValue === null) { - return 'null'; - } - if ($column->type === ColumnType::Datetime) { - $currentTimestamp = stripos($defaultValue, 'current_timestamp') === 0 || $defaultValue === 'now()'; - if ($currentTimestamp) { - $defaultValue = "new \DateTimeImmutable()"; - } else { - if ($defaultValue === 'epoch') { - $defaultValue = '1970-01-01 00:00:00'; - } elseif ($defaultValue instanceof \DateTimeInterface) { - $defaultValue = DateTimeHelper::dateTimeToString($defaultValue); - } - $defaultValue = "new \DateTimeImmutable('" . $defaultValue . "')"; - } - } elseif ($column->type === ColumnType::Enum) { - if ($enumName = $this->getEnumName($column->name)) { - $valueName = null; - /** @var \UnitEnum $enumClass */ - $enumClass = $this->enums[$column->name]; - foreach ($enumClass::cases() as $enumCase) { - if ($enumCase->name === $defaultValue) { - $valueName = $enumCase->name; - } - } - if ($valueName) { - $defaultValue = $enumName . '::' . $valueName; - } else { - return 'null'; - } - } else { - $defaultValue = "'$defaultValue'"; - } - } elseif ($column->type === ColumnType::Boolean) { - if (strcasecmp($defaultValue, 'false') === 0) { - return 'false'; - } - if (strcasecmp($defaultValue, 'true') === 0) { - return 'true'; - } - return !empty($defaultValue) ? 'true' : 'false'; - } elseif ($column->type === ColumnType::Array) { - if ($defaultValue === '{}' || $defaultValue === '[]') { - return '[]'; - } - if ($decoded = json_decode($defaultValue, true)) { - return var_export($decoded, true); - } - return $defaultValue; - } else { - if ($column->type !== ColumnType::Integer && $column->type !== ColumnType::Float) { - $defaultValue = "'$defaultValue'"; - } - } - return $defaultValue; - } - - private function getEnumName(string $columnName): ?string - { - if (empty($this->enums[$columnName])) { - return null; - } - $enumClass = $this->enums[$columnName]; - if (!\in_array($enumClass, $this->useNamespaces)) { - $this->useNamespaces[] = $enumClass; - } - return substr(strrchr($enumClass, "\\"), 1); - } - - private function getIndexes(): array - { - $result = []; - foreach ($this->schema->indexes as $index) { - $properties = [ - "columns: ['" . implode("', '", $index->columns) . "']", - ]; - if ($index->isUnique) { - $properties[] = "isUnique: true"; - } - if ($index->sort) { - $sortParts = []; - foreach ($index->sort as $key => $direction) { - $sortParts[] = "'$key' => '$direction'"; - } - $properties[] = 'sort: [' . implode(', ', $sortParts) . ']'; - } - if ($index->name) { - $properties[] = "name: '" . $index->name . "'"; - } - $this->useAttributes[] = 'Index'; - $result[] = '#[Index(' . implode(', ', $properties) . ')]'; - } - return $result; - } - - private function renderTemplate(string $templateName, array $variables = []): string - { - $filePath = implode( - DIRECTORY_SEPARATOR, - [ - __DIR__, - 'Templates', - "$templateName.php", - ] - ); - if (!file_exists($filePath)) { - throw new \Exception("File `$filePath` not found"); - } - extract($variables, EXTR_SKIP); - ob_start(); - include $filePath; - return ob_get_clean(); - } -} \ No newline at end of file diff --git a/src/Generator/EnumClassBuilder.php b/src/Generator/EnumClassBuilder.php deleted file mode 100644 index b2bd580..0000000 --- a/src/Generator/EnumClassBuilder.php +++ /dev/null @@ -1,32 +0,0 @@ -cases as $case) { - $enumCases[] = new EnumCase($case); - } - $file = new PhpFile(); - $file - ->setStrictTypes() - ->addEnum($this->enumClass) - ->setCases($enumCases); - - return (string)$file; - } -} \ No newline at end of file diff --git a/src/Generator/Schema/ColumnType.php b/src/Generator/Schema/ColumnType.php deleted file mode 100644 index 5f20634..0000000 --- a/src/Generator/Schema/ColumnType.php +++ /dev/null @@ -1,15 +0,0 @@ -executeQuery("SHOW CREATE TABLE $tableName") - ->fetchAssociative(); - $this->sql = $showResult['Create Table'] ?? throw new \Exception("Table `$tableName` not found"); - } - - public function getSchema(): SQLSchema - { - $columns = $enums = $primaryKeys = $indexes = []; - $parser = new SQLParser(); - $tokens = $parser->parse($this->sql); - $table = current($tokens); - $tableName = $table['name']; - - foreach ($table['fields'] as $field) { - $name = $field['name']; - $precision = $scale = null; - $sqlType = $field['type']; - $size = !empty($field['length']) ? (int)$field['length'] : null; - $type = $this->getType($sqlType, $size); - - if ($type === ColumnType::Enum) { - $enums[$name] = new SQLEnum(name: $name, values: $field['values']); - } elseif ($type === ColumnType::Float) { - $precision = $size; - $scale = !empty($field['decimals']) ? (int)$field['decimals'] : null; - $size = null; - } - if (isset($field['default'])) { - $hasDefaultValue = true; - $defaultValue = $this->getDefaultValue($type, $field['default']); - } else { - $hasDefaultValue = false; - $defaultValue = null; - } - $column = new SQLColumn( - name: $name, - sql: $sqlType, - type: $type, - size: $size, - precision: $precision, - scale: $scale, - isNullable: !empty($field['null']), - hasDefaultValue: $hasDefaultValue, - defaultValue: $defaultValue, - isAutoincrement: !empty($field['auto_increment']), - ); - $columns[$column->name] = $column; - } - foreach ($table['indexes'] as $index) { - $indexType = strtolower($index['type']); - $cols = []; - foreach ($index['cols'] as $col) { - $colName = $col['name']; - $cols[] = $colName; - } - if ($indexType === 'primary') { - $primaryKeys = $cols; - continue; - } - $indexes[] = new SQLIndex( - name: $index['name'] ?? null, - isUnique: $indexType === 'unique', - columns: $cols, - ); - } - return new SQLSchema( - tableName: $tableName, - columns: $columns, - enums: $enums, - primaryKeys: array_unique($primaryKeys), - indexes: $indexes, - ); - } - - private function getType(string $type, ?int $size): ColumnType - { - $type = strtolower($type); - if ($type === 'tinyint' && $size === 1) { - return ColumnType::Boolean; - } - return match ($type) { - 'integer', 'int', 'smallint', 'tinyint', 'mediumint', 'bigint' => ColumnType::Integer, - 'float', 'double', 'numeric', 'decimal' => ColumnType::Float, - 'timestamp', 'datetime' => ColumnType::Datetime, - 'json', 'set' => ColumnType::Array, - 'enum' => ColumnType::Enum, - default => ColumnType::String, - }; - } - - private function getDefaultValue(ColumnType $type, mixed $value): mixed - { - if ($value === null || (is_string($value) && strcasecmp($value, 'null') === 0)) { - return null; - } - return $value; - } -} \ No newline at end of file diff --git a/src/Generator/Schema/Parsers/PostgresSchemaParser.php b/src/Generator/Schema/Parsers/PostgresSchemaParser.php deleted file mode 100644 index bcf7ebd..0000000 --- a/src/Generator/Schema/Parsers/PostgresSchemaParser.php +++ /dev/null @@ -1,221 +0,0 @@ -tableName = $tableName; - $this->informationSchemaColumns = $connection->executeQuery( - sql: PostgresSchemaParser::COLUMNS_SQL, - params: ['tableName' => $tableName], - )->fetchAllAssociative(); - $this->informationSchemaIndexes = $connection->executeQuery( - sql: PostgresSchemaParser::INDEXES_SQL, - params: ['tableName' => $tableName], - )->fetchAllAssociative(); - - if ($primaryKeySQL = PostgresSchemaParser::getPrimaryKeySQL($tableName)) { - $primaryKeys = array_map( - fn(array $row): string => $row['column_name'], - $connection->executeQuery($primaryKeySQL)->fetchAllAssociative() - ); - } else { - $primaryKeys = []; - } - $this->primaryKeys = $primaryKeys; - - $allEnumsRaw = $connection->executeQuery(PostgresSchemaParser::ALL_ENUMS_SQL)->fetchAllAssociative(); - $allEnums = []; - foreach ($allEnumsRaw as $enumRaw) { - $name = $enumRaw['enum_name']; - $value = $enumRaw['enum_value']; - if (!isset($allEnums[$name])) { - $allEnums[$name] = []; - } - $allEnums[$name][] = $value; - } - $this->allEnums = $allEnums; - } - - public function getSchema(): SQLSchema - { - $columns = $enums = []; - foreach ($this->informationSchemaColumns as $informationSchemaColumn) { - $name = $informationSchemaColumn['column_name']; - $type = $this->getType($informationSchemaColumn); - $sqlDefault = $informationSchemaColumn['column_default']; - $isNullable = $informationSchemaColumn['is_nullable'] === 'YES'; - $defaultValue = $this->getDefaultValue($type, $sqlDefault); - $hasDefaultValue = $defaultValue !== null || $isNullable; - $isAutoincrement = $sqlDefault && str_starts_with($sqlDefault, 'nextval('); - - if ($type === ColumnType::Enum) { - $udtName = $informationSchemaColumn['udt_name']; - $enums[$name] = new SQLEnum(name: $udtName, values: $this->allEnums[$udtName]); - } - $column = new SQLColumn( - name: $name, - sql: $informationSchemaColumn['udt_name'], - type: $type, - size: $this->getSize($type, $informationSchemaColumn), - precision: $this->getPrecision($type, $informationSchemaColumn), - scale: $this->getScale($type, $informationSchemaColumn), - isNullable: $isNullable, - hasDefaultValue: $hasDefaultValue, - defaultValue: $defaultValue, - isAutoincrement: $isAutoincrement, - ); - $columns[$column->name] = $column; - } - return new SQLSchema( - tableName: $this->tableName, - columns: $columns, - enums: $enums, - primaryKeys: $this->primaryKeys, - indexes: $this->parseIndexes(), - ); - } - - private function getType(array $informationSchemaColumn): ColumnType - { - $dataType = $informationSchemaColumn['data_type']; - $udtName = $informationSchemaColumn['udt_name']; - if ($dataType === 'USER-DEFINED' && !empty($this->allEnums[$udtName])) { - return ColumnType::Enum; - } - if (preg_match('/^int(\d?)$/', $udtName)) { - return ColumnType::Integer; - } - if (preg_match('/^float(\d?)$/', $udtName)) { - return ColumnType::Float; - } - $matchType = match ($udtName) { - 'numeric' => ColumnType::Float, - 'timestamp', 'timestamptz' => ColumnType::Datetime, - 'json', 'array' => ColumnType::Array, - 'bool' => ColumnType::Boolean, - default => null, - }; - return $matchType ?? ColumnType::String; - } - - private function getSize(ColumnType $type, array $informationSchemaColumn): ?int - { - if ($type === ColumnType::String) { - return $informationSchemaColumn['character_maximum_length']; - } - return null; - } - - private function getPrecision(ColumnType $type, array $informationSchemaColumn): ?int - { - if ($type !== ColumnType::Float) { - return null; - } - return $informationSchemaColumn['numeric_precision']; - } - - private function getScale(ColumnType $type, array $informationSchemaColumn): ?int - { - if ($type !== ColumnType::Float) { - return null; - } - return $informationSchemaColumn['numeric_scale']; - } - - private function getDefaultValue(ColumnType $type, ?string $sqlValue): mixed - { - if ($sqlValue === null || strcasecmp($sqlValue, 'null') === 0) { - return null; - } - if (str_starts_with($sqlValue, 'nextval(')) { - return null; - } - $parts = explode('::', $sqlValue); - return trim($parts[0], '\''); - } - - private function parseIndexes(): array - { - $result = []; - foreach ($this->informationSchemaIndexes as $informationSchemaIndex) { - $name = $informationSchemaIndex['indexname']; - $sql = $informationSchemaIndex['indexdef']; - $isUnique = stripos($sql, ' unique index ') !== false; - - if (!preg_match('/\(([`"\',\s\w]+)\)/', $sql, $columnsMatch)) { - continue; - } - $columnsRaw = array_map( - fn (string $column) => str_replace(['`', '\'', '"'], '', trim($column)), - explode(',', $columnsMatch[1]) - ); - $columns = $sort = []; - foreach ($columnsRaw as $columnRaw) { - $parts = explode(' ', $columnRaw); - $columns[] = $parts[0]; - if (!empty($parts[1])) { - $sort[$parts[0]] = strtoupper($parts[1]); - } - } - if ($columns === $this->primaryKeys) { - continue; - } - $result[] = new SQLIndex( - name: $name, - isUnique: $isUnique, - columns: $columns, - ); - } - return $result; - } -} \ No newline at end of file diff --git a/src/Generator/Schema/Parsers/SQLiteSchemaParser.php b/src/Generator/Schema/Parsers/SQLiteSchemaParser.php deleted file mode 100644 index c11c49a..0000000 --- a/src/Generator/Schema/Parsers/SQLiteSchemaParser.php +++ /dev/null @@ -1,247 +0,0 @@ -tableSql = $connection->executeQuery( - sql: self::TABLE_SQL, - params: ['tableName' => $tableName], - )->fetchOne(); - $this->indexesSql = $connection->executeQuery( - sql: self::INDEXES_SQL, - params: ['tableName' => $tableName], - )->fetchFirstColumn(); - } - - public function getSchema(): SQLSchema - { - $columns = $enums = $primaryKeys = []; - $columnsStarted = false; - $tableName = ''; - $lines = array_map( - fn ($line) => trim(preg_replace("/\s+/", " ", $line)), - explode("\n", $this->tableSql), - ); - for ($i = 0; $i < count($lines); $i++) { - $line = $lines[$i]; - if (!$line) { - continue; - } - if (!$tableName && preg_match(self::TABLE_NAME_PATTERN, $line, $matches)) { - $tableName = $matches[1]; - } - if (!$columnsStarted) { - if (str_starts_with($line, '(') || str_ends_with($line, '(')) { - $columnsStarted = true; - } - continue; - } - if ($line === ')') { - break; - } - if (!str_ends_with($line, ',')) { - if (!empty($lines[$i + 1]) && !str_starts_with($lines[$i + 1], ')')) { - $lines[$i + 1] = $line . ' ' . $lines[$i + 1]; - continue; - } - } - if ($column = $this->parseSQLColumn($line)) { - $columns[$column->name] = $column; - } - $primaryKeys = array_merge($primaryKeys, $this->parsePrimaryKeys($line)); - if ($enum = $this->parseEnum($line)) { - $enums[$column?->name ?? $enum->name] = $enum; - } - } - return new SQLSchema( - tableName: $tableName, - columns: $columns, - enums: $enums, - primaryKeys: array_unique($primaryKeys), - indexes: $this->getIndexes(), - ); - } - - private function parseSQLColumn(string $sqlLine): ?SQLColumn - { - if (!preg_match(self::COLUMN_PATTERN, $sqlLine, $matches)) { - return null; - } - $name = $matches[1]; - $rawType = $matches[2]; - $rawTypeParams = !empty($matches[4]) ? str_replace(' ', '', $matches[4]) : null; - $type = $this->getColumnType($rawType) ?? ColumnType::String; - $hasDefaultValue = stripos($sqlLine, ' default ') !== false; - return new SQLColumn( - name: $name, - sql: $sqlLine, - type: $type, - size: $this->getColumnSize($type, $rawTypeParams), - precision: $this->getColumnPrecision($type, $rawTypeParams), - scale: $this->getScale($type, $rawTypeParams), - isNullable: stripos($sqlLine, ' not null') === false, - hasDefaultValue: $hasDefaultValue, - defaultValue: $hasDefaultValue ? $this->getDefaultValue($sqlLine) : null, - isAutoincrement: stripos($sqlLine, ' autoincrement') !== false, - ); - } - - private function getColumnType(string $rawType): ?ColumnType - { - if (!preg_match('/^([a-zA-Z]+).*/', $rawType, $matches)) { - return null; - } - $type = strtolower($matches[1]); - return match ($type) { - 'integer', 'int' => ColumnType::Integer, - 'real' => ColumnType::Float, - 'timestamp' => ColumnType::Datetime, - 'enum' => ColumnType::Enum, - default => ColumnType::String, - }; - } - - private function getColumnSize(ColumnType $type, ?string $typeParams): ?int - { - if ($type !== ColumnType::String || !$typeParams) { - return null; - } - return (int)$typeParams; - } - - private function getColumnPrecision(ColumnType $type, ?string $typeParams): ?int - { - if ($type !== ColumnType::Float || !$typeParams) { - return null; - } - $parts = explode(',', $typeParams); - return (int)$parts[0]; - } - - private function getScale(ColumnType $type, ?string $typeParams): ?int - { - if ($type !== ColumnType::Float || !$typeParams) { - return null; - } - $parts = explode(',', $typeParams); - return !empty($parts[1]) ? (int)$parts[1] : null; - } - - private function getDefaultValue(string $sqlLine): mixed - { - $sqlLine = $this->cleanCheckEnum($sqlLine); - if (preg_match('/default\s+\'(.*)\'/iu', $sqlLine, $matches)) { - return $matches[1]; - } elseif (preg_match('/default\s+([\w.]+)/iu', $sqlLine, $matches)) { - $defaultValue = $matches[1]; - if (strtolower($defaultValue) === 'null') { - return null; - } - return $defaultValue; - } - return null; - } - - private function parsePrimaryKeys(string $sqlLine): array - { - if (preg_match(self::COLUMN_PATTERN, $sqlLine, $matches)) { - $name = $matches[1]; - return stripos($sqlLine, ' primary key') !== false ? [$name] : []; - } - if (!preg_match(self::CONSTRAINT_PATTERN, $sqlLine, $matches) - && !preg_match(self::PRIMARY_KEY_PATTERN, $sqlLine, $matches)) { - return []; - } - $primaryColumnsRaw = $matches[1]; - $primaryColumnsRaw = str_replace(['\'', '"', '`', ' '], '', $primaryColumnsRaw); - return explode(',', $primaryColumnsRaw); - } - - private function parseEnum(string $sqlLine): ?SQLEnum - { - if (!preg_match(self::ENUM_PATTERN, $sqlLine, $matches)) { - return null; - } - $name = $matches[1]; - $values = []; - $sqlValues = array_map('trim', explode(',', $matches[2])); - foreach ($sqlValues as $value) { - $value = trim($value); - if (str_starts_with($value, '\'')) { - $value = trim($value, '\''); - } elseif (str_starts_with($value, '"')) { - $value = trim($value, '"'); - } - $values[] = $value; - } - return new SQLEnum(name: $name, values: $values); - } - - /** - * @return SQLIndex[] - */ - private function getIndexes(): array - { - $result = []; - foreach ($this->indexesSql as $indexSql) { - if (!$indexSql) continue; - $indexSql = trim(str_replace("\n", " ", $indexSql)); - $indexSql = preg_replace("/\s+/", " ", $indexSql); - if (!preg_match('/index\s+(?:`|\"|\')?(\w+)(?:`|\"|\')?/i', $indexSql, $nameMatch)) { - continue; - } - $name = $nameMatch[1]; - if (!preg_match('/\(([`"\',\s\w]+)\)/', $indexSql, $columnsMatch)) { - continue; - } - $columnsRaw = array_map( - fn (string $column) => str_replace(['`', '\'', '"'], '', trim($column)), - explode(',', $columnsMatch[1]) - ); - $columns = $sort = []; - foreach ($columnsRaw as $columnRaw) { - $parts = explode(' ', $columnRaw); - $columns[] = $parts[0]; - if (!empty($parts[1])) { - $sort[$parts[0]] = strtolower($parts[1]); - } - } - $result[] = new SQLIndex( - name: $name, - isUnique: stripos($indexSql, ' unique index ') !== false, - columns: $columns, - sort: $sort, - ); - } - return $result; - } - - private function cleanCheckEnum(string $sqlLine): string - { - return preg_replace('/ check \(\"\w+\" IN \(.+\)\)/i', '', $sqlLine); - } -} \ No newline at end of file diff --git a/src/Generator/Schema/SQLColumn.php b/src/Generator/Schema/SQLColumn.php deleted file mode 100644 index 00ad7d3..0000000 --- a/src/Generator/Schema/SQLColumn.php +++ /dev/null @@ -1,45 +0,0 @@ -type !== ColumnType::String) { - return true; - } - if ($this->size === null) { - return true; - } - return $this->size === 255; - } - - public function getColumnAttributeProperties(): array - { - $result = []; - if ($this->size && !$this->sizeIsDefault()) { - $result[] = 'size: ' . $this->size; - } - if ($this->precision) { - $result[] = 'precision: ' . $this->precision; - } - if ($this->scale) { - $result[] = 'scale: ' . $this->scale; - } - return $result; - } -} \ No newline at end of file diff --git a/src/Generator/Schema/SQLEnum.php b/src/Generator/Schema/SQLEnum.php deleted file mode 100644 index ba726df..0000000 --- a/src/Generator/Schema/SQLEnum.php +++ /dev/null @@ -1,11 +0,0 @@ -getDriver(); - if ($driver instanceof Driver\AbstractSQLiteDriver) { - $parser = new SQLiteSchemaParser($connection, $tableName); - return $parser->getSchema(); - } elseif ($driver instanceof Driver\AbstractMySQLDriver) { - $parser = new MySQLSchemaParser($connection, $tableName); - return $parser->getSchema(); - } elseif ($driver instanceof Driver\AbstractPostgreSQLDriver) { - $parser = new PostgresSchemaParser($connection, $tableName); - return $parser->getSchema(); - } else { - throw new \Exception("Driver `" . $driver::class . "` is not yet supported"); - } - } - - public function isPrimaryKey(string $name): bool - { - return \in_array($name, $this->primaryKeys); - } -} \ No newline at end of file diff --git a/src/Generator/TableClassBuilder.php b/src/Generator/TableClassBuilder.php deleted file mode 100644 index ba840f8..0000000 --- a/src/Generator/TableClassBuilder.php +++ /dev/null @@ -1,77 +0,0 @@ -file - ->setStrictTypes() - ->addNamespace(ClassHelper::extractNamespace($this->tableClass)) - ->addUse(AbstractTable::class) - ->addUse(TableConfig::class) - ->addUse($this->schema->class) - ->addClass(ClassHelper::extractShortName($this->tableClass)) - ->setExtends(AbstractTable::class) - ->setMethods($this->getMethods()); - } - - private function getMethods(): array - { - return array_filter([ - $this->generateGetConfig(), - $this->generateFindOne(), - $this->generateFindAll(), - $this->generateCountAll(), - ]); - } - - protected function generateFindOne(): ?Method - { - $primaryColumns = array_map( - fn(string $key): AbstractColumn => $this->schema->getColumn($key) ?? throw new \Exception("Primary key column `$key` not found in entity."), - $this->tableConfig->primaryKeys - ); - if (count($this->tableConfig->primaryKeys) === 1) { - $body = 'return $this->createEntity($this->findByPkInternal(' . $this->buildVarsList($this->tableConfig->primaryKeys) . '));'; - } else { - $body = 'return $this->createEntity($this->findOneInternal(' . $this->buildVarsList($this->tableConfig->primaryKeys) . '));'; - } - $method = (new Method('findByPk')) - ->setPublic() - ->setReturnType($this->schema->class) - ->setReturnNullable() - ->setBody($body); - $this->addMethodParameters($method, $primaryColumns); - return $method; - } - - protected function generateFindAll(): Method - { - return (new Method('findAll')) - ->setPublic() - ->setComment('@return ' . $this->entityClassShortName . '[]') - ->setReturnType('array') - ->setBody('return $this->createEntities($this->findAllInternal());'); - } - - protected function generateCountAll(): Method - { - return (new Method('countAll')) - ->setPublic() - ->setReturnType('int') - ->setBody('return $this->countAllInternal();'); - } -} \ No newline at end of file diff --git a/src/Generator/Templates/EntityTemplate.php b/src/Generator/Templates/EntityTemplate.php deleted file mode 100644 index 9384d5b..0000000 --- a/src/Generator/Templates/EntityTemplate.php +++ /dev/null @@ -1,41 +0,0 @@ - - - -namespace ; - - -use Composite\DB\Attributes\{}; - - -use ; - - -#[Table(connection: '', name: '')] - - - - -class extends AbstractEntity -{ - - use ; - - - - - - - - ; - - - public function __construct( - - - - - - , - - ) {} -} diff --git a/src/Helpers/ClassHelper.php b/src/Helpers/ClassHelper.php deleted file mode 100644 index fa7b7f8..0000000 --- a/src/Helpers/ClassHelper.php +++ /dev/null @@ -1,18 +0,0 @@ - Date: Sat, 6 May 2023 10:02:03 +0100 Subject: [PATCH 04/69] Update Entity version dependency to ^0.1.4 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5d8f909..77acd90 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "php": "^8.1", "ext-pdo": "*", "psr/simple-cache": "1 - 3", - "compositephp/entity": "dev-master", + "compositephp/entity": "^0.1.4", "doctrine/dbal": "^3.5" }, "require-dev": { From 5c4f41b58e8541d573307d0f44d0e0ee0c93fc90 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 13 May 2023 11:23:02 +0100 Subject: [PATCH 05/69] Change configure method to loadConfigs that returns array of configs --- src/ConnectionManager.php | 51 ++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/ConnectionManager.php b/src/ConnectionManager.php index 8f7c4c6..dfed177 100644 --- a/src/ConnectionManager.php +++ b/src/ConnectionManager.php @@ -40,27 +40,7 @@ public static function getConnection(string $name, ?Configuration $config = null private static function getConnectionParams(string $name): array { if (self::$configs === null) { - $configFile = getenv(self::CONNECTIONS_CONFIG_ENV_VAR, true) ?: ($_ENV[self::CONNECTIONS_CONFIG_ENV_VAR] ?? false); - if (empty($configFile)) { - throw new DbException(sprintf( - 'ConnectionManager is not configured, please call ConnectionManager::configure() method or setup putenv(\'%s=/path/to/config/file.php\') variable', - self::CONNECTIONS_CONFIG_ENV_VAR - )); - } - if (!file_exists($configFile)) { - throw new DbException(sprintf( - 'Connections config file `%s` does not exist', - $configFile - )); - } - $configContent = require_once $configFile; - if (empty($configContent) || !is_array($configContent)) { - throw new DbException(sprintf( - 'Connections config file `%s` should return array of connection params', - $configFile - )); - } - self::configure($configContent); + self::$configs = self::loadConfigs(); } return self::$configs[$name] ?? throw new DbException("Connection config `$name` not found"); } @@ -68,17 +48,38 @@ private static function getConnectionParams(string $name): array /** * @throws DbException */ - private static function configure(array $configs): void + private static function loadConfigs(): array { - foreach ($configs as $name => $connectionConfig) { + $configFile = getenv(self::CONNECTIONS_CONFIG_ENV_VAR, true) ?: ($_ENV[self::CONNECTIONS_CONFIG_ENV_VAR] ?? false); + if (empty($configFile)) { + throw new DbException(sprintf( + 'ConnectionManager is not configured, please define ENV variable `%s`', + self::CONNECTIONS_CONFIG_ENV_VAR + )); + } + if (!file_exists($configFile)) { + throw new DbException(sprintf( + 'Connections config file `%s` does not exist', + $configFile + )); + } + $configFileContent = require_once $configFile; + if (empty($configFileContent) || !is_array($configFileContent)) { + throw new DbException(sprintf( + 'Connections config file `%s` should return array of connection params', + $configFile + )); + } + $result = []; + foreach ($configFileContent as $name => $connectionConfig) { if (empty($name) || !is_string($name)) { throw new DbException('Config has invalid connection name ' . var_export($name, true)); } if (empty($connectionConfig) || !is_array($connectionConfig)) { throw new DbException("Connection `$name` has invalid connection params"); } - self::$configs[$name] = $connectionConfig; + $result[$name] = $connectionConfig; } - self::$configs = $configs; + return $result; } } \ No newline at end of file From c9dafe8d34cc216d085b78d543cce9a99d3c9e24 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 13 May 2023 11:46:30 +0100 Subject: [PATCH 06/69] Minor optimizations and adapt to phpstan level 8 --- src/AbstractCachedTable.php | 29 ++++++++++++++++++++++---- src/AbstractTable.php | 39 +++++++++++++++++++++++++++++++++++ src/Attributes/PrimaryKey.php | 2 +- src/Attributes/Table.php | 2 +- src/CombinedTransaction.php | 1 + src/ConnectionManager.php | 4 ++++ src/TableConfig.php | 11 +++++----- 7 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index 1f0906f..e6370e4 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -37,7 +37,7 @@ public function save(AbstractEntity &$entity): void /** * @param AbstractEntity[] $entities - * @return AbstractEntity[] + * @return AbstractEntity[] * @throws \Throwable */ public function saveMany(array $entities): array @@ -128,6 +128,7 @@ protected function findOneCachedInternal(array $condition, null|int|\DateInterva } /** + * @param array $whereParams * @param array|string $orderBy * @return array[] */ @@ -175,6 +176,12 @@ protected function getCached(string $cacheKey, callable $dataCallback, null|int| return $data; } + /** + * @param mixed[] $ids + * @param int|\DateInterval|null $ttl + * @return array> + * @throws \Psr\SimpleCache\InvalidArgumentException + */ protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $ttl = null): array { $result = $cacheKeys = $foundIds = []; @@ -199,6 +206,10 @@ protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $t return $result; } + /** + * @param string|int|array|AbstractEntity $keyOrEntity + * @throws \Composite\Entity\Exceptions\EntityException + */ protected function getOneCacheKey(string|int|array|AbstractEntity $keyOrEntity): string { if (!is_array($keyOrEntity)) { @@ -209,6 +220,10 @@ protected function getOneCacheKey(string|int|array|AbstractEntity $keyOrEntity): return $this->buildCacheKey('o', $condition ?: 'one'); } + /** + * @param array $whereParams + * @param array|string $orderBy + */ protected function getListCacheKey( string $whereString = '', array $whereParams = [], @@ -225,6 +240,9 @@ protected function getListCacheKey( ); } + /** + * @param array $whereParams + */ protected function getCountCacheKey( string $whereString = '', array $whereParams = [], @@ -247,7 +265,7 @@ protected function buildCacheKey(mixed ...$parts): string if ($this->config->isSoftDelete && array_key_exists('deleted_at', $part)) { unset($part['deleted_at']); } - $string = json_encode($part); + $string = json_encode($part, JSON_THROW_ON_ERROR); } else { $string = strval($part); } @@ -273,10 +291,13 @@ private function formatStringForCacheKey(string $string): string { $string = mb_strtolower($string); $string = str_replace(['!=', '<>', '>', '<', '='], ['_not_', '_not_', '_gt_', '_lt_', '_eq_'], $string); - $string = preg_replace('/\W/', '_', $string); - return trim(preg_replace('/_+/', '_', $string), '_'); + $string = (string)preg_replace('/\W/', '_', $string); + return trim((string)preg_replace('/_+/', '_', $string), '_'); } + /** + * @param array $whereParams + */ private function prepareWhereKey(string $whereString, array $whereParams): ?string { if (!$whereString) { diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 4a4c1c4..6d72b16 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -148,6 +148,10 @@ public function deleteMany(array $entities): bool }); } + /** + * @param array $whereParams + * @throws \Doctrine\DBAL\Exception + */ protected function countAllInternal(string $whereString = '', array $whereParams = []): int { $query = $this->select('COUNT(*)'); @@ -161,12 +165,22 @@ protected function countAllInternal(string $whereString = '', array $whereParams return intval($query->executeQuery()->fetchOne()); } + /** + * @return array|null + * @throws EntityException + * @throws \Doctrine\DBAL\Exception + */ protected function findByPkInternal(mixed $pk): ?array { $where = $this->getPkCondition($pk); return $this->findOneInternal($where); } + /** + * @param array $where + * @return array|null + * @throws \Doctrine\DBAL\Exception + */ protected function findOneInternal(array $where): ?array { $query = $this->select(); @@ -175,6 +189,12 @@ protected function findOneInternal(array $where): ?array return $query->fetchAssociative() ?: null; } + /** + * @param array $whereParams + * @param array|string $orderBy + * @return array + * @throws \Doctrine\DBAL\Exception + */ protected function findAllInternal( string $whereString = '', array $whereParams = [], @@ -224,6 +244,9 @@ final protected function createEntity(mixed $data): mixed } } + /** + * @return AbstractEntity[] + */ final protected function createEntities(mixed $data): array { if (!is_array($data)) { @@ -245,6 +268,11 @@ final protected function createEntities(mixed $data): array return $result; } + /** + * @param int|string|array|AbstractEntity $data + * @return array + * @throws EntityException + */ protected function getPkCondition(int|string|array|AbstractEntity $data): array { $condition = []; @@ -267,6 +295,9 @@ protected function getPkCondition(int|string|array|AbstractEntity $data): array return $condition; } + /** + * @param array|QueryBuilder $query + */ protected function enrichCondition(array|QueryBuilder &$query): void { if ($this->config->isSoftDelete) { @@ -288,6 +319,9 @@ protected function select(string $select = '*'): QueryBuilder return (clone $this->selectQuery)->select($select); } + /** + * @param array $where + */ private function buildWhere(QueryBuilder $query, array $where): void { foreach ($where as $column => $value) { @@ -300,6 +334,11 @@ private function buildWhere(QueryBuilder $query, array $where): void } } + /** + * @param array $data + * @return array + * @throws \Doctrine\DBAL\Exception + */ private function formatData(array $data): array { foreach ($data as $columnName => $value) { diff --git a/src/Attributes/PrimaryKey.php b/src/Attributes/PrimaryKey.php index a9540bc..d8db396 100644 --- a/src/Attributes/PrimaryKey.php +++ b/src/Attributes/PrimaryKey.php @@ -2,7 +2,7 @@ namespace Composite\DB\Attributes; -#[\Attribute] +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_PARAMETER)] class PrimaryKey { public function __construct( diff --git a/src/Attributes/Table.php b/src/Attributes/Table.php index 8f09858..3c8eefc 100644 --- a/src/Attributes/Table.php +++ b/src/Attributes/Table.php @@ -2,7 +2,7 @@ namespace Composite\DB\Attributes; -#[\Attribute] +#[\Attribute(\Attribute::TARGET_CLASS)] class Table { public function __construct( diff --git a/src/CombinedTransaction.php b/src/CombinedTransaction.php index e5f2671..ec2598e 100644 --- a/src/CombinedTransaction.php +++ b/src/CombinedTransaction.php @@ -80,6 +80,7 @@ public function commit(): void /** * Pessimistic lock + * @param string[] $keyParts * @throws DbException */ public function lock(CacheInterface $cache, array $keyParts, int $duration = 10): void diff --git a/src/ConnectionManager.php b/src/ConnectionManager.php index dfed177..aeb463f 100644 --- a/src/ConnectionManager.php +++ b/src/ConnectionManager.php @@ -12,7 +12,9 @@ class ConnectionManager { private const CONNECTIONS_CONFIG_ENV_VAR = 'CONNECTIONS_CONFIG_FILE'; + /** @var array>|null */ private static ?array $configs = null; + /** @var array */ private static array $connections = []; /** @@ -35,6 +37,7 @@ public static function getConnection(string $name, ?Configuration $config = null } /** + * @return array * @throws DbException */ private static function getConnectionParams(string $name): array @@ -46,6 +49,7 @@ private static function getConnectionParams(string $name): array } /** + * @return array> * @throws DbException */ private static function loadConfigs(): array diff --git a/src/TableConfig.php b/src/TableConfig.php index ef2bacb..a145ffb 100644 --- a/src/TableConfig.php +++ b/src/TableConfig.php @@ -9,6 +9,10 @@ class TableConfig { + /** + * @param class-string $entityClass + * @param string[] $primaryKeys + */ public function __construct( public readonly string $connectionName, public readonly string $tableName, @@ -26,12 +30,7 @@ public function __construct( public static function fromEntitySchema(Schema $schema): TableConfig { /** @var Attributes\Table|null $tableAttribute */ - $tableAttribute = null; - foreach ($schema->attributes as $attribute) { - if ($attribute instanceof Attributes\Table) { - $tableAttribute = $attribute; - } - } + $tableAttribute = $schema->getFirstAttributeByClass(Attributes\Table::class); if (!$tableAttribute) { throw new EntityException(sprintf( 'Attribute `%s` not found in Entity `%s`', From a047a0bc2b073b3456a00aa20f318220744a7856 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 14 May 2023 23:52:58 +0100 Subject: [PATCH 07/69] Update documentation --- README.md | 2 +- doc/code-generators.md | 99 ++++++++++++++++++----- doc/migrations.md | 164 ++++++++++++++++++-------------------- doc/sync_illustration.png | Bin 0 -> 62723 bytes 4 files changed, 161 insertions(+), 104 deletions(-) create mode 100644 doc/sync_illustration.png diff --git a/README.md b/README.md index c4f3f59..a3802fd 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ just use native php class syntax. It also has many popular features such as: * **Query Builder** - build your queries with constructor, based on [doctrine/dbal](https://github.com/doctrine/dbal) -* **Migrations** - based on [doctrine/migrations](https://github.com/doctrine/migrations) +* **Migrations** - synchronise your php entities with database tables But there is 1 sacrifice for all these features - there is no support for relations in Composite DB. Its too much uncontrollable magic and hidden bottlenecks with "JOINs" and its not possible to implement automatic caching with diff --git a/doc/code-generators.md b/doc/code-generators.md index 276f294..865c987 100644 --- a/doc/code-generators.md +++ b/doc/code-generators.md @@ -1,27 +1,90 @@ # Code generators -Before start, you need to [configure](configuration.md#configure-console-commands) code generators. +Composite Sync is a powerful and flexible PHP library designed to streamline and automate the synchronization process between SQL database table structures and PHP entity classes. +By providing a set of easy-to-use tools, Composite Sync eliminates the need for manual synchronization and helps you maintain the consistency of your application's data model. -## Entity class generator -Arguments: -1. `db` - DatabaseManager database name -2. `table` - SQL table name -3. `entity` - Full classname of new entity -4. `--force` - option if existing file should be overwritten +## Supported Databases +- MySQL +- Postgres +- SQLite + +## Getting Started + +To begin using Composite Sync in your project, follow these steps: + +### 1. Install package via composer: + ```shell + $ composer require compositephp/sync + ``` +### 2. Configure connections +You need to configure ConnectionManager, see instructions [here](configuration.md) + +### 3. Configure commands + +Add [symfony/console](https://symfony.com/doc/current/components/console.html) commands to your application: +- Composite\Sync\Commands\MigrateCommand +- Composite\Sync\Commands\MigrateNewCommand +- Composite\Sync\Commands\MigrateDownCommand + +Here is an example of a minimalist, functional PHP file if you don't have configured symfony/console: + +```php +addCommands([ + new Commands\GenerateEntityCommand(), + new Commands\GenerateTableCommand(), +]); +$app->run(); +``` +## Available commands + +* ### composite:generate-entity + +The command examines the specific SQL table and generates an [Composite\Entity\AbstractEntity](https://github.com/compositephp/entity) PHP class. +This class embodies the table structure using native PHP syntax, thereby representing the original SQL table in a more PHP-friendly format. -Example: ```shell -$ php console.php composite-db:generate-entity dbName Users 'App\User' --force +php cli.php composite:generate-entity connection_name TableName 'App\Models\EntityName' ``` -## Table class generator -Arguments: -1. `entity` - Entity full class name -2. `table` - Table full class name -3. `--cached` - Option if cached version of table class should be generated -4. `--force` - Option if existing file should be overwritten +| Argument | Required | Description | +|------------|----------|------------------------------------------------------| +| connection | Yes | Name of connection from connection config file | +| table | Yes | Name of SQL table | +| entity | Yes | Full classname of the class that needs to be created | + +Options: + +| Option | Description | +|---------|-------------------------| +| --force | Overwrite existing file | + +* ### composite:generate-table + +The command examines the specific Entity and generates a [Table](https://github.com/compositephp/db) PHP class. +This class acts as a gateway to a specific SQL table, providing user-friendly CRUD tools for interacting with SQL right off the bat. -Example: ```shell -$ php console.php composite-db:generate-table 'App\User' 'App\UsersTable' -``` \ No newline at end of file +php cli.php composite:generate-table connection_name TableName 'App\Models\EntityName' +``` + +| Argument | Required | Description | +|-----------|----------|-----------------------------------------------| +| entity | Yes | Full Entity classname | +| table | No | Full Table classname that needs to be created | + +Options: + +| Option | Description | +|----------|--------------------------------------------| +| --cached | Generate cached version of PHP Table class | +| --force | Overwrite existing file | \ No newline at end of file diff --git a/doc/migrations.md b/doc/migrations.md index da0fe68..b3247ae 100644 --- a/doc/migrations.md +++ b/doc/migrations.md @@ -2,96 +2,90 @@ > **_NOTE:_** This is experimental feature -Migrations used a bridge to [doctrine/migrations](https://github.com/doctrine/migrations) package. -If you are not familiar with it, please read documentation before using composite bridge. - -1. Install package: - ```shell - $ composer require compositephp/doctrine-migrations - ``` - -2. Configure bridge: - ```php - $bridge = new \Composite\DoctrineMigrations\SchemaProviderBridge( - entityDirs: [ - '/path/to/your/src', //path to your source code, where bridge will search for entities - ], - connectionName: 'sqlite', //only entities with this connection name will be affected - connection: $connection, //Doctrine\DBAL\Connection instance - ); - ``` - -3. Inject bridge into `\Doctrine\Migrations\DependencyFactory` as `\Doctrine\Migrations\Provider\SchemaProvider` -instance. - ```php - $dependencyFactory->setDefinition(SchemaProvider::class, static fn () => $bridge); - ``` - -Full example: +Code generation is a key feature of the Composite Sync package. +This enables you to generate Entity classes directly from SQL tables, thereby enabling a literal reflection of the SQL table schema into native PHP classes. + +## Supported Databases +- MySQL +- Postgres (Coming soon) +- SQLite (Coming soon) + +## Getting Started + +To begin using Composite Sync in your project, follow these steps: + +### 1. Install package via composer: + ```shell + $ composer require compositephp/sync + ``` +### 2. Configure connections +You need to configure ConnectionManager, see instructions [here](configuration.md) + +### 3. Configure commands + +Add [symfony/console](https://symfony.com/doc/current/components/console.html) commands to your application: +- Composite\Sync\Commands\GenerateEntityCommand +- Composite\Sync\Commands\GenerateTableCommand + +Here is an example of a minimalist, functional PHP file: + ```php 'pdo_mysql', - 'dbname' => 'test', - 'user' => 'test', - 'password' => 'test', - 'host' => '127.0.0.1', +$app = new Application(); +$app->addCommands([ + new Commands\GenerateEntityCommand(), + new Commands\GenerateTableCommand(), ]); +$app->run(); +``` +## Available commands + +* ### composite:generate-entity + +The command examines the specific SQL table and generates an [Composite\Entity\AbstractEntity](https://github.com/compositephp/entity) PHP class. +This class embodies the table structure using native PHP syntax, thereby representing the original SQL table in a more PHP-friendly format. + +```shell +php cli.php composite:generate-entity connection_name TableName 'App\Models\EntityName' +``` + +| Argument | Required | Description | +|------------|----------|------------------------------------------------------| +| connection | Yes | Name of connection from connection config file | +| table | Yes | Name of SQL table | +| entity | Yes | Full classname of the class that needs to be created | + +Options: + +| Option | Description | +|---------|-------------------------| +| --force | Overwrite existing file | + +* ### composite:generate-table + +The command examines the specific Entity and generates a [Table](https://github.com/compositephp/db) PHP class. +This class acts as a gateway to a specific SQL table, providing user-friendly CRUD tools for interacting with SQL right off the bat. + +```shell +php cli.php composite:generate-table connection_name TableName 'App\Models\EntityName' +``` + +| Argument | Required | Description | +|-----------|----------|-----------------------------------------------| +| entity | Yes | Full Entity classname | +| table | No | Full Table classname that needs to be created | + +Options: -$configuration = new Configuration(); - -$configuration->addMigrationsDirectory('Composite\DoctrineMigrations\Tests\runtime\migrations', __DIR__ . '/tests/runtime/migrations'); -$configuration->setAllOrNothing(true); -$configuration->setCheckDatabasePlatform(false); - -$storageConfiguration = new TableMetadataStorageConfiguration(); -$storageConfiguration->setTableName('doctrine_migration_versions'); - -$configuration->setMetadataStorageConfiguration($storageConfiguration); - -$dependencyFactory = DependencyFactory::fromConnection( - new ExistingConfiguration($configuration), - new ExistingConnection($connection) -); - -$bridge = new \Composite\DoctrineMigrations\SchemaProviderBridge( - entityDirs: [ - __DIR__ . '/src', - ], - connectionName: 'mysql', - connection: $connection, -); -$dependencyFactory->setDefinition(SchemaProvider::class, static fn () => $bridge); - -$cli = new Application('Migrations'); -$cli->setCatchExceptions(true); - -$cli->addCommands(array( - new Command\DumpSchemaCommand($dependencyFactory), - new Command\ExecuteCommand($dependencyFactory), - new Command\GenerateCommand($dependencyFactory), - new Command\LatestCommand($dependencyFactory), - new Command\ListCommand($dependencyFactory), - new Command\MigrateCommand($dependencyFactory), - new Command\DiffCommand($dependencyFactory), - new Command\RollupCommand($dependencyFactory), - new Command\StatusCommand($dependencyFactory), - new Command\SyncMetadataCommand($dependencyFactory), - new Command\VersionCommand($dependencyFactory), -)); - -$cli->run(); -``` \ No newline at end of file +| Option | Description | +|----------|--------------------------------------------| +| --cached | Generate cached version of PHP Table class | +| --force | Overwrite existing file | \ No newline at end of file diff --git a/doc/sync_illustration.png b/doc/sync_illustration.png new file mode 100644 index 0000000000000000000000000000000000000000..94c451a362331b53657587942b825015c6f2e841 GIT binary patch literal 62723 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 NqN#f!|D4s`{{ypaC9ePg literal 0 HcmV?d00001 From 6c04f620bca895e63af87f1bd1366508e19498e8 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Mon, 15 May 2023 00:12:30 +0100 Subject: [PATCH 08/69] Update documentation --- doc/code-generators.md | 4 ++-- doc/migrations.md | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/code-generators.md b/doc/code-generators.md index 865c987..ce53614 100644 --- a/doc/code-generators.md +++ b/doc/code-generators.md @@ -1,7 +1,7 @@ # Code generators -Composite Sync is a powerful and flexible PHP library designed to streamline and automate the synchronization process between SQL database table structures and PHP entity classes. -By providing a set of easy-to-use tools, Composite Sync eliminates the need for manual synchronization and helps you maintain the consistency of your application's data model. +Code generation is on of key features of the Composite Sync package. +This enables you to generate Entity classes directly from SQL tables, thereby enabling a literal reflection of the SQL table schema into native PHP classes. ## Supported Databases - MySQL diff --git a/doc/migrations.md b/doc/migrations.md index b3247ae..d6180e4 100644 --- a/doc/migrations.md +++ b/doc/migrations.md @@ -2,8 +2,10 @@ > **_NOTE:_** This is experimental feature -Code generation is a key feature of the Composite Sync package. -This enables you to generate Entity classes directly from SQL tables, thereby enabling a literal reflection of the SQL table schema into native PHP classes. +Migrations enable you to maintain your database schema within your PHP entity classes. +Any modification made in your class triggers the generation of migration files. +These files execute SQL queries which synchronize the schema from the PHP class to the corresponding SQL table. +This mechanism ensures consistent alignment between your codebase and the database structure. ## Supported Databases - MySQL @@ -12,7 +14,7 @@ This enables you to generate Entity classes directly from SQL tables, thereby en ## Getting Started -To begin using Composite Sync in your project, follow these steps: +To begin using migrations you need to add Composite Sync package into your project and configure it: ### 1. Install package via composer: ```shell From 6fb07662bb03d5a97e73206519f8db94e56e5f66 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Mon, 15 May 2023 00:13:25 +0100 Subject: [PATCH 09/69] Update entity version in composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 77acd90..5f435c3 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "php": "^8.1", "ext-pdo": "*", "psr/simple-cache": "1 - 3", - "compositephp/entity": "^0.1.4", + "compositephp/entity": "^0.1.5", "doctrine/dbal": "^3.5" }, "require-dev": { From 31917ddd8666c0f6cd31fb96617f771ea0059417 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 20 May 2023 15:02:27 +0100 Subject: [PATCH 10/69] Add UpdatedAt trait, remove hidden logic behind SoftDelete trait --- src/AbstractCachedTable.php | 3 -- src/AbstractTable.php | 51 ++++++------------ src/Helpers/DateTimeHelper.php | 2 + src/TableConfig.php | 41 +++++++++------ src/Traits/UpdatedAt.php | 8 +++ tests/Table/AbstractTableTest.php | 52 ++----------------- tests/Table/AutoIncrementTableTest.php | 14 ++++- tests/Table/CompositeTableTest.php | 21 ++++---- tests/Table/TableConfigTest.php | 4 +- tests/Table/UniqueTableTest.php | 23 ++++---- .../Entities/TestUpdatedAtEntity.php | 21 ++++++++ tests/TestStand/Tables/TestCompositeTable.php | 2 +- tests/TestStand/Tables/TestUpdateAtTable.php | 41 +++++++++++++++ tests/Traits/UpdatedAtTest.php | 26 ++++++++++ 14 files changed, 181 insertions(+), 128 deletions(-) create mode 100644 src/Traits/UpdatedAt.php create mode 100644 tests/TestStand/Entities/TestUpdatedAtEntity.php create mode 100644 tests/TestStand/Tables/TestUpdateAtTable.php create mode 100644 tests/Traits/UpdatedAtTest.php diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index e6370e4..92d124d 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -262,9 +262,6 @@ protected function buildCacheKey(mixed ...$parts): string $formattedParts = []; foreach ($parts as $part) { if (is_array($part)) { - if ($this->config->isSoftDelete && array_key_exists('deleted_at', $part)) { - unset($part['deleted_at']); - } $string = json_encode($part, JSON_THROW_ON_ERROR); } else { $string = strval($part); diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 6d72b16..ad54d38 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -47,6 +47,9 @@ public function save(AbstractEntity &$entity): void $this->config->checkEntity($entity); if ($entity->isNew()) { $connection = $this->getConnection(); + if ($this->config->hasUpdatedAt() && property_exists($entity, 'updated_at') && $entity->updated_at === null) { + $entity->updated_at = new \DateTimeImmutable(); + } $insertData = $this->formatData($entity->toArray()); $this->getConnection()->insert($this->getTableName(), $insertData); @@ -62,9 +65,13 @@ public function save(AbstractEntity &$entity): void } $connection = $this->getConnection(); $where = $this->getPkCondition($entity); - $this->enrichCondition($where); - if ($this->config->isOptimisticLock && isset($entity->version)) { + if ($this->config->hasUpdatedAt() && property_exists($entity, 'updated_at')) { + $entity->updated_at = new \DateTimeImmutable(); + $changedColumns['updated_at'] = DateTimeHelper::dateTimeToString($entity->updated_at); + } + + if ($this->config->hasOptimisticLock() && isset($entity->version)) { $currentVersion = $entity->version; try { $connection->beginTransaction(); @@ -118,19 +125,13 @@ public function saveMany(array $entities): array public function delete(AbstractEntity &$entity): void { $this->config->checkEntity($entity); - if ($this->config->isSoftDelete) { + if ($this->config->hasSoftDelete()) { if (method_exists($entity, 'delete')) { - $condition = $this->getPkCondition($entity); - $this->getConnection()->update( - $this->getTableName(), - ['deleted_at' => DateTimeHelper::dateTimeToString(new \DateTime())], - $condition, - ); $entity->delete(); + $this->save($entity); } } else { $where = $this->getPkCondition($entity); - $this->enrichCondition($where); $this->getConnection()->delete($this->getTableName(), $where); } } @@ -161,7 +162,6 @@ protected function countAllInternal(string $whereString = '', array $whereParams $query->setParameter($param, $value); } } - $this->enrichCondition($query); return intval($query->executeQuery()->fetchOne()); } @@ -184,7 +184,6 @@ protected function findByPkInternal(mixed $pk): ?array protected function findOneInternal(array $where): ?array { $query = $this->select(); - $this->enrichCondition($where); $this->buildWhere($query, $where); return $query->fetchAssociative() ?: null; } @@ -210,8 +209,6 @@ protected function findAllInternal( $query->setParameter($param, $value); } } - $this->enrichCondition($query); - if ($orderBy) { if (is_array($orderBy)) { foreach ($orderBy as $column => $direction) { @@ -285,32 +282,12 @@ protected function getPkCondition(int|string|array|AbstractEntity $data): array } } else { foreach ($this->config->primaryKeys as $key) { - if ($this->config->isSoftDelete && $key === 'deleted_at') { - $condition['deleted_at'] = null; - } else { - $condition[$key] = $data; - } + $condition[$key] = $data; } } return $condition; } - /** - * @param array|QueryBuilder $query - */ - protected function enrichCondition(array|QueryBuilder &$query): void - { - if ($this->config->isSoftDelete) { - if ($query instanceof QueryBuilder) { - $query->andWhere('deleted_at IS NULL'); - } else { - if (!isset($query['deleted_at'])) { - $query['deleted_at'] = null; - } - } - } - } - protected function select(string $select = '*'): QueryBuilder { if ($this->selectQuery === null) { @@ -342,6 +319,10 @@ private function buildWhere(QueryBuilder $query, array $where): void private function formatData(array $data): array { foreach ($data as $columnName => $value) { + if ($value === null && $this->config->isPrimaryKey($columnName)) { + unset($data[$columnName]); + continue; + } if ($this->getConnection()->getDatabasePlatform() instanceof AbstractMySQLPlatform) { if (is_bool($value)) { $data[$columnName] = $value ? 1 : 0; diff --git a/src/Helpers/DateTimeHelper.php b/src/Helpers/DateTimeHelper.php index 59eea94..ae7220c 100644 --- a/src/Helpers/DateTimeHelper.php +++ b/src/Helpers/DateTimeHelper.php @@ -2,6 +2,8 @@ namespace Composite\DB\Helpers; +use Doctrine\DBAL\Platforms; + class DateTimeHelper { final public const DEFAULT_TIMESTAMP = '1970-01-01 00:00:01'; diff --git a/src/TableConfig.php b/src/TableConfig.php index a145ffb..ce1ace5 100644 --- a/src/TableConfig.php +++ b/src/TableConfig.php @@ -9,6 +9,8 @@ class TableConfig { + private readonly array $entityTraits; + /** * @param class-string $entityClass * @param string[] $primaryKeys @@ -19,10 +21,10 @@ public function __construct( public readonly string $entityClass, public readonly array $primaryKeys, public readonly ?string $autoIncrementKey = null, - public readonly bool $isSoftDelete = false, - public readonly bool $isOptimisticLock = false, ) - {} + { + $this->entityTraits = array_fill_keys(class_uses($entityClass), true); + } /** * @throws EntityException @@ -39,7 +41,6 @@ public static function fromEntitySchema(Schema $schema): TableConfig } $primaryKeys = []; $autoIncrementKey = null; - $isSoftDelete = $isOptimisticLock = false; foreach ($schema->columns as $column) { foreach ($column->attributes as $attribute) { @@ -51,24 +52,12 @@ public static function fromEntitySchema(Schema $schema): TableConfig } } } - foreach (class_uses($schema->class) as $traitClass) { - if ($traitClass === Traits\SoftDelete::class) { - $isSoftDelete = true; - if (!\in_array('deleted_at', $primaryKeys)) { - $primaryKeys[] = 'deleted_at'; - } - } elseif ($traitClass === Traits\OptimisticLock::class) { - $isOptimisticLock = true; - } - } return new TableConfig( connectionName: $tableAttribute->connection, tableName: $tableAttribute->name, entityClass: $schema->class, primaryKeys: $primaryKeys, autoIncrementKey: $autoIncrementKey, - isSoftDelete: $isSoftDelete, - isOptimisticLock: $isOptimisticLock, ); } @@ -84,4 +73,24 @@ public function checkEntity(AbstractEntity $entity): void ); } } + + public function isPrimaryKey(string $columnName): bool + { + return \in_array($columnName, $this->primaryKeys); + } + + public function hasSoftDelete(): bool + { + return !empty($this->entityTraits[Traits\SoftDelete::class]); + } + + public function hasOptimisticLock(): bool + { + return !empty($this->entityTraits[Traits\OptimisticLock::class]); + } + + public function hasUpdatedAt(): bool + { + return !empty($this->entityTraits[Traits\UpdatedAt::class]); + } } \ No newline at end of file diff --git a/src/Traits/UpdatedAt.php b/src/Traits/UpdatedAt.php new file mode 100644 index 0000000..1e8d878 --- /dev/null +++ b/src/Traits/UpdatedAt.php @@ -0,0 +1,8 @@ + 123, 'name' => 'John']), - ['id' => 123, 'deleted_at' => null], + ['id' => 123], ], [ new Tables\TestCompositeSdTable(), new Entities\TestCompositeSdEntity(user_id: 123, post_id: 456, message: 'Text'), - ['user_id' => 123, 'post_id' => 456, 'deleted_at' => null], + ['user_id' => 123, 'post_id' => 456], ], [ new Tables\TestUniqueSdTable(), new Entities\TestUniqueSdEntity(id: '123abc', name: 'John'), - ['id' => '123abc', 'deleted_at' => null], + ['id' => '123abc'], ], ]; } @@ -73,52 +73,6 @@ public function test_getPkCondition(AbstractTable $table, int|string|array|Abstr $this->assertEquals($expected, $actual); } - public function enrichCondition_dataProvider(): array - { - return [ - [ - new Tables\TestAutoincrementTable(), - ['id' => 123], - ['id' => 123], - ], - [ - new Tables\TestCompositeTable(), - ['user_id' => 123, 'post_id' => 456], - ['user_id' => 123, 'post_id' => 456], - ], - [ - new Tables\TestUniqueTable(), - ['id' => '123abc'], - ['id' => '123abc'], - ], - [ - new Tables\TestAutoincrementSdTable(), - ['id' => 123], - ['id' => 123, 'deleted_at' => null], - ], - [ - new Tables\TestCompositeSdTable(), - ['user_id' => 123, 'post_id' => 456], - ['user_id' => 123, 'post_id' => 456, 'deleted_at' => null], - ], - [ - new Tables\TestUniqueSdTable(), - ['id' => '123abc'], - ['id' => '123abc', 'deleted_at' => null], - ], - ]; - } - - /** - * @dataProvider enrichCondition_dataProvider - */ - public function test_enrichCondition(AbstractTable $table, array $condition, array $expected): void - { - $reflectionMethod = new \ReflectionMethod($table, 'enrichCondition'); - $reflectionMethod->invokeArgs($table, [&$condition]); - $this->assertEquals($expected, $condition); - } - public function test_illegalEntitySave(): void { $entity = new Entities\TestAutoincrementEntity(name: 'Foo'); diff --git a/tests/Table/AutoIncrementTableTest.php b/tests/Table/AutoIncrementTableTest.php index c1a3535..9065d49 100644 --- a/tests/Table/AutoIncrementTableTest.php +++ b/tests/Table/AutoIncrementTableTest.php @@ -2,6 +2,8 @@ namespace Composite\DB\Tests\Table; +use Composite\DB\AbstractTable; +use Composite\DB\TableConfig; use Composite\DB\Tests\TestStand\Tables; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Interfaces\IAutoincrementTable; @@ -37,11 +39,13 @@ public function crud_dataProvider(): array } /** + * @param class-string $class * @dataProvider crud_dataProvider */ - public function test_crud(IAutoincrementTable $table, string $class): void + public function test_crud(AbstractTable&IAutoincrementTable $table, string $class): void { $table->truncate(); + $tableConfig = TableConfig::fromEntitySchema($class::schema()); $entity = new $class( name: $this->getUniqueName(), @@ -62,7 +66,13 @@ public function test_crud(IAutoincrementTable $table, string $class): void $this->assertEquals($newName, $foundEntity->name); $table->delete($entity); - $this->assertEntityNotExists($table, $entity->id, $entity->name); + if ($tableConfig->hasSoftDelete()) { + /** @var Entities\TestAutoincrementSdEntity $deletedEntity */ + $deletedEntity = $table->findByPk($entity->id); + $this->assertTrue($deletedEntity->isDeleted()); + } else { + $this->assertEntityNotExists($table, $entity->id, $entity->name); + } } private function assertEntityExists(IAutoincrementTable $table, Entities\TestAutoincrementEntity $entity): void diff --git a/tests/Table/CompositeTableTest.php b/tests/Table/CompositeTableTest.php index 87bc767..57dc064 100644 --- a/tests/Table/CompositeTableTest.php +++ b/tests/Table/CompositeTableTest.php @@ -2,6 +2,8 @@ namespace Composite\DB\Tests\Table; +use Composite\DB\AbstractTable; +use Composite\DB\TableConfig; use Composite\DB\Tests\TestStand\Tables; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Interfaces\ICompositeTable; @@ -37,11 +39,13 @@ public function crud_dataProvider(): array } /** + * @param class-string $class * @dataProvider crud_dataProvider */ - public function test_crud(ICompositeTable $table, string $class): void + public function test_crud(AbstractTable&ICompositeTable $table, string $class): void { $table->truncate(); + $tableConfig = TableConfig::fromEntitySchema($class::schema()); $entity = new $class( user_id: mt_rand(1, 1000000), @@ -57,15 +61,14 @@ public function test_crud(ICompositeTable $table, string $class): void $this->assertEntityExists($table, $entity); $table->delete($entity); - $this->assertEntityNotExists($table, $entity); - $newEntity = new $entity( - user_id: $entity->user_id, - post_id: $entity->post_id, - message: 'Hello User', - ); - $table->save($newEntity); - $this->assertEntityExists($table, $newEntity); + if ($tableConfig->hasSoftDelete()) { + /** @var Entities\TestCompositeSdEntity $deletedEntity */ + $deletedEntity = $table->findOne(user_id: $entity->user_id, post_id: $entity->post_id); + $this->assertTrue($deletedEntity->isDeleted()); + } else { + $this->assertEntityNotExists($table, $entity); + } } private function assertEntityExists(ICompositeTable $table, Entities\TestCompositeEntity $entity): void diff --git a/tests/Table/TableConfigTest.php b/tests/Table/TableConfigTest.php index 528a835..5b712e0 100644 --- a/tests/Table/TableConfigTest.php +++ b/tests/Table/TableConfigTest.php @@ -30,8 +30,8 @@ public function __construct( $tableConfig = TableConfig::fromEntitySchema($schema); $this->assertNotEmpty($tableConfig->connectionName); $this->assertNotEmpty($tableConfig->tableName); - $this->assertTrue($tableConfig->isSoftDelete); - $this->assertCount(2, $tableConfig->primaryKeys); + $this->assertTrue($tableConfig->hasSoftDelete()); + $this->assertCount(1, $tableConfig->primaryKeys); $this->assertSame('id', $tableConfig->autoIncrementKey); } } \ No newline at end of file diff --git a/tests/Table/UniqueTableTest.php b/tests/Table/UniqueTableTest.php index cb834c3..e2f0a1e 100644 --- a/tests/Table/UniqueTableTest.php +++ b/tests/Table/UniqueTableTest.php @@ -2,6 +2,8 @@ namespace Composite\DB\Tests\Table; +use Composite\DB\AbstractTable; +use Composite\DB\TableConfig; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Tables; use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable; @@ -37,11 +39,13 @@ public function crud_dataProvider(): array } /** + * @param class-string $class * @dataProvider crud_dataProvider */ - public function test_crud(IUniqueTable $table, string $class): void + public function test_crud(AbstractTable&IUniqueTable $table, string $class): void { $table->truncate(); + $tableConfig = TableConfig::fromEntitySchema($class::schema()); $entity = new $class( id: uniqid(), @@ -56,17 +60,14 @@ public function test_crud(IUniqueTable $table, string $class): void $this->assertEntityExists($table, $entity); $table->delete($entity); - $this->assertEntityNotExists($table, $entity); - - $newEntity = new $entity( - id: $entity->id, - name: $entity->name . ' new', - ); - $table->save($newEntity); - $this->assertEntityExists($table, $newEntity); - $table->delete($newEntity); - $this->assertEntityNotExists($table, $newEntity); + if ($tableConfig->hasSoftDelete()) { + /** @var Entities\TestUniqueSdEntity $deletedEntity */ + $deletedEntity = $table->findByPk($entity->id); + $this->assertTrue($deletedEntity->isDeleted()); + } else { + $this->assertEntityNotExists($table, $entity); + } } private function assertEntityExists(IUniqueTable $table, Entities\TestUniqueEntity $entity): void diff --git a/tests/TestStand/Entities/TestUpdatedAtEntity.php b/tests/TestStand/Entities/TestUpdatedAtEntity.php new file mode 100644 index 0000000..c47f652 --- /dev/null +++ b/tests/TestStand/Entities/TestUpdatedAtEntity.php @@ -0,0 +1,21 @@ +countAllInternal( 'user_id = :user_id', - ['user_id' => $userId], + ['user_id' => $userId, 'deleted_at' => null], ); } diff --git a/tests/TestStand/Tables/TestUpdateAtTable.php b/tests/TestStand/Tables/TestUpdateAtTable.php new file mode 100644 index 0000000..9ecc449 --- /dev/null +++ b/tests/TestStand/Tables/TestUpdateAtTable.php @@ -0,0 +1,41 @@ +createEntity($this->findByPkInternal($id)); + } + + public function init(): bool + { + $this->getConnection()->executeStatement( + " + CREATE TABLE IF NOT EXISTS {$this->getTableName()} + ( + `id` INTEGER NOT NULL CONSTRAINT TestAutoincrement_pk PRIMARY KEY AUTOINCREMENT, + `name` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP NOT NULL, + `updated_at` TIMESTAMP NOT NULL + ); + " + ); + return true; + } + + public function truncate(): void + { + $this->getConnection()->executeStatement("DELETE FROM {$this->getTableName()} WHERE 1"); + } +} \ No newline at end of file diff --git a/tests/Traits/UpdatedAtTest.php b/tests/Traits/UpdatedAtTest.php new file mode 100644 index 0000000..a3a3712 --- /dev/null +++ b/tests/Traits/UpdatedAtTest.php @@ -0,0 +1,26 @@ +assertNull($entity->updated_at); + + $table = new TestUpdateAtTable(); + $table->init(); + $table->save($entity); + + $this->assertNotNull($entity->updated_at); + + $dbEntity = $table->findByPk($entity->id); + $this->assertNotNull($dbEntity); + + $this->assertEquals($entity->updated_at, $dbEntity->updated_at); + } +} \ No newline at end of file From d041bf33c04a5f2d46ff73f8c19a539a4de4a349 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 20 May 2023 15:02:53 +0100 Subject: [PATCH 11/69] Add .gitattributes --- .gitattributes | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..af43e6b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +/.gitattributes export-ignore +/.gitignore export-ignore +/doc export-ignore +/phpunit.xml export-ignore +/tests export-ignore From 57ce3ece14db631f5c29cbc4f0bc941462a1cec3 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 20 May 2023 15:11:15 +0100 Subject: [PATCH 12/69] Update compositephp/entity version in composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5f435c3..2167628 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "php": "^8.1", "ext-pdo": "*", "psr/simple-cache": "1 - 3", - "compositephp/entity": "^0.1.5", + "compositephp/entity": "^0.1.7", "doctrine/dbal": "^3.5" }, "require-dev": { From 34fae1b693fbc9ea38514c3d7cbcf31a4c86cc51 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 27 May 2023 10:04:32 +0100 Subject: [PATCH 13/69] Move optimistic lock test from AbstractTableTest to separate test file --- tests/Table/AbstractTableTest.php | 57 ------------------------ tests/Traits/OptimisticLockTest.php | 68 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 57 deletions(-) create mode 100644 tests/Traits/OptimisticLockTest.php diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php index a0a0dad..aaf68c3 100644 --- a/tests/Table/AbstractTableTest.php +++ b/tests/Table/AbstractTableTest.php @@ -86,61 +86,4 @@ public function test_illegalEntitySave(): void } $this->assertTrue($exceptionCatch); } - - public function test_optimisticLock(): void - { - //checking that problem exists - $aiEntity1 = new Entities\TestAutoincrementEntity(name: 'John'); - $aiTable1 = new Tables\TestAutoincrementTable(); - $aiTable2 = new Tables\TestAutoincrementTable(); - - $aiTable1->save($aiEntity1); - - $aiEntity2 = $aiTable2->findByPk($aiEntity1->id); - - $db = ConnectionManager::getConnection($aiTable1->getConnectionName()); - - $db->beginTransaction(); - $aiEntity1->name = 'John1'; - $aiTable1->save($aiEntity1); - - $aiEntity2->name = 'John2'; - $aiTable2->save($aiEntity2); - - $this->assertTrue($db->commit()); - - $aiEntity3 = $aiTable1->findByPk($aiEntity1->id); - $this->assertEquals('John2', $aiEntity3->name); - - //Checking optimistic lock - $olEntity1 = new Entities\TestOptimisticLockEntity(name: 'John'); - $olTable1 = new Tables\TestOptimisticLockTable(); - $olTable2 = new Tables\TestOptimisticLockTable(); - - $olTable1->init(); - - $olTable1->save($olEntity1); - - $olEntity2 = $olTable2->findByPk($olEntity1->id); - - $db->beginTransaction(); - $olEntity1->name = 'John1'; - $olTable1->save($olEntity1); - - $olEntity2->name = 'John2'; - - $exceptionCaught = false; - try { - $olTable2->save($olEntity2); - } catch (DbException) { - $exceptionCaught = true; - } - $this->assertTrue($exceptionCaught); - - $this->assertTrue($db->rollBack()); - - $olEntity3 = $olTable1->findByPk($olEntity1->id); - $this->assertEquals(1, $olEntity3->version); - $this->assertEquals('John', $olEntity3->name); - } } \ No newline at end of file diff --git a/tests/Traits/OptimisticLockTest.php b/tests/Traits/OptimisticLockTest.php new file mode 100644 index 0000000..737e279 --- /dev/null +++ b/tests/Traits/OptimisticLockTest.php @@ -0,0 +1,68 @@ +save($aiEntity1); + + $aiEntity2 = $aiTable2->findByPk($aiEntity1->id); + + $db = ConnectionManager::getConnection($aiTable1->getConnectionName()); + + $db->beginTransaction(); + $aiEntity1->name = 'John1'; + $aiTable1->save($aiEntity1); + + $aiEntity2->name = 'John2'; + $aiTable2->save($aiEntity2); + + $this->assertTrue($db->commit()); + + $aiEntity3 = $aiTable1->findByPk($aiEntity1->id); + $this->assertEquals('John2', $aiEntity3->name); + + //Checking optimistic lock + $olEntity1 = new Entities\TestOptimisticLockEntity(name: 'John'); + $olTable1 = new Tables\TestOptimisticLockTable(); + $olTable2 = new Tables\TestOptimisticLockTable(); + + $olTable1->init(); + + $olTable1->save($olEntity1); + + $olEntity2 = $olTable2->findByPk($olEntity1->id); + + $db->beginTransaction(); + $olEntity1->name = 'John1'; + $olTable1->save($olEntity1); + + $olEntity2->name = 'John2'; + + $exceptionCaught = false; + try { + $olTable2->save($olEntity2); + } catch (DbException) { + $exceptionCaught = true; + } + $this->assertTrue($exceptionCaught); + + $this->assertTrue($db->rollBack()); + + $olEntity3 = $olTable1->findByPk($olEntity1->id); + $this->assertEquals(1, $olEntity3->version); + $this->assertEquals('John', $olEntity3->name); + } +} \ No newline at end of file From ba7be35f264bfb9127fc097f665786ad1d755184 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 27 May 2023 10:12:19 +0100 Subject: [PATCH 14/69] Update transactions doc --- doc/table.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/doc/table.md b/doc/table.md index 326f095..c80f0d8 100644 --- a/doc/table.md +++ b/doc/table.md @@ -93,27 +93,36 @@ public function findCustom(): array ``` ## Transactions +In order to encapsulate your operations within a single transaction, you have two strategies at your disposal: +1. Use the internal table class method transaction() if your operations are confined to a single table. +2. Use the Composite\DB\CombinedTransaction class if your operations involve multiple tables within a single transaction. -To wrap you operations in 1 transactions there are 2 ways: -1. Use internal table class method `transaction()` if you are working only with 1 table. -2. Use class `Composite\DB\CombinedTransaction` if you need to work with several tables in 1 transaction. +Below is a sample code snippet illustrating how you can use the CombinedTransaction class: ```php + // Create instances of the tables you want to work with $usersTable = new UsersTable(); $photosTable = new PhotosTable(); - + + // Instantiate the CombinedTransaction class $transaction = new CombinedTransaction(); + // Create a new user and add it to the users table within the transaction $user = new User(...); $transaction->save($usersTable, $user); + // Create a new photo associated with the user and add it to the photos table within the transaction $photo = new Photo( user_id: $user->id, ... ); $transaction->save($photosTable, $photo); + + // Commit the transaction to finalize the changes $transaction->commit(); ``` + +Remember, using a transaction ensures that your operations are atomic. This means that either all changes are committed to the database, or if an error occurs, no changes are made. ## Locks If you worry about concurrency updates during your transaction and want to be sure that only 1 process changing your From f2798fa061dbec00ecb5dbb5aecff4758431ce10 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 28 May 2023 09:43:15 +0100 Subject: [PATCH 15/69] Add github actions and codecov --- .github/workflows/main.yml | 45 ++++++++++++++++++++++++++++++++++++++ .gitignore | 2 ++ phpunit.xml | 34 +++++++++++++++++++++++----- 3 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..544a7d2 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,45 @@ +name: PHP Composer + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Execute Tests + run: vendor/bin/phpunit tests --colors=always --coverage-clover=coverage.clover + env: + XDEBUG_MODE: coverage + + - name: Upload coverage reports to Codecov + continue-on-error: true + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.clover diff --git a/.gitignore b/.gitignore index a4cb13c..49f2327 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .idea .phpunit.result.cache +.phpunit.cache +coverage.clover vendor/ composer.lock tests/runtime/ diff --git a/phpunit.xml b/phpunit.xml index 23ced23..f39f444 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,28 @@ - - \ No newline at end of file + + + + + tests + + + + + + src + + + From c566dbfe624c432debaf15e5224dfd608ed3c884 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 28 May 2023 09:55:43 +0100 Subject: [PATCH 16/69] Update github workflow --- .github/workflows/main.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 544a7d2..4f98a1a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,6 +32,12 @@ jobs: - name: Install dependencies run: composer install --prefer-dist --no-progress + - name: Create runtime cache folder + run: mkdir -p -m 777 runtime/cache + + - name: Create sqlite folder + run: mkdir -p -m 777 runtime/sqlite && touch runtime/sqlite/database.db + - name: Execute Tests run: vendor/bin/phpunit tests --colors=always --coverage-clover=coverage.clover env: From ccb5f3245ed2607757601c9f23e0a024c490c4bd Mon Sep 17 00:00:00 2001 From: compositephp <38870693+compositephp@users.noreply.github.com> Date: Sun, 28 May 2023 09:58:30 +0100 Subject: [PATCH 17/69] Update main.yml --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4f98a1a..a6cfd81 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,10 +33,10 @@ jobs: run: composer install --prefer-dist --no-progress - name: Create runtime cache folder - run: mkdir -p -m 777 runtime/cache + run: mkdir -v -p -m 777 runtime/cache - name: Create sqlite folder - run: mkdir -p -m 777 runtime/sqlite && touch runtime/sqlite/database.db + run: mkdir -v -p -m 777 runtime/sqlite && touch runtime/sqlite/database.db - name: Execute Tests run: vendor/bin/phpunit tests --colors=always --coverage-clover=coverage.clover From 98fac68b69241f944630eab857a101477fd1cc26 Mon Sep 17 00:00:00 2001 From: compositephp <38870693+compositephp@users.noreply.github.com> Date: Sun, 28 May 2023 10:00:10 +0100 Subject: [PATCH 18/69] Update main.yml --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a6cfd81..b17c9ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,10 +33,10 @@ jobs: run: composer install --prefer-dist --no-progress - name: Create runtime cache folder - run: mkdir -v -p -m 777 runtime/cache + run: mkdir -v -p -m 777 tests/runtime/cache - name: Create sqlite folder - run: mkdir -v -p -m 777 runtime/sqlite && touch runtime/sqlite/database.db + run: mkdir -v -p -m 777 tests/runtime/sqlite && touch tests/runtime/sqlite/database.db - name: Execute Tests run: vendor/bin/phpunit tests --colors=always --coverage-clover=coverage.clover From bcb7b703ac334f79ec9578d4b6283804934c93c6 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 28 May 2023 10:06:27 +0100 Subject: [PATCH 19/69] Fix some test sequence --- tests/Table/AbstractTableTest.php | 2 -- tests/Table/CombinedTransactionTest.php | 11 +++++++++++ tests/TestStand/Tables/TestCompositeTable.php | 11 ----------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php index aaf68c3..ce6f402 100644 --- a/tests/Table/AbstractTableTest.php +++ b/tests/Table/AbstractTableTest.php @@ -3,8 +3,6 @@ namespace Composite\DB\Tests\Table; use Composite\DB\AbstractTable; -use Composite\DB\ConnectionManager; -use Composite\DB\Exceptions\DbException; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Tables; use Composite\Entity\AbstractEntity; diff --git a/tests/Table/CombinedTransactionTest.php b/tests/Table/CombinedTransactionTest.php index 7108179..4ee8865 100644 --- a/tests/Table/CombinedTransactionTest.php +++ b/tests/Table/CombinedTransactionTest.php @@ -12,7 +12,10 @@ final class CombinedTransactionTest extends BaseTableTest public function test_transactionCommit(): void { $autoIncrementTable = new Tables\TestAutoincrementTable(); + $autoIncrementTable->init(); + $compositeTable = new Tables\TestCompositeTable(); + $compositeTable->init(); $saveTransaction = new CombinedTransaction(); @@ -39,7 +42,10 @@ public function test_transactionCommit(): void public function test_transactionRollback(): void { $autoIncrementTable = new Tables\TestAutoincrementTable(); + $autoIncrementTable->init(); + $compositeTable = new Tables\TestCompositeTable(); + $compositeTable->init(); $transaction = new CombinedTransaction(); @@ -58,7 +64,10 @@ public function test_transactionRollback(): void public function test_transactionException(): void { $autoIncrementTable = new Tables\TestAutoincrementTable(); + $autoIncrementTable->init(); + $compositeTable = new Tables\TestCompositeTable(); + $compositeTable->init(); $transaction = new CombinedTransaction(); @@ -80,6 +89,8 @@ public function test_lock(): void { $cache = self::getCache(); $table = new Tables\TestAutoincrementTable(); + $table->init(); + $e1 = new Entities\TestAutoincrementEntity(name: 'Foo'); $e2 = new Entities\TestAutoincrementEntity(name: 'Bar'); diff --git a/tests/TestStand/Tables/TestCompositeTable.php b/tests/TestStand/Tables/TestCompositeTable.php index a6b9bba..f69c00b 100644 --- a/tests/TestStand/Tables/TestCompositeTable.php +++ b/tests/TestStand/Tables/TestCompositeTable.php @@ -46,17 +46,6 @@ public function countAllByUser(int $userId): int ); } - public function test(): array - { - $rows = $this - ->select() - ->where() - ->orWhere() - ->orderBy() - ->fetchAllAssociative(); - return $this->createEntities($rows); - } - public function init(): bool { $this->getConnection()->executeStatement( From ffab09bccf4b264bcc6d1b4444f292ba625598cf Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 28 May 2023 10:13:44 +0100 Subject: [PATCH 20/69] Delete useless Helper --- src/AbstractTable.php | 2 +- src/Helpers/DateTimeHelper.php | 39 ------------------- .../Castable/TestCastableIntObject.php | 2 +- 3 files changed, 2 insertions(+), 41 deletions(-) delete mode 100644 src/Helpers/DateTimeHelper.php diff --git a/src/AbstractTable.php b/src/AbstractTable.php index ad54d38..7434c5d 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -2,7 +2,7 @@ namespace Composite\DB; -use Composite\DB\Helpers\DateTimeHelper; +use Composite\Entity\Helpers\DateTimeHelper; use Composite\Entity\AbstractEntity; use Composite\DB\Exceptions\DbException; use Composite\Entity\Exceptions\EntityException; diff --git a/src/Helpers/DateTimeHelper.php b/src/Helpers/DateTimeHelper.php deleted file mode 100644 index ae7220c..0000000 --- a/src/Helpers/DateTimeHelper.php +++ /dev/null @@ -1,39 +0,0 @@ -format($withMicro ? self::DATETIME_MICRO_FORMAT : self::DATETIME_FORMAT); - } - - public static function isDefault(mixed $value): bool - { - if (!$value) { - return true; - } - if ($value instanceof \DateTimeInterface) { - $value = self::dateTimeToString($value); - } - return in_array( - $value, - [self::DEFAULT_TIMESTAMP, self::DEFAULT_TIMESTAMP_MICRO, self::DEFAULT_DATETIME, self::DEFAULT_DATETIME_MICRO] - ); - } -} diff --git a/tests/TestStand/Entities/Castable/TestCastableIntObject.php b/tests/TestStand/Entities/Castable/TestCastableIntObject.php index 56baddf..221da00 100644 --- a/tests/TestStand/Entities/Castable/TestCastableIntObject.php +++ b/tests/TestStand/Entities/Castable/TestCastableIntObject.php @@ -2,7 +2,7 @@ namespace Composite\DB\Tests\TestStand\Entities\Castable; -use Composite\DB\Helpers\DateTimeHelper; +use Composite\Entity\Helpers\DateTimeHelper; use Composite\Entity\CastableInterface; class TestCastableIntObject extends \DateTime implements CastableInterface From 8bec53e2f9f0113f267f4665739bc753fd9cc491 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 28 May 2023 10:58:39 +0100 Subject: [PATCH 21/69] Add \Composite\DB\Tests\Connection\ConnectionManagerTest --- tests/Connection/ConnectionManagerTest.php | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/Connection/ConnectionManagerTest.php diff --git a/tests/Connection/ConnectionManagerTest.php b/tests/Connection/ConnectionManagerTest.php new file mode 100644 index 0000000..f1e63ee --- /dev/null +++ b/tests/Connection/ConnectionManagerTest.php @@ -0,0 +1,31 @@ +assertInstanceOf(Connection::class, $connection); + } + + public function test_getConnectionWithInvalidConfig(): void + { + putenv('CONNECTIONS_CONFIG_FILE=invalid/path'); + $this->expectException(DbException::class); + + ConnectionManager::getConnection('db1'); + } + + public function test_getConnectionWithMissingName(): void + { + $this->expectException(DbException::class); + + ConnectionManager::getConnection('missing_db'); + } +} \ No newline at end of file From d14f57d305a630dcfa54984962b4f8f619a36ead Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 28 May 2023 11:32:06 +0100 Subject: [PATCH 22/69] Update saveMany adn deleteMany test cases --- src/AbstractCachedTable.php | 6 +----- src/AbstractTable.php | 3 +-- tests/Table/AutoIncrementTableTest.php | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index 92d124d..65f1e01 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -44,10 +44,8 @@ public function saveMany(array $entities): array { return $this->getConnection()->transactional(function() use ($entities) { $cacheKeys = []; - foreach ($entities as $entity) { + foreach ($entities as &$entity) { $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity)); - } - foreach ($entities as $entity) { parent::save($entity); } if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) { @@ -81,8 +79,6 @@ public function deleteMany(array $entities): bool $cacheKeys = []; foreach ($entities as $entity) { $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity)); - } - foreach ($entities as $entity) { parent::delete($entity); } if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) { diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 7434c5d..929d35c 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -106,13 +106,12 @@ public function save(AbstractEntity &$entity): void /** * @param AbstractEntity[] $entities - * @return AbstractEntity[] $entities * @throws \Throwable */ public function saveMany(array $entities): array { return $this->getConnection()->transactional(function() use ($entities) { - foreach ($entities as $entity) { + foreach ($entities as &$entity) { $this->save($entity); } return $entities; diff --git a/tests/Table/AutoIncrementTableTest.php b/tests/Table/AutoIncrementTableTest.php index 9065d49..2d34292 100644 --- a/tests/Table/AutoIncrementTableTest.php +++ b/tests/Table/AutoIncrementTableTest.php @@ -73,6 +73,27 @@ public function test_crud(AbstractTable&IAutoincrementTable $table, string $clas } else { $this->assertEntityNotExists($table, $entity->id, $entity->name); } + + $e1 = new $class($this->getUniqueName()); + $e2 = new $class($this->getUniqueName()); + + [$e1, $e2] = $table->saveMany([$e1, $e2]); + $this->assertEntityExists($table, $e1); + $this->assertEntityExists($table, $e2); + $this->assertTrue($table->deleteMany([$e1, $e2])); + + if ($tableConfig->hasSoftDelete()) { + /** @var Entities\TestAutoincrementSdEntity $deletedEntity1 */ + $deletedEntity1 = $table->findByPk($e1->id); + $this->assertTrue($deletedEntity1->isDeleted()); + + /** @var Entities\TestAutoincrementSdEntity $deletedEntity2 */ + $deletedEntity2 = $table->findByPk($e2->id); + $this->assertTrue($deletedEntity2->isDeleted()); + } else { + $this->assertEntityNotExists($table, $e1->id, $e1->name); + $this->assertEntityNotExists($table, $e2->id, $e2->name); + } } private function assertEntityExists(IAutoincrementTable $table, Entities\TestAutoincrementEntity $entity): void From 2071365b467cd7fe7cb37f831c034de8f6fc7e63 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 28 May 2023 11:56:47 +0100 Subject: [PATCH 23/69] More tests --- tests/Connection/ConnectionManagerTest.php | 48 ++++++++++++++++--- tests/TestStand/configs/empty_config.php | 0 .../configs/wrong_content_config.php | 2 + tests/TestStand/configs/wrong_name_config.php | 4 ++ .../TestStand/configs/wrong_params_config.php | 4 ++ 5 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 tests/TestStand/configs/empty_config.php create mode 100644 tests/TestStand/configs/wrong_content_config.php create mode 100644 tests/TestStand/configs/wrong_name_config.php create mode 100644 tests/TestStand/configs/wrong_params_config.php diff --git a/tests/Connection/ConnectionManagerTest.php b/tests/Connection/ConnectionManagerTest.php index f1e63ee..31c7b90 100644 --- a/tests/Connection/ConnectionManagerTest.php +++ b/tests/Connection/ConnectionManagerTest.php @@ -14,18 +14,52 @@ public function test_getConnection(): void $this->assertInstanceOf(Connection::class, $connection); } - public function test_getConnectionWithInvalidConfig(): void + public function invalidConfig_dataProvider(): array { - putenv('CONNECTIONS_CONFIG_FILE=invalid/path'); - $this->expectException(DbException::class); + $testStandConfigsBaseDir = __DIR__ . '../TestStand/configs/'; + return [ + [ + 'invalid/path', + ], + [ + $testStandConfigsBaseDir . 'empty_config.php', + ], + [ + $testStandConfigsBaseDir . 'wrong_content_config.php', + ], + [ + $testStandConfigsBaseDir . 'wrong_name_config.php', + ], + [ + $testStandConfigsBaseDir . 'wrong_params_config.php', + ], + ]; + } - ConnectionManager::getConnection('db1'); + /** + * @dataProvider invalidConfig_dataProvider + */ + public function test_invalidConfig(string $configPath): void + { + $currentPath = getenv('CONNECTIONS_CONFIG_FILE'); + putenv('CONNECTIONS_CONFIG_FILE=' . $configPath); + try { + ConnectionManager::getConnection('db1'); + $this->assertTrue(false); + } catch (DbException) { + $this->assertTrue(true); + } finally { + putenv('CONNECTIONS_CONFIG_FILE=' . $currentPath); + } } public function test_getConnectionWithMissingName(): void { - $this->expectException(DbException::class); - - ConnectionManager::getConnection('missing_db'); + try { + ConnectionManager::getConnection('invalid_name'); + $this->assertTrue(false); + } catch (DbException) { + $this->assertTrue(true); + } } } \ No newline at end of file diff --git a/tests/TestStand/configs/empty_config.php b/tests/TestStand/configs/empty_config.php new file mode 100644 index 0000000..e69de29 diff --git a/tests/TestStand/configs/wrong_content_config.php b/tests/TestStand/configs/wrong_content_config.php new file mode 100644 index 0000000..27cdc70 --- /dev/null +++ b/tests/TestStand/configs/wrong_content_config.php @@ -0,0 +1,2 @@ + [], +]; \ No newline at end of file diff --git a/tests/TestStand/configs/wrong_params_config.php b/tests/TestStand/configs/wrong_params_config.php new file mode 100644 index 0000000..7993161 --- /dev/null +++ b/tests/TestStand/configs/wrong_params_config.php @@ -0,0 +1,4 @@ + 123, +]; \ No newline at end of file From e18ae2de7d409e64614b3ed69a99ca75407a8a19 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 28 May 2023 12:16:09 +0100 Subject: [PATCH 24/69] Update tests --- tests/Connection/ConnectionManagerTest.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/Connection/ConnectionManagerTest.php b/tests/Connection/ConnectionManagerTest.php index 31c7b90..b820380 100644 --- a/tests/Connection/ConnectionManagerTest.php +++ b/tests/Connection/ConnectionManagerTest.php @@ -16,8 +16,11 @@ public function test_getConnection(): void public function invalidConfig_dataProvider(): array { - $testStandConfigsBaseDir = __DIR__ . '../TestStand/configs/'; + $testStandConfigsBaseDir = __DIR__ . '/../TestStand/configs/'; return [ + [ + '', + ], [ 'invalid/path', ], @@ -41,8 +44,11 @@ public function invalidConfig_dataProvider(): array */ public function test_invalidConfig(string $configPath): void { + $reflection = new \ReflectionClass(ConnectionManager::class); + $reflection->setStaticPropertyValue('configs', null); $currentPath = getenv('CONNECTIONS_CONFIG_FILE'); putenv('CONNECTIONS_CONFIG_FILE=' . $configPath); + try { ConnectionManager::getConnection('db1'); $this->assertTrue(false); @@ -50,6 +56,7 @@ public function test_invalidConfig(string $configPath): void $this->assertTrue(true); } finally { putenv('CONNECTIONS_CONFIG_FILE=' . $currentPath); + $reflection->setStaticPropertyValue('configs', null); } } From 4d7cdf8b5cd1ac58ea496add26acf6376ec80c8d Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 28 May 2023 12:23:45 +0100 Subject: [PATCH 25/69] Update tests --- tests/Connection/ConnectionManagerTest.php | 11 +++++------ tests/Table/TableConfigTest.php | 17 +++++++++++++++++ .../TestStand/configs/wrong_doctrine_config.php | 4 ++++ 3 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 tests/TestStand/configs/wrong_doctrine_config.php diff --git a/tests/Connection/ConnectionManagerTest.php b/tests/Connection/ConnectionManagerTest.php index b820380..4ee5b3e 100644 --- a/tests/Connection/ConnectionManagerTest.php +++ b/tests/Connection/ConnectionManagerTest.php @@ -36,6 +36,9 @@ public function invalidConfig_dataProvider(): array [ $testStandConfigsBaseDir . 'wrong_params_config.php', ], + [ + $testStandConfigsBaseDir . 'wrong_doctrine_config.php', + ], ]; } @@ -62,11 +65,7 @@ public function test_invalidConfig(string $configPath): void public function test_getConnectionWithMissingName(): void { - try { - ConnectionManager::getConnection('invalid_name'); - $this->assertTrue(false); - } catch (DbException) { - $this->assertTrue(true); - } + $this->expectException(DbException::class); + ConnectionManager::getConnection('invalid_name'); } } \ No newline at end of file diff --git a/tests/Table/TableConfigTest.php b/tests/Table/TableConfigTest.php index 5b712e0..63c5983 100644 --- a/tests/Table/TableConfigTest.php +++ b/tests/Table/TableConfigTest.php @@ -6,6 +6,7 @@ use Composite\DB\TableConfig; use Composite\DB\Traits; use Composite\Entity\AbstractEntity; +use Composite\Entity\Exceptions\EntityException; use Composite\Entity\Schema; final class TableConfigTest extends \PHPUnit\Framework\TestCase @@ -34,4 +35,20 @@ public function __construct( $this->assertCount(1, $tableConfig->primaryKeys); $this->assertSame('id', $tableConfig->autoIncrementKey); } + + public function test_missingAttribute(): void + { + $class = new + class extends AbstractEntity { + #[Attributes\PrimaryKey(autoIncrement: true)] + public readonly int $id; + + public function __construct( + public string $str = 'abc', + ) {} + }; + $schema = Schema::build($class::class); + $this->expectException(EntityException::class); + TableConfig::fromEntitySchema($schema); + } } \ No newline at end of file diff --git a/tests/TestStand/configs/wrong_doctrine_config.php b/tests/TestStand/configs/wrong_doctrine_config.php new file mode 100644 index 0000000..a7a4b50 --- /dev/null +++ b/tests/TestStand/configs/wrong_doctrine_config.php @@ -0,0 +1,4 @@ + [], +]; \ No newline at end of file From dfcbb3ab6aba08488730953b4c3538144bb5a907 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Mon, 29 May 2023 23:17:28 +0100 Subject: [PATCH 26/69] Fix orderBy in AbstractTable --- src/AbstractTable.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 929d35c..8048796 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -214,7 +214,14 @@ protected function findAllInternal( $query->addOrderBy($column, $direction); } } else { - $query->orderBy($orderBy); + foreach (explode(',', $orderBy) as $orderByPart) { + $orderByPart = trim($orderByPart); + if (preg_match('/(.+)\s(asc|desc)$/i', $orderByPart, $orderByPartMatch)) { + $query->addOrderBy($orderByPartMatch[1], $orderByPartMatch[2]); + } else { + $query->addOrderBy($orderByPart); + } + } } } if ($limit > 0) { From 30a78e5270b06ac234d14e522f20907e9cd6bc13 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Mon, 29 May 2023 23:39:28 +0100 Subject: [PATCH 27/69] Update formatData method and how it works with booleans --- src/AbstractTable.php | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 8048796..798cf31 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -7,7 +7,7 @@ use Composite\DB\Exceptions\DbException; use Composite\Entity\Exceptions\EntityException; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Query\QueryBuilder; abstract class AbstractTable @@ -324,15 +324,10 @@ private function buildWhere(QueryBuilder $query, array $where): void */ private function formatData(array $data): array { + $supportsBoolean = $this->getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform; foreach ($data as $columnName => $value) { - if ($value === null && $this->config->isPrimaryKey($columnName)) { - unset($data[$columnName]); - continue; - } - if ($this->getConnection()->getDatabasePlatform() instanceof AbstractMySQLPlatform) { - if (is_bool($value)) { - $data[$columnName] = $value ? 1 : 0; - } + if (is_bool($value) && !$supportsBoolean) { + $data[$columnName] = $value ? 1 : 0; } } return $data; From c9a125875a763a3422746996e7a889500e08c16e Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Mon, 29 May 2023 23:40:24 +0100 Subject: [PATCH 28/69] Tests cleanup and coverage improvements --- .gitignore | 1 + tests/Table/AbstractTableTest.php | 31 +++-- tests/Table/AutoIncrementTableTest.php | 8 ++ .../Castable/TestCastableIntObject.php | 32 ----- .../Castable/TestCastableStringObject.php | 26 ----- .../Entities/Enums/TestBackedIntEnum.php | 9 -- .../Entities/Enums/TestBackedStringEnum.php | 9 -- .../TestStand/Entities/Enums/TestUnitEnum.php | 9 -- .../Entities/TestAutoincrementEntity.php | 1 + .../Entities/TestDiversityEntity.php | 110 ------------------ tests/TestStand/Entities/TestEntity.php | 29 ----- tests/TestStand/Entities/TestStrictEntity.php | 17 +++ tests/TestStand/Entities/TestSubEntity.php | 13 --- .../Interfaces/IAutoincrementTable.php | 1 + .../Tables/TestAutoincrementCachedTable.php | 12 ++ .../Tables/TestAutoincrementSdCachedTable.php | 14 ++- .../Tables/TestAutoincrementSdTable.php | 15 ++- .../Tables/TestAutoincrementTable.php | 18 ++- tests/TestStand/Tables/TestStrictTable.php | 28 +++++ tests/Traits/UpdatedAtTest.php | 11 ++ 20 files changed, 146 insertions(+), 248 deletions(-) delete mode 100644 tests/TestStand/Entities/Castable/TestCastableIntObject.php delete mode 100644 tests/TestStand/Entities/Castable/TestCastableStringObject.php delete mode 100644 tests/TestStand/Entities/Enums/TestBackedIntEnum.php delete mode 100644 tests/TestStand/Entities/Enums/TestBackedStringEnum.php delete mode 100644 tests/TestStand/Entities/Enums/TestUnitEnum.php delete mode 100644 tests/TestStand/Entities/TestDiversityEntity.php delete mode 100644 tests/TestStand/Entities/TestEntity.php create mode 100644 tests/TestStand/Entities/TestStrictEntity.php delete mode 100644 tests/TestStand/Entities/TestSubEntity.php create mode 100644 tests/TestStand/Tables/TestStrictTable.php diff --git a/.gitignore b/.gitignore index 49f2327..caa2c69 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .phpunit.result.cache .phpunit.cache coverage.clover +coverage.txt vendor/ composer.lock tests/runtime/ diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php index ce6f402..6adb264 100644 --- a/tests/Table/AbstractTableTest.php +++ b/tests/Table/AbstractTableTest.php @@ -76,12 +76,29 @@ public function test_illegalEntitySave(): void $entity = new Entities\TestAutoincrementEntity(name: 'Foo'); $compositeTable = new Tables\TestUniqueTable(); - $exceptionCatch = false; - try { - $compositeTable->save($entity); - } catch (EntityException) { - $exceptionCatch = true; - } - $this->assertTrue($exceptionCatch); + $this->expectException(EntityException::class); + $compositeTable->save($entity); + } + + public function test_illegalCreateEntity(): void + { + $table = new Tables\TestStrictTable(); + $null = $table->buildEntity(['dti1' => 'abc']); + $this->assertNull($null); + + $empty = $table->buildEntities([['dti1' => 'abc']]); + $this->assertEmpty($empty); + + $empty = $table->buildEntities([]); + $this->assertEmpty($empty); + + $empty = $table->buildEntities(false); + $this->assertEmpty($empty); + + $empty = $table->buildEntities('abc'); + $this->assertEmpty($empty); + + $empty = $table->buildEntities(['abc']); + $this->assertEmpty($empty); } } \ No newline at end of file diff --git a/tests/Table/AutoIncrementTableTest.php b/tests/Table/AutoIncrementTableTest.php index 2d34292..83fd800 100644 --- a/tests/Table/AutoIncrementTableTest.php +++ b/tests/Table/AutoIncrementTableTest.php @@ -49,6 +49,7 @@ public function test_crud(AbstractTable&IAutoincrementTable $table, string $clas $entity = new $class( name: $this->getUniqueName(), + is_test: true, ); $this->assertEntityNotExists($table, PHP_INT_MAX, uniqid()); @@ -80,6 +81,13 @@ public function test_crud(AbstractTable&IAutoincrementTable $table, string $clas [$e1, $e2] = $table->saveMany([$e1, $e2]); $this->assertEntityExists($table, $e1); $this->assertEntityExists($table, $e2); + + $recentEntities = $table->findRecent(2, 0); + $this->assertEquals($e2, $recentEntities[0]); + $this->assertEquals($e1, $recentEntities[1]); + $preLastEntity = $table->findRecent(1, 1); + $this->assertEquals($e1, $preLastEntity[0]); + $this->assertTrue($table->deleteMany([$e1, $e2])); if ($tableConfig->hasSoftDelete()) { diff --git a/tests/TestStand/Entities/Castable/TestCastableIntObject.php b/tests/TestStand/Entities/Castable/TestCastableIntObject.php deleted file mode 100644 index 221da00..0000000 --- a/tests/TestStand/Entities/Castable/TestCastableIntObject.php +++ /dev/null @@ -1,32 +0,0 @@ -format('U'); - return $unixTime === 0 ? null : $unixTime ; - } -} \ No newline at end of file diff --git a/tests/TestStand/Entities/Castable/TestCastableStringObject.php b/tests/TestStand/Entities/Castable/TestCastableStringObject.php deleted file mode 100644 index 2a09e03..0000000 --- a/tests/TestStand/Entities/Castable/TestCastableStringObject.php +++ /dev/null @@ -1,26 +0,0 @@ -value ? '_' . $this->value . '_' : null; - } -} \ No newline at end of file diff --git a/tests/TestStand/Entities/Enums/TestBackedIntEnum.php b/tests/TestStand/Entities/Enums/TestBackedIntEnum.php deleted file mode 100644 index 8244fa3..0000000 --- a/tests/TestStand/Entities/Enums/TestBackedIntEnum.php +++ /dev/null @@ -1,9 +0,0 @@ -createEntities($this->findAllInternal( + orderBy: ['id' => 'DESC'], + limit: $limit, + offset: $offset, + )); + } + public function countAllByName(string $name): int { return $this->countAllCachedInternal( diff --git a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php index 007a187..ebc74da 100644 --- a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php +++ b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php @@ -48,7 +48,19 @@ public function findAllByName(string $name): array { return $this->createEntities($this->findAllCachedInternal( 'name = :name', - ['name' => $name], + ['name' => $name, 'deleted_at' => null], + )); + } + + /** + * @return TestAutoincrementSdEntity[] + */ + public function findRecent(int $limit, int $offset): array + { + return $this->createEntities($this->findAllInternal( + orderBy: 'id DESC', + limit: $limit, + offset: $offset, )); } diff --git a/tests/TestStand/Tables/TestAutoincrementSdTable.php b/tests/TestStand/Tables/TestAutoincrementSdTable.php index 8f68332..136d32b 100644 --- a/tests/TestStand/Tables/TestAutoincrementSdTable.php +++ b/tests/TestStand/Tables/TestAutoincrementSdTable.php @@ -19,7 +19,7 @@ public function findByPk(int $id): ?TestAutoincrementSdEntity public function findOneByName(string $name): ?TestAutoincrementSdEntity { - return $this->createEntity($this->findOneInternal(['name' => $name])); + return $this->createEntity($this->findOneInternal(['name' => $name, 'deleted_at' => null])); } /** @@ -33,6 +33,18 @@ public function findAllByName(string $name): array )); } + /** + * @return TestAutoincrementSdEntity[] + */ + public function findRecent(int $limit, int $offset): array + { + return $this->createEntities($this->findAllInternal( + orderBy: 'id DESC', + limit: $limit, + offset: $offset, + )); + } + public function init(): bool { $this->getConnection()->executeStatement( @@ -41,6 +53,7 @@ public function init(): bool ( `id` INTEGER NOT NULL CONSTRAINT TestAutoincrementSd_pk PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(255) NOT NULL, + `is_test` INTEGER NOT NULL DEFAULT 0, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, `deleted_at` TIMESTAMP NULL DEFAULT NULL ); diff --git a/tests/TestStand/Tables/TestAutoincrementTable.php b/tests/TestStand/Tables/TestAutoincrementTable.php index 900e576..9dfa129 100644 --- a/tests/TestStand/Tables/TestAutoincrementTable.php +++ b/tests/TestStand/Tables/TestAutoincrementTable.php @@ -30,8 +30,21 @@ public function findOneByName(string $name): ?TestAutoincrementEntity public function findAllByName(string $name): array { return $this->createEntities($this->findAllInternal( - 'name = :name', - ['name' => $name], + whereString: 'name = :name', + whereParams: ['name' => $name], + orderBy: 'id', + )); + } + + /** + * @return TestAutoincrementEntity[] + */ + public function findRecent(int $limit, int $offset): array + { + return $this->createEntities($this->findAllInternal( + orderBy: ['id' => 'DESC'], + limit: $limit, + offset: $offset, )); } @@ -51,6 +64,7 @@ public function init(): bool ( `id` INTEGER NOT NULL CONSTRAINT TestAutoincrement_pk PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(255) NOT NULL, + `is_test` INTEGER NOT NULL DEFAULT 0, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL ); " diff --git a/tests/TestStand/Tables/TestStrictTable.php b/tests/TestStand/Tables/TestStrictTable.php new file mode 100644 index 0000000..81a2c4b --- /dev/null +++ b/tests/TestStand/Tables/TestStrictTable.php @@ -0,0 +1,28 @@ +createEntity($data); + } + + /** + * @return TestStrictEntity[] + */ + public function buildEntities(mixed $data): array + { + return $this->createEntities($data); + } +} \ No newline at end of file diff --git a/tests/Traits/UpdatedAtTest.php b/tests/Traits/UpdatedAtTest.php index a3a3712..5a32b09 100644 --- a/tests/Traits/UpdatedAtTest.php +++ b/tests/Traits/UpdatedAtTest.php @@ -22,5 +22,16 @@ public function test_trait(): void $this->assertNotNull($dbEntity); $this->assertEquals($entity->updated_at, $dbEntity->updated_at); + + + $entity->name = 'Richard'; + $table->save($entity); + + $this->assertNotEquals($entity->updated_at, $dbEntity->updated_at); + $lastUpdatedAt = $entity->updated_at; + + //should not update entity + $table->save($entity); + $this->assertEquals($lastUpdatedAt, $entity->updated_at); } } \ No newline at end of file From b0d04ae74b848a4c1398cb37002001f9b97442ec Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Mon, 29 May 2023 23:46:39 +0100 Subject: [PATCH 29/69] TableConfig cleanup --- src/TableConfig.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/TableConfig.php b/src/TableConfig.php index ce1ace5..d11ea63 100644 --- a/src/TableConfig.php +++ b/src/TableConfig.php @@ -74,11 +74,6 @@ public function checkEntity(AbstractEntity $entity): void } } - public function isPrimaryKey(string $columnName): bool - { - return \in_array($columnName, $this->primaryKeys); - } - public function hasSoftDelete(): bool { return !empty($this->entityTraits[Traits\SoftDelete::class]); From dc48c36de833f0544b93e223abcf8d4fc3949a9b Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Tue, 30 May 2023 00:41:39 +0100 Subject: [PATCH 30/69] Fix \Composite\DB\AbstractCachedTable::findMultiCachedInternal --- src/AbstractCachedTable.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index 65f1e01..7d56924 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -187,6 +187,9 @@ protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $t } $cache = $this->cache->getMultiple(array_keys($cacheKeys)); foreach ($cache as $cacheKey => $cachedRow) { + if ($cachedRow === null) { + continue; + } $result[] = $cachedRow; if (empty($cacheKeys[$cacheKey])) { continue; @@ -195,7 +198,7 @@ protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $t } $ids = array_diff($ids, $foundIds); foreach ($ids as $id) { - if ($row = $this->findOneCachedInternal($id, $ttl)) { + if ($row = $this->findByPkCachedInternal($id, $ttl)) { $result[] = $row; } } From 8bf96bab52df1194adad4392edd3b2f3cb421aba Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Tue, 30 May 2023 00:42:01 +0100 Subject: [PATCH 31/69] More tests --- tests/Table/AbstractCachedTableTest.php | 16 ++++++++++++++++ .../Tables/TestAutoincrementCachedTable.php | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/tests/Table/AbstractCachedTableTest.php b/tests/Table/AbstractCachedTableTest.php index 544c09b..9dd59d4 100644 --- a/tests/Table/AbstractCachedTableTest.php +++ b/tests/Table/AbstractCachedTableTest.php @@ -242,4 +242,20 @@ public function test_collectCacheKeysByEntity(AbstractEntity $entity, AbstractCa $actual = $reflectionMethod->invoke($table, $entity); $this->assertEquals($expected, $actual); } + + public function test_findMulti(): void + { + $table = new Tables\TestAutoincrementCachedTable($this->getCache()); + $e1 = new Entities\TestAutoincrementEntity('John'); + $e2 = new Entities\TestAutoincrementEntity('Constantine'); + + [$e1, $e2] = $table->saveMany([$e1, $e2]); + + $multi1 = $table->findMulti([$e1->id]); + $this->assertEquals($e1, $multi1[0]); + + $multi2 = $table->findMulti([$e1->id, $e2->id]); + $this->assertEquals($e1, $multi2[0]); + $this->assertEquals($e2, $multi2[1]); + } } \ No newline at end of file diff --git a/tests/TestStand/Tables/TestAutoincrementCachedTable.php b/tests/TestStand/Tables/TestAutoincrementCachedTable.php index 90368ce..eecba0d 100644 --- a/tests/TestStand/Tables/TestAutoincrementCachedTable.php +++ b/tests/TestStand/Tables/TestAutoincrementCachedTable.php @@ -72,6 +72,14 @@ public function countAllByName(string $name): int ); } + /** + * @return TestAutoincrementEntity[] + */ + public function findMulti(array $ids): array + { + return $this->createEntities($this->findMultiCachedInternal($ids)); + } + public function truncate(): void { $this->getConnection()->executeStatement("DELETE FROM {$this->getTableName()} WHERE 1"); From 22a71741280ad53cb7931b3715a2ae790d7e5a90 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Tue, 30 May 2023 00:48:19 +0100 Subject: [PATCH 32/69] Fix test --- tests/Table/AbstractCachedTableTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Table/AbstractCachedTableTest.php b/tests/Table/AbstractCachedTableTest.php index 9dd59d4..03bbed3 100644 --- a/tests/Table/AbstractCachedTableTest.php +++ b/tests/Table/AbstractCachedTableTest.php @@ -245,6 +245,8 @@ public function test_collectCacheKeysByEntity(AbstractEntity $entity, AbstractCa public function test_findMulti(): void { + (new Tables\TestAutoincrementTable())->init(); + $table = new Tables\TestAutoincrementCachedTable($this->getCache()); $e1 = new Entities\TestAutoincrementEntity('John'); $e2 = new Entities\TestAutoincrementEntity('Constantine'); From 9d7251f2fecc291f48adfb47e91e5f474509c05a Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Tue, 30 May 2023 00:59:13 +0100 Subject: [PATCH 33/69] Update tests --- src/AbstractCachedTable.php | 7 +++---- tests/Table/AbstractCachedTableTest.php | 8 +++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index 7d56924..bac38aa 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -190,11 +190,10 @@ protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $t if ($cachedRow === null) { continue; } - $result[] = $cachedRow; - if (empty($cacheKeys[$cacheKey])) { - continue; + if (isset($cacheKeys[$cacheKey])) { + $result[] = $cachedRow; + $foundIds[] = $cacheKeys[$cacheKey]; } - $foundIds[] = $cacheKeys[$cacheKey]; } $ids = array_diff($ids, $foundIds); foreach ($ids as $id) { diff --git a/tests/Table/AbstractCachedTableTest.php b/tests/Table/AbstractCachedTableTest.php index 03bbed3..b8af512 100644 --- a/tests/Table/AbstractCachedTableTest.php +++ b/tests/Table/AbstractCachedTableTest.php @@ -246,7 +246,7 @@ public function test_collectCacheKeysByEntity(AbstractEntity $entity, AbstractCa public function test_findMulti(): void { (new Tables\TestAutoincrementTable())->init(); - + $table = new Tables\TestAutoincrementCachedTable($this->getCache()); $e1 = new Entities\TestAutoincrementEntity('John'); $e2 = new Entities\TestAutoincrementEntity('Constantine'); @@ -259,5 +259,11 @@ public function test_findMulti(): void $multi2 = $table->findMulti([$e1->id, $e2->id]); $this->assertEquals($e1, $multi2[0]); $this->assertEquals($e2, $multi2[1]); + + $e11 = $table->findByPk($e1->id); + $this->assertEquals($e1, $e11); + + $e111 = $table->findByPk($e1->id); + $this->assertEquals($e1, $e111); } } \ No newline at end of file From 9c484dac80690340c2f156b47136f0ee276b9dfe Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 4 Jun 2023 10:11:13 +0100 Subject: [PATCH 34/69] 1. Add \Composite\DB\AbstractTable::findMultiInternal 2. Improve code coverage 3. Migrate to phpunit 10 --- composer.json | 5 +- phpunit.xml | 28 +++---- src/AbstractCachedTable.php | 60 +++++++------- src/AbstractTable.php | 70 ++++++++++++++-- src/CombinedTransaction.php | 39 +++++---- tests/Attributes/PrimaryKeyAttributeTest.php | 2 +- tests/Connection/ConnectionManagerTest.php | 4 +- .../CacheHelper.php} | 4 +- tests/Helpers/FalseCache.php | 49 +++++++++++ tests/Helpers/StringHelper.php | 11 +++ tests/Table/AbstractCachedTableTest.php | 34 ++++---- tests/Table/AbstractTableTest.php | 4 +- tests/Table/AutoIncrementTableTest.php | 47 ++++++++--- tests/Table/CombinedTransactionTest.php | 83 +++++++++++++++---- tests/Table/CompositeTableTest.php | 60 +++++++++++--- tests/Table/UniqueTableTest.php | 17 ++-- .../Tables/TestAutoincrementCachedTable.php | 6 ++ .../Tables/TestAutoincrementSdCachedTable.php | 6 ++ .../Tables/TestAutoincrementSdTable.php | 6 ++ .../Tables/TestAutoincrementTable.php | 16 ++++ .../Tables/TestCompositeCachedTable.php | 6 ++ .../Tables/TestCompositeSdCachedTable.php | 6 ++ .../TestStand/Tables/TestCompositeSdTable.php | 6 ++ tests/TestStand/Tables/TestCompositeTable.php | 18 ++++ .../Tables/TestOptimisticLockTable.php | 6 ++ .../Tables/TestUniqueCachedTable.php | 6 ++ .../Tables/TestUniqueSdCachedTable.php | 6 ++ tests/TestStand/Tables/TestUniqueSdTable.php | 6 ++ tests/TestStand/Tables/TestUniqueTable.php | 6 ++ tests/TestStand/Tables/TestUpdateAtTable.php | 6 ++ .../TestStand/configs/wrong_params_config.php | 5 +- tests/Traits/OptimisticLockTest.php | 2 - tests/Traits/UpdatedAtTest.php | 1 - 33 files changed, 483 insertions(+), 148 deletions(-) rename tests/{Table/BaseTableTest.php => Helpers/CacheHelper.php} (83%) create mode 100644 tests/Helpers/FalseCache.php create mode 100644 tests/Helpers/StringHelper.php diff --git a/composer.json b/composer.json index 2167628..31fe04e 100644 --- a/composer.json +++ b/composer.json @@ -20,8 +20,9 @@ }, "require-dev": { "kodus/file-cache": "^2.0", - "phpunit/phpunit": "^9.5", - "phpstan/phpstan": "^1.9" + "phpunit/phpunit": "^10.1", + "phpstan/phpstan": "^1.9", + "phpunit/php-code-coverage": "^10.1" }, "autoload": { "psr-4": { diff --git a/phpunit.xml b/phpunit.xml index f39f444..f68b1fa 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,28 +1,22 @@ + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" bootstrap="tests/bootstrap.php" + executionOrder="depends,defects" beStrictAboutOutputDuringTests="true" failOnRisky="true" failOnWarning="true" + colors="true" cacheDirectory=".phpunit.cache" requireCoverageMetadata="false" + beStrictAboutCoverageMetadata="true"> tests - - + + + + + + src - + diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index bac38aa..247ce28 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -26,13 +26,11 @@ abstract protected function getFlushCacheKeys(AbstractEntity $entity): array; */ public function save(AbstractEntity &$entity): void { - $this->getConnection()->transactional(function () use (&$entity) { - $cacheKeys = $this->collectCacheKeysByEntity($entity); - parent::save($entity); - if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) { - throw new DbException('Failed to flush cache keys'); - } - }); + $cacheKeys = $this->collectCacheKeysByEntity($entity); + parent::save($entity); + if ($cacheKeys) { + $this->cache->deleteMultiple($cacheKeys); + } } /** @@ -42,17 +40,20 @@ public function save(AbstractEntity &$entity): void */ public function saveMany(array $entities): array { - return $this->getConnection()->transactional(function() use ($entities) { - $cacheKeys = []; + $cacheKeys = []; + foreach ($entities as $entity) { + $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity)); + } + $result = $this->getConnection()->transactional(function() use ($entities, $cacheKeys) { foreach ($entities as &$entity) { - $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity)); parent::save($entity); } - if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) { - throw new DbException('Failed to flush cache keys'); - } return $entities; }); + if ($cacheKeys) { + $this->cache->deleteMultiple(array_unique($cacheKeys)); + } + return $result; } /** @@ -60,13 +61,11 @@ public function saveMany(array $entities): array */ public function delete(AbstractEntity &$entity): void { - $this->getConnection()->transactional(function () use (&$entity) { - $cacheKeys = $this->collectCacheKeysByEntity($entity); - parent::delete($entity); - if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) { - throw new DbException('Failed to flush cache keys'); - } - }); + $cacheKeys = $this->collectCacheKeysByEntity($entity); + parent::delete($entity); + if ($cacheKeys) { + $this->cache->deleteMultiple($cacheKeys); + } } /** @@ -75,17 +74,21 @@ public function delete(AbstractEntity &$entity): void */ public function deleteMany(array $entities): bool { - return (bool)$this->getConnection()->transactional(function() use ($entities) { - $cacheKeys = []; + $cacheKeys = []; + foreach ($entities as $entity) { + $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity)); + parent::delete($entity); + } + $result = (bool)$this->getConnection()->transactional(function() use ($entities) { foreach ($entities as $entity) { - $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity)); parent::delete($entity); } - if ($cacheKeys && !$this->cache->deleteMultiple(array_unique($cacheKeys))) { - throw new DbException('Failed to flush cache keys'); - } return true; }); + if ($cacheKeys) { + $this->cache->deleteMultiple($cacheKeys); + } + return $result; } /** @@ -98,7 +101,7 @@ private function collectCacheKeysByEntity(AbstractEntity $entity): array if (!$entity->isNew() || !$this->getConfig()->autoIncrementKey) { $keys[] = $this->getOneCacheKey($entity); } - return $keys; + return array_unique($keys); } /** @@ -298,9 +301,6 @@ private function prepareWhereKey(string $whereString, array $whereParams): ?stri if (!$whereString) { return null; } - if (!$whereParams) { - return $whereString; - } return str_replace( array_map(fn (string $key): string => ':' . $key, array_keys($whereParams)), array_values($whereParams), diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 798cf31..7b1c3d5 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -187,6 +187,58 @@ protected function findOneInternal(array $where): ?array return $query->fetchAssociative() ?: null; } + /** + * @param array> $pkList + * @return array + * @throws DbException + * @throws EntityException + * @throws \Doctrine\DBAL\Exception + */ + protected function findMultiInternal(array $pkList): array + { + if (!$pkList) { + return []; + } + /** @var class-string $class */ + $class = $this->config->entityClass; + + $pkColumns = []; + foreach ($this->config->primaryKeys as $primaryKeyName) { + $pkColumns[$primaryKeyName] = $class::schema()->getColumn($primaryKeyName); + } + if (count($pkColumns) === 1) { + if (!array_is_list($pkList)) { + throw new DbException('Input argument $pkList must be list'); + } + /** @var \Composite\Entity\Columns\AbstractColumn $pkColumn */ + $pkColumn = reset($pkColumns); + $preparedPkValues = array_map(fn ($pk) => $pkColumn->uncast($pk), $pkList); + $query = $this->select(); + $this->buildWhere($query, [$pkColumn->name => $preparedPkValues]); + } else { + $query = $this->select(); + $expressions = []; + foreach ($pkList as $i => $pkArray) { + if (!is_array($pkArray) || array_is_list($pkArray)) { + throw new DbException('For tables with composite keys, input array must consist associative arrays'); + } + $pkOrExpr = []; + foreach ($pkArray as $pkName => $pkValue) { + if (is_string($pkName) && isset($pkColumns[$pkName])) { + $preparedPkValue = $pkColumns[$pkName]->cast($pkValue); + $pkOrExpr[] = $query->expr()->eq($pkName, ':' . $pkName . $i); + $query->setParameter($pkName . $i, $preparedPkValue); + } + } + if ($pkOrExpr) { + $expressions[] = $query->expr()->and(...$pkOrExpr); + } + } + $query->where($query->expr()->or(...$expressions)); + } + return $query->executeQuery()->fetchAllAssociative(); + } + /** * @param array $whereParams * @param array|string $orderBy @@ -239,7 +291,7 @@ final protected function createEntity(mixed $data): mixed return null; } try { - /** @psalm-var class-string $entityClass */ + /** @var class-string $entityClass */ $entityClass = $this->config->entityClass; return $entityClass::fromArray($data); } catch (\Throwable) { @@ -250,7 +302,7 @@ final protected function createEntity(mixed $data): mixed /** * @return AbstractEntity[] */ - final protected function createEntities(mixed $data): array + final protected function createEntities(mixed $data, ?string $keyColumnName = null): array { if (!is_array($data)) { return []; @@ -263,7 +315,11 @@ final protected function createEntities(mixed $data): array if (!is_array($datum)) { continue; } - $result[] = $entityClass::fromArray($datum); + if ($keyColumnName && isset($datum[$keyColumnName])) { + $result[$datum[$keyColumnName]] = $entityClass::fromArray($datum); + } else { + $result[] = $entityClass::fromArray($datum); + } } } catch (\Throwable) { return []; @@ -310,9 +366,13 @@ private function buildWhere(QueryBuilder $query, array $where): void foreach ($where as $column => $value) { if ($value === null) { $query->andWhere("$column IS NULL"); + } elseif (is_array($value)) { + $query + ->andWhere($query->expr()->in($column, $value)); } else { - $query->andWhere("$column = :" . $column); - $query->setParameter($column, $value); + $query + ->andWhere("$column = :" . $column) + ->setParameter($column, $value); } } } diff --git a/src/CombinedTransaction.php b/src/CombinedTransaction.php index ec2598e..1b53586 100644 --- a/src/CombinedTransaction.php +++ b/src/CombinedTransaction.php @@ -70,10 +70,13 @@ public function commit(): void if (!$connection->commit()) { throw new Exceptions\DbException("Could not commit transaction for database `$connectionName`"); } + // I have no idea how to simulate failed commit + // @codeCoverageIgnoreStart } catch (\Throwable $e) { $this->rollback(); throw new Exceptions\DbException($e->getMessage(), 500, $e); } + // @codeCoverageIgnoreEnd } $this->finish(); } @@ -82,23 +85,17 @@ public function commit(): void * Pessimistic lock * @param string[] $keyParts * @throws DbException + * @throws InvalidArgumentException */ public function lock(CacheInterface $cache, array $keyParts, int $duration = 10): void { $this->cache = $cache; - $this->lockKey = implode('.', array_merge(['composite', 'lock'], $keyParts)); - if (strlen($this->lockKey) > 64) { - $this->lockKey = sha1($this->lockKey); + $this->lockKey = $this->buildLockKey($keyParts); + if ($this->cache->get($this->lockKey)) { + throw new DbException("Failed to get lock `{$this->lockKey}`"); } - try { - if ($this->cache->get($this->lockKey)) { - throw new DbException("Failed to get lock `{$this->lockKey}`"); - } - if (!$this->cache->set($this->lockKey, 1, $duration)) { - throw new DbException("Failed to save lock `{$this->lockKey}`"); - } - } catch (InvalidArgumentException) { - throw new DbException("Lock key is invalid `{$this->lockKey}`"); + if (!$this->cache->set($this->lockKey, 1, $duration)) { + throw new DbException("Failed to save lock `{$this->lockKey}`"); } } @@ -107,9 +104,21 @@ public function releaseLock(): void if (!$this->cache || !$this->lockKey) { return; } - try { - $this->cache->delete($this->lockKey); - } catch (InvalidArgumentException) {} + $this->cache->delete($this->lockKey); + } + + /** + * @param string[] $keyParts + * @return string + */ + private function buildLockKey(array $keyParts): string + { + $keyParts = array_merge(['composite', 'lock'], $keyParts); + $result = implode('.', $keyParts); + if (strlen($result) > 64) { + $result = sha1($result); + } + return $result; } private function finish(): void diff --git a/tests/Attributes/PrimaryKeyAttributeTest.php b/tests/Attributes/PrimaryKeyAttributeTest.php index 26b6c55..750cd6a 100644 --- a/tests/Attributes/PrimaryKeyAttributeTest.php +++ b/tests/Attributes/PrimaryKeyAttributeTest.php @@ -8,7 +8,7 @@ final class PrimaryKeyAttributeTest extends \PHPUnit\Framework\TestCase { - public function primaryKey_dataProvider(): array + public static function primaryKey_dataProvider(): array { return [ [ diff --git a/tests/Connection/ConnectionManagerTest.php b/tests/Connection/ConnectionManagerTest.php index 4ee5b3e..7d634ff 100644 --- a/tests/Connection/ConnectionManagerTest.php +++ b/tests/Connection/ConnectionManagerTest.php @@ -14,7 +14,7 @@ public function test_getConnection(): void $this->assertInstanceOf(Connection::class, $connection); } - public function invalidConfig_dataProvider(): array + public static function invalidConfig_dataProvider(): array { $testStandConfigsBaseDir = __DIR__ . '/../TestStand/configs/'; return [ @@ -54,7 +54,7 @@ public function test_invalidConfig(string $configPath): void try { ConnectionManager::getConnection('db1'); - $this->assertTrue(false); + $this->fail('This line should not be reached'); } catch (DbException) { $this->assertTrue(true); } finally { diff --git a/tests/Table/BaseTableTest.php b/tests/Helpers/CacheHelper.php similarity index 83% rename from tests/Table/BaseTableTest.php rename to tests/Helpers/CacheHelper.php index 2b811c7..a6ad2ec 100644 --- a/tests/Table/BaseTableTest.php +++ b/tests/Helpers/CacheHelper.php @@ -1,11 +1,11 @@ format('U') . '_' . uniqid(); + } +} \ No newline at end of file diff --git a/tests/Table/AbstractCachedTableTest.php b/tests/Table/AbstractCachedTableTest.php index b8af512..2cf2252 100644 --- a/tests/Table/AbstractCachedTableTest.php +++ b/tests/Table/AbstractCachedTableTest.php @@ -4,15 +4,17 @@ use Composite\DB\AbstractCachedTable; use Composite\DB\AbstractTable; +use Composite\DB\Exceptions\DbException; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Tables; use Composite\Entity\AbstractEntity; +use Composite\DB\Tests\Helpers; -final class AbstractCachedTableTest extends BaseTableTest +final class AbstractCachedTableTest extends \PHPUnit\Framework\TestCase { - public function getOneCacheKey_dataProvider(): array + public static function getOneCacheKey_dataProvider(): array { - $cache = self::getCache(); + $cache = Helpers\CacheHelper::getCache(); return [ [ new Tables\TestAutoincrementCachedTable($cache), @@ -51,7 +53,7 @@ public function test_getOneCacheKey(AbstractTable $table, AbstractEntity $object } - public function getCountCacheKey_dataProvider(): array + public static function getCountCacheKey_dataProvider(): array { return [ [ @@ -87,13 +89,13 @@ public function getCountCacheKey_dataProvider(): array */ public function test_getCountCacheKey(string $whereString, array $whereParams, string $expected): void { - $table = new Tables\TestAutoincrementCachedTable(self::getCache()); + $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()); $reflectionMethod = new \ReflectionMethod($table, 'getCountCacheKey'); $actual = $reflectionMethod->invoke($table, $whereString, $whereParams); $this->assertEquals($expected, $actual); } - public function getListCacheKey_dataProvider(): array + public static function getListCacheKey_dataProvider(): array { return [ [ @@ -146,14 +148,14 @@ public function getListCacheKey_dataProvider(): array */ public function test_getListCacheKey(string $whereString, array $whereArray, array $orderBy, ?int $limit, string $expected): void { - $table = new Tables\TestAutoincrementCachedTable(self::getCache()); + $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()); $reflectionMethod = new \ReflectionMethod($table, 'getListCacheKey'); $actual = $reflectionMethod->invoke($table, $whereString, $whereArray, $orderBy, $limit); $this->assertEquals($expected, $actual); } - public function getCustomCacheKey_dataProvider(): array + public static function getCustomCacheKey_dataProvider(): array { return [ [ @@ -184,18 +186,18 @@ public function getCustomCacheKey_dataProvider(): array */ public function test_getCustomCacheKey(array $parts, string $expected): void { - $table = new Tables\TestAutoincrementCachedTable(self::getCache()); + $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()); $reflectionMethod = new \ReflectionMethod($table, 'buildCacheKey'); $actual = $reflectionMethod->invoke($table, ...$parts); $this->assertEquals($expected, $actual); } - public function collectCacheKeysByEntity_dataProvider(): array + public static function collectCacheKeysByEntity_dataProvider(): array { return [ [ new Entities\TestAutoincrementEntity(name: 'foo'), - new Tables\TestAutoincrementCachedTable(self::getCache()), + new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()), [ 'sqlite.TestAutoincrement.v1.o.name_foo', 'sqlite.TestAutoincrement.v1.l.name_eq_foo', @@ -204,7 +206,7 @@ public function collectCacheKeysByEntity_dataProvider(): array ], [ Entities\TestAutoincrementEntity::fromArray(['id' => 123, 'name' => 'bar']), - new Tables\TestAutoincrementCachedTable(self::getCache()), + new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()), [ 'sqlite.TestAutoincrement.v1.o.name_bar', 'sqlite.TestAutoincrement.v1.l.name_eq_bar', @@ -214,7 +216,7 @@ public function collectCacheKeysByEntity_dataProvider(): array ], [ new Entities\TestUniqueEntity(id: '123abc', name: 'foo'), - new Tables\TestUniqueCachedTable(self::getCache()), + new Tables\TestUniqueCachedTable(Helpers\CacheHelper::getCache()), [ 'sqlite.TestUnique.v1.l.name_eq_foo', 'sqlite.TestUnique.v1.c.name_eq_foo', @@ -223,7 +225,7 @@ public function collectCacheKeysByEntity_dataProvider(): array ], [ Entities\TestUniqueEntity::fromArray(['id' => '456def', 'name' => 'bar']), - new Tables\TestUniqueCachedTable(self::getCache()), + new Tables\TestUniqueCachedTable(Helpers\CacheHelper::getCache()), [ 'sqlite.TestUnique.v1.l.name_eq_bar', 'sqlite.TestUnique.v1.c.name_eq_bar', @@ -245,9 +247,7 @@ public function test_collectCacheKeysByEntity(AbstractEntity $entity, AbstractCa public function test_findMulti(): void { - (new Tables\TestAutoincrementTable())->init(); - - $table = new Tables\TestAutoincrementCachedTable($this->getCache()); + $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()); $e1 = new Entities\TestAutoincrementEntity('John'); $e2 = new Entities\TestAutoincrementEntity('Constantine'); diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php index 6adb264..6923fdb 100644 --- a/tests/Table/AbstractTableTest.php +++ b/tests/Table/AbstractTableTest.php @@ -8,9 +8,9 @@ use Composite\Entity\AbstractEntity; use Composite\Entity\Exceptions\EntityException; -final class AbstractTableTest extends BaseTableTest +final class AbstractTableTest extends \PHPUnit\Framework\TestCase { - public function getPkCondition_dataProvider(): array + public static function getPkCondition_dataProvider(): array { return [ [ diff --git a/tests/Table/AutoIncrementTableTest.php b/tests/Table/AutoIncrementTableTest.php index 83fd800..c8e22a1 100644 --- a/tests/Table/AutoIncrementTableTest.php +++ b/tests/Table/AutoIncrementTableTest.php @@ -3,20 +3,16 @@ namespace Composite\DB\Tests\Table; use Composite\DB\AbstractTable; +use Composite\DB\Exceptions\DbException; use Composite\DB\TableConfig; +use Composite\DB\Tests\Helpers; use Composite\DB\Tests\TestStand\Tables; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Interfaces\IAutoincrementTable; -final class AutoIncrementTableTest extends BaseTableTest +final class AutoIncrementTableTest extends \PHPUnit\Framework\TestCase { - public static function setUpBeforeClass(): void - { - (new Tables\TestAutoincrementTable())->init(); - (new Tables\TestAutoincrementSdTable())->init(); - } - - public function crud_dataProvider(): array + public static function crud_dataProvider(): array { return [ [ @@ -28,11 +24,11 @@ public function crud_dataProvider(): array Entities\TestAutoincrementSdEntity::class, ], [ - new Tables\TestAutoincrementCachedTable(self::getCache()), + new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()), Entities\TestAutoincrementEntity::class, ], [ - new Tables\TestAutoincrementSdCachedTable(self::getCache()), + new Tables\TestAutoincrementSdCachedTable(Helpers\CacheHelper::getCache()), Entities\TestAutoincrementSdEntity::class, ], ]; @@ -48,7 +44,7 @@ public function test_crud(AbstractTable&IAutoincrementTable $table, string $clas $tableConfig = TableConfig::fromEntitySchema($class::schema()); $entity = new $class( - name: $this->getUniqueName(), + name: Helpers\StringHelper::getUniqueName(), is_test: true, ); $this->assertEntityNotExists($table, PHP_INT_MAX, uniqid()); @@ -75,8 +71,8 @@ public function test_crud(AbstractTable&IAutoincrementTable $table, string $clas $this->assertEntityNotExists($table, $entity->id, $entity->name); } - $e1 = new $class($this->getUniqueName()); - $e2 = new $class($this->getUniqueName()); + $e1 = new $class(Helpers\StringHelper::getUniqueName()); + $e2 = new $class(Helpers\StringHelper::getUniqueName()); [$e1, $e2] = $table->saveMany([$e1, $e2]); $this->assertEntityExists($table, $e1); @@ -104,6 +100,31 @@ public function test_crud(AbstractTable&IAutoincrementTable $table, string $clas } } + public function test_getMulti(): void + { + $table = new Tables\TestAutoincrementTable(); + + $e1 = new Entities\TestAutoincrementEntity('name1'); + $e2 = new Entities\TestAutoincrementEntity('name2'); + $e3 = new Entities\TestAutoincrementEntity('name3'); + + [$e1, $e2, $e3] = $table->saveMany([$e1, $e2, $e3]); + + $multiResult = $table->findMulti([$e1->id, $e2->id, $e3->id]); + $this->assertEquals($e1, $multiResult[$e1->id]); + $this->assertEquals($e2, $multiResult[$e2->id]); + $this->assertEquals($e3, $multiResult[$e3->id]); + + $this->assertEmpty($table->findMulti([])); + } + + public function test_illegalGetMulti(): void + { + $table = new Tables\TestAutoincrementTable(); + $this->expectException(DbException::class); + $table->findMulti(['a' => 1]); + } + private function assertEntityExists(IAutoincrementTable $table, Entities\TestAutoincrementEntity $entity): void { $this->assertNotNull($table->findByPk($entity->id)); diff --git a/tests/Table/CombinedTransactionTest.php b/tests/Table/CombinedTransactionTest.php index 4ee8865..63ffff5 100644 --- a/tests/Table/CombinedTransactionTest.php +++ b/tests/Table/CombinedTransactionTest.php @@ -6,16 +6,14 @@ use Composite\DB\Exceptions\DbException; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Tables; +use Composite\DB\Tests\Helpers; -final class CombinedTransactionTest extends BaseTableTest +final class CombinedTransactionTest extends \PHPUnit\Framework\TestCase { public function test_transactionCommit(): void { $autoIncrementTable = new Tables\TestAutoincrementTable(); - $autoIncrementTable->init(); - $compositeTable = new Tables\TestCompositeTable(); - $compositeTable->init(); $saveTransaction = new CombinedTransaction(); @@ -42,10 +40,7 @@ public function test_transactionCommit(): void public function test_transactionRollback(): void { $autoIncrementTable = new Tables\TestAutoincrementTable(); - $autoIncrementTable->init(); - $compositeTable = new Tables\TestCompositeTable(); - $compositeTable->init(); $transaction = new CombinedTransaction(); @@ -61,13 +56,10 @@ public function test_transactionRollback(): void $this->assertNull($compositeTable->findOne($e2->user_id, $e2->post_id)); } - public function test_transactionException(): void + public function test_failedSave(): void { $autoIncrementTable = new Tables\TestAutoincrementTable(); - $autoIncrementTable->init(); - $compositeTable = new Tables\TestCompositeTable(); - $compositeTable->init(); $transaction = new CombinedTransaction(); @@ -78,18 +70,54 @@ public function test_transactionException(): void try { $transaction->save($compositeTable, $e2); $transaction->commit(); - $this->assertFalse(true, 'This line should not be reached'); + $this->fail('This line should not be reached'); } catch (DbException) {} $this->assertNull($autoIncrementTable->findByPk($e1->id)); $this->assertNull($compositeTable->findOne($e2->user_id, $e2->post_id)); } + public function test_failedDelete(): void + { + $autoIncrementTable = new Tables\TestAutoincrementTable(); + $compositeTable = new Tables\TestCompositeTable(); + + $aiEntity = new Entities\TestAutoincrementEntity(name: 'Foo'); + $cEntity = new Entities\TestCompositeEntity(user_id: mt_rand(1, 1000), post_id: mt_rand(1, 1000), message: 'Bar');; + + $autoIncrementTable->save($aiEntity); + $compositeTable->save($cEntity); + + $transaction = new CombinedTransaction(); + try { + $aiEntity->name = 'Foo1'; + $cEntity->message = 'Exception'; + + $transaction->save($autoIncrementTable, $aiEntity); + $transaction->delete($compositeTable, $cEntity); + + $transaction->commit(); + $this->fail('This line should not be reached'); + } catch (DbException) {} + + $this->assertEquals('Foo', $autoIncrementTable->findByPk($aiEntity->id)->name); + $this->assertNotNull($compositeTable->findOne($cEntity->user_id, $cEntity->post_id)); + } + + public function test_lockFailed(): void + { + $cache = new Helpers\FalseCache(); + $keyParts = [uniqid()]; + $transaction = new CombinedTransaction(); + + $this->expectException(DbException::class); + $transaction->lock($cache, $keyParts); + } + public function test_lock(): void { - $cache = self::getCache(); + $cache = Helpers\CacheHelper::getCache(); $table = new Tables\TestAutoincrementTable(); - $table->init(); $e1 = new Entities\TestAutoincrementEntity(name: 'Foo'); $e2 = new Entities\TestAutoincrementEntity(name: 'Bar'); @@ -101,8 +129,10 @@ public function test_lock(): void $transaction2 = new CombinedTransaction(); try { $transaction2->lock($cache, $keyParts); - $this->assertFalse(false, 'Lock should not be free'); - } catch (DbException) {} + $this->fail('Lock should not be free'); + } catch (DbException) { + $this->assertTrue(true); + } $transaction1->save($table, $e1); $transaction1->commit(); @@ -114,4 +144,25 @@ public function test_lock(): void $this->assertNotEmpty($table->findByPk($e1->id)); $this->assertNotEmpty($table->findByPk($e2->id)); } + + /** + * @dataProvider buildLockKey_dataProvider + */ + public function test_buildLockKey($keyParts, $expectedResult) + { + $reflection = new \ReflectionClass(CombinedTransaction::class); + $object = new CombinedTransaction(); + $result = $reflection->getMethod('buildLockKey')->invoke($object, $keyParts); + $this->assertEquals($expectedResult, $result); + } + + public static function buildLockKey_dataProvider() + { + return [ + 'empty array' => [[], 'composite.lock'], + 'one element' => [['element'], 'composite.lock.element'], + 'exact length' => [[str_repeat('a', 49)], 'composite.lock.' . str_repeat('a', 49)], + 'more than max length' => [[str_repeat('a', 55)], sha1('composite.lock.' . str_repeat('a', 55))], + ]; + } } \ No newline at end of file diff --git a/tests/Table/CompositeTableTest.php b/tests/Table/CompositeTableTest.php index 57dc064..7359e56 100644 --- a/tests/Table/CompositeTableTest.php +++ b/tests/Table/CompositeTableTest.php @@ -3,20 +3,16 @@ namespace Composite\DB\Tests\Table; use Composite\DB\AbstractTable; +use Composite\DB\Exceptions\DbException; use Composite\DB\TableConfig; +use Composite\DB\Tests\Helpers; use Composite\DB\Tests\TestStand\Tables; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Interfaces\ICompositeTable; -final class CompositeTableTest extends BaseTableTest +final class CompositeTableTest extends \PHPUnit\Framework\TestCase { - public static function setUpBeforeClass(): void - { - (new Tables\TestCompositeTable())->init(); - (new Tables\TestCompositeSdTable())->init(); - } - - public function crud_dataProvider(): array + public static function crud_dataProvider(): array { return [ [ @@ -28,11 +24,11 @@ public function crud_dataProvider(): array Entities\TestCompositeSdEntity::class, ], [ - new Tables\TestCompositeCachedTable(self::getCache()), + new Tables\TestCompositeCachedTable(Helpers\CacheHelper::getCache()), Entities\TestCompositeEntity::class, ], [ - new Tables\TestCompositeSdCachedTable(self::getCache()), + new Tables\TestCompositeSdCachedTable(Helpers\CacheHelper::getCache()), Entities\TestCompositeSdEntity::class, ], ]; @@ -50,7 +46,7 @@ public function test_crud(AbstractTable&ICompositeTable $table, string $class): $entity = new $class( user_id: mt_rand(1, 1000000), post_id: mt_rand(1, 1000000), - message: $this->getUniqueName(), + message: Helpers\StringHelper::getUniqueName(), ); $this->assertEntityNotExists($table, $entity); $table->save($entity); @@ -71,6 +67,48 @@ public function test_crud(AbstractTable&ICompositeTable $table, string $class): } } + public function test_getMulti(): void + { + $table = new Tables\TestCompositeTable(); + $userId = mt_rand(1, 1000000); + + $e1 = new Entities\TestCompositeEntity( + user_id: $userId, + post_id: mt_rand(1, 1000000), + message: Helpers\StringHelper::getUniqueName(), + ); + + $e2 = new Entities\TestCompositeEntity( + user_id: $userId, + post_id: mt_rand(1, 1000000), + message: Helpers\StringHelper::getUniqueName(), + ); + + $e3 = new Entities\TestCompositeEntity( + user_id: $userId, + post_id: mt_rand(1, 1000000), + message: Helpers\StringHelper::getUniqueName(), + ); + + [$e1, $e2, $e3] = $table->saveMany([$e1, $e2, $e3]); + + $multiResult = $table->findMulti([ + ['user_id' => $e1->user_id, 'post_id' => $e1->post_id], + ['user_id' => $e2->user_id, 'post_id' => $e2->post_id], + ['user_id' => $e3->user_id, 'post_id' => $e3->post_id], + ]); + $this->assertEquals($e1, $multiResult[$e1->post_id]); + $this->assertEquals($e2, $multiResult[$e2->post_id]); + $this->assertEquals($e3, $multiResult[$e3->post_id]); + } + + public function test_illegalGetMulti(): void + { + $table = new Tables\TestCompositeTable(); + $this->expectException(DbException::class); + $table->findMulti(['a']); + } + private function assertEntityExists(ICompositeTable $table, Entities\TestCompositeEntity $entity): void { $this->assertNotNull($table->findOne(user_id: $entity->user_id, post_id: $entity->post_id)); diff --git a/tests/Table/UniqueTableTest.php b/tests/Table/UniqueTableTest.php index e2f0a1e..72b277a 100644 --- a/tests/Table/UniqueTableTest.php +++ b/tests/Table/UniqueTableTest.php @@ -4,19 +4,14 @@ use Composite\DB\AbstractTable; use Composite\DB\TableConfig; +use Composite\DB\Tests\Helpers; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Tables; use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable; -final class UniqueTableTest extends BaseTableTest +final class UniqueTableTest extends \PHPUnit\Framework\TestCase { - public static function setUpBeforeClass(): void - { - (new Tables\TestUniqueTable())->init(); - (new Tables\TestUniqueSdTable())->init(); - } - - public function crud_dataProvider(): array + public static function crud_dataProvider(): array { return [ [ @@ -28,11 +23,11 @@ public function crud_dataProvider(): array Entities\TestUniqueSdEntity::class, ], [ - new Tables\TestUniqueCachedTable(self::getCache()), + new Tables\TestUniqueCachedTable(Helpers\CacheHelper::getCache()), Entities\TestUniqueEntity::class, ], [ - new Tables\TestUniqueSdCachedTable(self::getCache()), + new Tables\TestUniqueSdCachedTable(Helpers\CacheHelper::getCache()), Entities\TestUniqueSdEntity::class, ], ]; @@ -49,7 +44,7 @@ public function test_crud(AbstractTable&IUniqueTable $table, string $class): voi $entity = new $class( id: uniqid(), - name: $this->getUniqueName(), + name: Helpers\StringHelper::getUniqueName(), ); $this->assertEntityNotExists($table, $entity); $table->save($entity); diff --git a/tests/TestStand/Tables/TestAutoincrementCachedTable.php b/tests/TestStand/Tables/TestAutoincrementCachedTable.php index eecba0d..5776f92 100644 --- a/tests/TestStand/Tables/TestAutoincrementCachedTable.php +++ b/tests/TestStand/Tables/TestAutoincrementCachedTable.php @@ -10,6 +10,12 @@ class TestAutoincrementCachedTable extends AbstractCachedTable implements IAutoincrementTable { + public function __construct(\Psr\SimpleCache\CacheInterface $cache) + { + parent::__construct($cache); + (new TestAutoincrementTable)->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestAutoincrementEntity::schema()); diff --git a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php index ebc74da..66645f4 100644 --- a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php +++ b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php @@ -10,6 +10,12 @@ class TestAutoincrementSdCachedTable extends AbstractCachedTable implements IAutoincrementTable { + public function __construct(\Psr\SimpleCache\CacheInterface $cache) + { + parent::__construct($cache); + (new TestAutoincrementSdTable)->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestAutoincrementSdEntity::schema()); diff --git a/tests/TestStand/Tables/TestAutoincrementSdTable.php b/tests/TestStand/Tables/TestAutoincrementSdTable.php index 136d32b..b9ce555 100644 --- a/tests/TestStand/Tables/TestAutoincrementSdTable.php +++ b/tests/TestStand/Tables/TestAutoincrementSdTable.php @@ -7,6 +7,12 @@ class TestAutoincrementSdTable extends TestAutoincrementTable { + public function __construct() + { + parent::__construct(); + $this->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestAutoincrementSdEntity::schema()); diff --git a/tests/TestStand/Tables/TestAutoincrementTable.php b/tests/TestStand/Tables/TestAutoincrementTable.php index 9dfa129..c0768e6 100644 --- a/tests/TestStand/Tables/TestAutoincrementTable.php +++ b/tests/TestStand/Tables/TestAutoincrementTable.php @@ -9,6 +9,12 @@ class TestAutoincrementTable extends AbstractTable implements IAutoincrementTable { + public function __construct() + { + parent::__construct(); + $this->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestAutoincrementEntity::schema()); @@ -56,6 +62,16 @@ public function countAllByName(string $name): int ); } + /** + * @param int[] $ids + * @return TestAutoincrementEntity[] + * @throws \Composite\DB\Exceptions\DbException + */ + public function findMulti(array $ids): array + { + return $this->createEntities($this->findMultiInternal($ids), 'id'); + } + public function init(): bool { $this->getConnection()->executeStatement( diff --git a/tests/TestStand/Tables/TestCompositeCachedTable.php b/tests/TestStand/Tables/TestCompositeCachedTable.php index a2519da..936d0a9 100644 --- a/tests/TestStand/Tables/TestCompositeCachedTable.php +++ b/tests/TestStand/Tables/TestCompositeCachedTable.php @@ -9,6 +9,12 @@ class TestCompositeCachedTable extends \Composite\DB\AbstractCachedTable implements ICompositeTable { + public function __construct(\Psr\SimpleCache\CacheInterface $cache) + { + parent::__construct($cache); + (new TestCompositeTable())->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestCompositeEntity::schema()); diff --git a/tests/TestStand/Tables/TestCompositeSdCachedTable.php b/tests/TestStand/Tables/TestCompositeSdCachedTable.php index 97f8512..46846dc 100644 --- a/tests/TestStand/Tables/TestCompositeSdCachedTable.php +++ b/tests/TestStand/Tables/TestCompositeSdCachedTable.php @@ -9,6 +9,12 @@ class TestCompositeSdCachedTable extends \Composite\DB\AbstractCachedTable implements ICompositeTable { + public function __construct(\Psr\SimpleCache\CacheInterface $cache) + { + parent::__construct($cache); + (new TestCompositeSdTable())->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestCompositeSdEntity::schema()); diff --git a/tests/TestStand/Tables/TestCompositeSdTable.php b/tests/TestStand/Tables/TestCompositeSdTable.php index a35bd1e..28d3123 100644 --- a/tests/TestStand/Tables/TestCompositeSdTable.php +++ b/tests/TestStand/Tables/TestCompositeSdTable.php @@ -7,6 +7,12 @@ class TestCompositeSdTable extends TestCompositeTable { + public function __construct() + { + parent::__construct(); + $this->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestCompositeSdEntity::schema()); diff --git a/tests/TestStand/Tables/TestCompositeTable.php b/tests/TestStand/Tables/TestCompositeTable.php index f69c00b..359e450 100644 --- a/tests/TestStand/Tables/TestCompositeTable.php +++ b/tests/TestStand/Tables/TestCompositeTable.php @@ -22,6 +22,14 @@ public function save(AbstractEntity|TestCompositeEntity &$entity): void parent::save($entity); } + public function delete(AbstractEntity|TestCompositeEntity &$entity): void + { + if ($entity->message === 'Exception') { + throw new \Exception('Test Exception'); + } + parent::delete($entity); + } + public function findOne(int $user_id, int $post_id): ?TestCompositeEntity { return $this->createEntity($this->findOneInternal(['user_id' => $user_id, 'post_id' => $post_id])); @@ -46,6 +54,16 @@ public function countAllByUser(int $userId): int ); } + /** + * @param array $ids + * @return TestCompositeEntity[] + * @throws \Composite\DB\Exceptions\DbException + */ + public function findMulti(array $ids): array + { + return $this->createEntities($this->findMultiInternal($ids), 'post_id'); + } + public function init(): bool { $this->getConnection()->executeStatement( diff --git a/tests/TestStand/Tables/TestOptimisticLockTable.php b/tests/TestStand/Tables/TestOptimisticLockTable.php index beb6123..cf318fd 100644 --- a/tests/TestStand/Tables/TestOptimisticLockTable.php +++ b/tests/TestStand/Tables/TestOptimisticLockTable.php @@ -8,6 +8,12 @@ class TestOptimisticLockTable extends AbstractTable { + public function __construct() + { + parent::__construct(); + $this->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestOptimisticLockEntity::schema()); diff --git a/tests/TestStand/Tables/TestUniqueCachedTable.php b/tests/TestStand/Tables/TestUniqueCachedTable.php index f95d102..46c5e55 100644 --- a/tests/TestStand/Tables/TestUniqueCachedTable.php +++ b/tests/TestStand/Tables/TestUniqueCachedTable.php @@ -10,6 +10,12 @@ class TestUniqueCachedTable extends AbstractCachedTable implements IUniqueTable { + public function __construct(\Psr\SimpleCache\CacheInterface $cache) + { + parent::__construct($cache); + (new TestUniqueTable())->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestUniqueEntity::schema()); diff --git a/tests/TestStand/Tables/TestUniqueSdCachedTable.php b/tests/TestStand/Tables/TestUniqueSdCachedTable.php index 278cb37..8c60a0c 100644 --- a/tests/TestStand/Tables/TestUniqueSdCachedTable.php +++ b/tests/TestStand/Tables/TestUniqueSdCachedTable.php @@ -10,6 +10,12 @@ class TestUniqueSdCachedTable extends AbstractCachedTable implements IUniqueTable { + public function __construct(\Psr\SimpleCache\CacheInterface $cache) + { + parent::__construct($cache); + (new TestUniqueSdTable())->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestUniqueSdEntity::schema()); diff --git a/tests/TestStand/Tables/TestUniqueSdTable.php b/tests/TestStand/Tables/TestUniqueSdTable.php index 6bed15f..33cbf62 100644 --- a/tests/TestStand/Tables/TestUniqueSdTable.php +++ b/tests/TestStand/Tables/TestUniqueSdTable.php @@ -7,6 +7,12 @@ class TestUniqueSdTable extends TestUniqueTable { + public function __construct() + { + parent::__construct(); + $this->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestUniqueSdEntity::schema()); diff --git a/tests/TestStand/Tables/TestUniqueTable.php b/tests/TestStand/Tables/TestUniqueTable.php index 317985c..137a24a 100644 --- a/tests/TestStand/Tables/TestUniqueTable.php +++ b/tests/TestStand/Tables/TestUniqueTable.php @@ -9,6 +9,12 @@ class TestUniqueTable extends AbstractTable implements IUniqueTable { + public function __construct() + { + parent::__construct(); + $this->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestUniqueEntity::schema()); diff --git a/tests/TestStand/Tables/TestUpdateAtTable.php b/tests/TestStand/Tables/TestUpdateAtTable.php index 9ecc449..2fb74c4 100644 --- a/tests/TestStand/Tables/TestUpdateAtTable.php +++ b/tests/TestStand/Tables/TestUpdateAtTable.php @@ -8,6 +8,12 @@ class TestUpdateAtTable extends AbstractTable { + public function __construct() + { + parent::__construct(); + $this->init(); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestUpdatedAtEntity::schema()); diff --git a/tests/TestStand/configs/wrong_params_config.php b/tests/TestStand/configs/wrong_params_config.php index 7993161..cee852e 100644 --- a/tests/TestStand/configs/wrong_params_config.php +++ b/tests/TestStand/configs/wrong_params_config.php @@ -1,4 +1,7 @@ 123, + 'db1' => [ + 'driver' => 'pdo_nothing', + 'path' => __DIR__ . '/runtime/sqlite/database.db', + ], ]; \ No newline at end of file diff --git a/tests/Traits/OptimisticLockTest.php b/tests/Traits/OptimisticLockTest.php index 737e279..b64e973 100644 --- a/tests/Traits/OptimisticLockTest.php +++ b/tests/Traits/OptimisticLockTest.php @@ -39,8 +39,6 @@ public function test_trait(): void $olTable1 = new Tables\TestOptimisticLockTable(); $olTable2 = new Tables\TestOptimisticLockTable(); - $olTable1->init(); - $olTable1->save($olEntity1); $olEntity2 = $olTable2->findByPk($olEntity1->id); diff --git a/tests/Traits/UpdatedAtTest.php b/tests/Traits/UpdatedAtTest.php index 5a32b09..bb149c5 100644 --- a/tests/Traits/UpdatedAtTest.php +++ b/tests/Traits/UpdatedAtTest.php @@ -13,7 +13,6 @@ public function test_trait(): void $this->assertNull($entity->updated_at); $table = new TestUpdateAtTable(); - $table->init(); $table->save($entity); $this->assertNotNull($entity->updated_at); From 268c3e853e1d727b8a1ef67961f18061559ad4db Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 4 Jun 2023 10:16:14 +0100 Subject: [PATCH 35/69] Add badges --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index a3802fd..0385896 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # What is Composite DB +[![Latest Stable Version](https://poser.pugx.org/compositephp/db/v/stable)](https://packagist.org/packages/compositephp/db) +[![Build Status](https://github.com/compositephp/db/actions/workflows/main.yml/badge.svg)](https://github.com/compositephp/db/actions) +[![Codecov](https://codecov.io/gh/compositephp/db/branch/master/graph/badge.svg)](https://codecov.io/gh/compositephp/db/) Composite DB is lightweight and fast PHP ORM, DataMapper and Table Gateway which allows you to represent your SQL tables scheme in OOP style using full power of PHP 8.1+ class syntax. From 986e8052b33865f5d68645164f5d13502cfd8629 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 4 Jun 2023 11:11:19 +0100 Subject: [PATCH 36/69] Add scrunitizer --- .scrutinizer.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .scrutinizer.yml diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..b9d8d04 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,16 @@ +checks: + php: true + +filter: + paths: + - "src/*" + +tools: + external_code_coverage: + timeout: 900 # Timeout in seconds. + runs: 2 # How many code coverage submissions Scrutinizer will wait + +build: + image: default-bionic + environment: + php: 8.1.2 \ No newline at end of file From bc902ed64d76209d551825345725db6a33b50320 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 4 Jun 2023 11:12:31 +0100 Subject: [PATCH 37/69] Change saveMany signature to return bool --- src/AbstractCachedTable.php | 8 +++---- src/AbstractTable.php | 6 ++--- tests/Table/AbstractCachedTableTest.php | 3 ++- tests/Table/AutoIncrementTableTest.php | 7 ++++-- tests/Table/CompositeTableTest.php | 32 ++++++++++++++++++++++++- 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index 247ce28..73a56d6 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -35,25 +35,23 @@ public function save(AbstractEntity &$entity): void /** * @param AbstractEntity[] $entities - * @return AbstractEntity[] * @throws \Throwable */ - public function saveMany(array $entities): array + public function saveMany(array $entities): bool { $cacheKeys = []; foreach ($entities as $entity) { $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity)); } - $result = $this->getConnection()->transactional(function() use ($entities, $cacheKeys) { + $this->getConnection()->transactional(function() use ($entities, $cacheKeys) { foreach ($entities as &$entity) { parent::save($entity); } - return $entities; }); if ($cacheKeys) { $this->cache->deleteMultiple(array_unique($cacheKeys)); } - return $result; + return true; } /** diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 7b1c3d5..c8af2b9 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -108,10 +108,10 @@ public function save(AbstractEntity &$entity): void * @param AbstractEntity[] $entities * @throws \Throwable */ - public function saveMany(array $entities): array + public function saveMany(array $entities): bool { - return $this->getConnection()->transactional(function() use ($entities) { - foreach ($entities as &$entity) { + return (bool)$this->getConnection()->transactional(function() use ($entities) { + foreach ($entities as $entity) { $this->save($entity); } return $entities; diff --git a/tests/Table/AbstractCachedTableTest.php b/tests/Table/AbstractCachedTableTest.php index 2cf2252..576d663 100644 --- a/tests/Table/AbstractCachedTableTest.php +++ b/tests/Table/AbstractCachedTableTest.php @@ -251,7 +251,8 @@ public function test_findMulti(): void $e1 = new Entities\TestAutoincrementEntity('John'); $e2 = new Entities\TestAutoincrementEntity('Constantine'); - [$e1, $e2] = $table->saveMany([$e1, $e2]); + $table->save($e1); + $table->save($e2); $multi1 = $table->findMulti([$e1->id]); $this->assertEquals($e1, $multi1[0]); diff --git a/tests/Table/AutoIncrementTableTest.php b/tests/Table/AutoIncrementTableTest.php index c8e22a1..f71394e 100644 --- a/tests/Table/AutoIncrementTableTest.php +++ b/tests/Table/AutoIncrementTableTest.php @@ -74,7 +74,8 @@ public function test_crud(AbstractTable&IAutoincrementTable $table, string $clas $e1 = new $class(Helpers\StringHelper::getUniqueName()); $e2 = new $class(Helpers\StringHelper::getUniqueName()); - [$e1, $e2] = $table->saveMany([$e1, $e2]); + $table->save($e1); + $table->save($e2); $this->assertEntityExists($table, $e1); $this->assertEntityExists($table, $e2); @@ -108,7 +109,9 @@ public function test_getMulti(): void $e2 = new Entities\TestAutoincrementEntity('name2'); $e3 = new Entities\TestAutoincrementEntity('name3'); - [$e1, $e2, $e3] = $table->saveMany([$e1, $e2, $e3]); + $table->save($e1); + $table->save($e2); + $table->save($e3); $multiResult = $table->findMulti([$e1->id, $e2->id, $e3->id]); $this->assertEquals($e1, $multiResult[$e1->id]); diff --git a/tests/Table/CompositeTableTest.php b/tests/Table/CompositeTableTest.php index 7359e56..ee4a62b 100644 --- a/tests/Table/CompositeTableTest.php +++ b/tests/Table/CompositeTableTest.php @@ -65,6 +65,36 @@ public function test_crud(AbstractTable&ICompositeTable $table, string $class): } else { $this->assertEntityNotExists($table, $entity); } + + $e1 = new $class( + user_id: mt_rand(1, 1000000), + post_id: mt_rand(1, 1000000), + message: Helpers\StringHelper::getUniqueName(), + ); + $e2 = new $class( + user_id: mt_rand(1, 1000000), + post_id: mt_rand(1, 1000000), + message: Helpers\StringHelper::getUniqueName(), + ); + + $table->saveMany([$e1, $e2]); + $this->assertEntityExists($table, $e1); + $this->assertEntityExists($table, $e2); + + $this->assertTrue($table->deleteMany([$e1, $e2])); + + if ($tableConfig->hasSoftDelete()) { + /** @var Entities\TestCompositeSdEntity $deletedEntity1 */ + $deletedEntity1 = $table->findOne(user_id: $e1->user_id, post_id: $e1->post_id); + $this->assertTrue($deletedEntity1->isDeleted()); + + /** @var Entities\TestCompositeSdEntity $deletedEntity2 */ + $deletedEntity2 = $table->findOne(user_id: $e2->user_id, post_id: $e2->post_id); + $this->assertTrue($deletedEntity2->isDeleted()); + } else { + $this->assertEntityNotExists($table, $e1); + $this->assertEntityNotExists($table, $e2); + } } public function test_getMulti(): void @@ -90,7 +120,7 @@ public function test_getMulti(): void message: Helpers\StringHelper::getUniqueName(), ); - [$e1, $e2, $e3] = $table->saveMany([$e1, $e2, $e3]); + $table->saveMany([$e1, $e2, $e3]); $multiResult = $table->findMulti([ ['user_id' => $e1->user_id, 'post_id' => $e1->post_id], From ed49b0cc0dcd885e9490ec53228fca560faf7b4f Mon Sep 17 00:00:00 2001 From: Composite PHP <38870693+compositephp@users.noreply.github.com> Date: Sat, 17 Jun 2023 09:51:46 +0100 Subject: [PATCH 38/69] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b17c9ef..206c8af 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: PHP Composer +name: build on: push: From a897c09f9c9789718200b3a14e2ea1ede30476f0 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 17 Jun 2023 09:55:34 +0100 Subject: [PATCH 39/69] update compositephp/entity dependency version --- composer.json | 2 +- tests/Table/TableConfigTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 31fe04e..1cea962 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "php": "^8.1", "ext-pdo": "*", "psr/simple-cache": "1 - 3", - "compositephp/entity": "^0.1.7", + "compositephp/entity": "^0.1.8", "doctrine/dbal": "^3.5" }, "require-dev": { diff --git a/tests/Table/TableConfigTest.php b/tests/Table/TableConfigTest.php index 63c5983..e74f318 100644 --- a/tests/Table/TableConfigTest.php +++ b/tests/Table/TableConfigTest.php @@ -27,7 +27,7 @@ public function __construct( private \DateTimeImmutable $dt = new \DateTimeImmutable(), ) {} }; - $schema = Schema::build($class::class); + $schema = new Schema($class::class); $tableConfig = TableConfig::fromEntitySchema($schema); $this->assertNotEmpty($tableConfig->connectionName); $this->assertNotEmpty($tableConfig->tableName); @@ -47,7 +47,7 @@ public function __construct( public string $str = 'abc', ) {} }; - $schema = Schema::build($class::class); + $schema = new Schema($class::class); $this->expectException(EntityException::class); TableConfig::fromEntitySchema($schema); } From e2264b66bdc17b9a9f1db5e362171fb2874563fc Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 18 Jun 2023 12:21:35 +0100 Subject: [PATCH 40/69] Add multi insert --- src/AbstractCachedTable.php | 19 ++---- src/AbstractTable.php | 65 +++++++++++++++---- src/MultiQuery/MultiInsert.php | 36 ++++++++++ tests/MultiQuery/MultiInsertTest.php | 57 ++++++++++++++++ tests/Table/AutoIncrementTableTest.php | 2 +- tests/Table/CompositeTableTest.php | 21 +++++- tests/Table/UniqueTableTest.php | 49 ++++++++++++++ .../Tables/TestCompositeSdCachedTable.php | 8 +++ tests/TestStand/Tables/TestUniqueTable.php | 9 +++ 9 files changed, 237 insertions(+), 29 deletions(-) create mode 100644 src/MultiQuery/MultiInsert.php create mode 100644 tests/MultiQuery/MultiInsertTest.php diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index 73a56d6..44e0936 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -37,21 +37,16 @@ public function save(AbstractEntity &$entity): void * @param AbstractEntity[] $entities * @throws \Throwable */ - public function saveMany(array $entities): bool + public function saveMany(array $entities): void { $cacheKeys = []; foreach ($entities as $entity) { $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity)); } - $this->getConnection()->transactional(function() use ($entities, $cacheKeys) { - foreach ($entities as &$entity) { - parent::save($entity); - } - }); + parent::saveMany($entities); if ($cacheKeys) { $this->cache->deleteMultiple(array_unique($cacheKeys)); } - return true; } /** @@ -70,23 +65,17 @@ public function delete(AbstractEntity &$entity): void * @param AbstractEntity[] $entities * @throws \Throwable */ - public function deleteMany(array $entities): bool + public function deleteMany(array $entities): void { $cacheKeys = []; foreach ($entities as $entity) { $cacheKeys = array_merge($cacheKeys, $this->collectCacheKeysByEntity($entity)); parent::delete($entity); } - $result = (bool)$this->getConnection()->transactional(function() use ($entities) { - foreach ($entities as $entity) { - parent::delete($entity); - } - return true; - }); + parent::deleteMany($entities); if ($cacheKeys) { $this->cache->deleteMultiple($cacheKeys); } - return $result; } /** diff --git a/src/AbstractTable.php b/src/AbstractTable.php index c8af2b9..9a63d2b 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -2,6 +2,7 @@ namespace Composite\DB; +use Composite\DB\MultiQuery\MultiInsert; use Composite\Entity\Helpers\DateTimeHelper; use Composite\Entity\AbstractEntity; use Composite\DB\Exceptions\DbException; @@ -47,9 +48,8 @@ public function save(AbstractEntity &$entity): void $this->config->checkEntity($entity); if ($entity->isNew()) { $connection = $this->getConnection(); - if ($this->config->hasUpdatedAt() && property_exists($entity, 'updated_at') && $entity->updated_at === null) { - $entity->updated_at = new \DateTimeImmutable(); - } + $this->checkUpdatedAt($entity); + $insertData = $this->formatData($entity->toArray()); $this->getConnection()->insert($this->getTableName(), $insertData); @@ -108,18 +108,46 @@ public function save(AbstractEntity &$entity): void * @param AbstractEntity[] $entities * @throws \Throwable */ - public function saveMany(array $entities): bool + public function saveMany(array $entities): void { - return (bool)$this->getConnection()->transactional(function() use ($entities) { + $rowsToInsert = []; + foreach ($entities as $i => $entity) { + if ($entity->isNew()) { + $this->config->checkEntity($entity); + $this->checkUpdatedAt($entity); + $rowsToInsert[] = $this->formatData($entity->toArray()); + unset($entities[$i]); + } + } + $connection = $this->getConnection(); + $connection->beginTransaction(); + try { foreach ($entities as $entity) { $this->save($entity); } - return $entities; - }); + if ($rowsToInsert) { + $chunks = array_chunk($rowsToInsert, 1000); + foreach ($chunks as $chunk) { + $multiInsert = new MultiInsert( + tableName: $this->getTableName(), + rows: $chunk, + ); + if ($multiInsert->sql) { + $stmt = $this->getConnection()->prepare($multiInsert->sql); + $stmt->executeQuery($multiInsert->parameters); + } + } + } + $connection->commit(); + } catch (\Throwable $e) { + $connection->rollBack(); + throw $e; + } } /** - * @throws EntityException + * @param AbstractEntity $entity + * @throws \Throwable */ public function delete(AbstractEntity &$entity): void { @@ -137,15 +165,21 @@ public function delete(AbstractEntity &$entity): void /** * @param AbstractEntity[] $entities + * @throws \Throwable */ - public function deleteMany(array $entities): bool + public function deleteMany(array $entities): void { - return (bool)$this->getConnection()->transactional(function() use ($entities) { + $connection = $this->getConnection(); + $connection->beginTransaction(); + try { foreach ($entities as $entity) { $this->delete($entity); } - return true; - }); + $connection->commit(); + } catch (\Throwable $e) { + $connection->rollBack(); + throw $e; + } } /** @@ -377,6 +411,13 @@ private function buildWhere(QueryBuilder $query, array $where): void } } + private function checkUpdatedAt(AbstractEntity $entity): void + { + if ($this->config->hasUpdatedAt() && property_exists($entity, 'updated_at') && $entity->updated_at === null) { + $entity->updated_at = new \DateTimeImmutable(); + } + } + /** * @param array $data * @return array diff --git a/src/MultiQuery/MultiInsert.php b/src/MultiQuery/MultiInsert.php new file mode 100644 index 0000000..b2138e4 --- /dev/null +++ b/src/MultiQuery/MultiInsert.php @@ -0,0 +1,36 @@ +sql = ''; + $this->parameters = []; + return; + } + $firstRow = reset($rows); + $columnNames = array_map(fn ($columnName) => "`$columnName`", array_keys($firstRow)); + $sql = "INSERT INTO `$tableName` (" . implode(', ', $columnNames) . ") VALUES "; + $valuesSql = $parameters = []; + + $index = 0; + foreach ($rows as $row) { + $valuePlaceholder = []; + foreach ($row as $column => $value) { + $valuePlaceholder[] = ":$column$index"; + $parameters["$column$index"] = $value; + } + $valuesSql[] = '(' . implode(', ', $valuePlaceholder) . ')'; + $index++; + } + + $sql .= implode(', ', $valuesSql); + $this->sql = $sql . ';'; + $this->parameters = $parameters; + } +} \ No newline at end of file diff --git a/tests/MultiQuery/MultiInsertTest.php b/tests/MultiQuery/MultiInsertTest.php new file mode 100644 index 0000000..012736f --- /dev/null +++ b/tests/MultiQuery/MultiInsertTest.php @@ -0,0 +1,57 @@ +assertEquals($expectedSql, $multiInserter->sql); + $this->assertEquals($expectedParameters, $multiInserter->parameters); + } + + public static function multiInsertQuery_dataProvider() + { + return [ + [ + 'testTable', + [], + '', + [] + ], + [ + 'testTable', + [ + ['a' => 'value1_1', 'b' => 'value2_1'], + ], + "INSERT INTO `testTable` (`a`, `b`) VALUES (:a0, :b0);", + ['a0' => 'value1_1', 'b0' => 'value2_1'] + ], + [ + 'testTable', + [ + ['a' => 'value1_1', 'b' => 'value2_1'], + ['a' => 'value1_2', 'b' => 'value2_2'] + ], + "INSERT INTO `testTable` (`a`, `b`) VALUES (:a0, :b0), (:a1, :b1);", + ['a0' => 'value1_1', 'b0' => 'value2_1', 'a1' => 'value1_2', 'b1' => 'value2_2'] + ], + [ + 'testTable', + [ + ['column1' => 'value1_1'], + ['column1' => 123] + ], + "INSERT INTO `testTable` (`column1`) VALUES (:column10), (:column11);", + ['column10' => 'value1_1', 'column11' => 123] + ] + ]; + } +} diff --git a/tests/Table/AutoIncrementTableTest.php b/tests/Table/AutoIncrementTableTest.php index f71394e..ec85178 100644 --- a/tests/Table/AutoIncrementTableTest.php +++ b/tests/Table/AutoIncrementTableTest.php @@ -85,7 +85,7 @@ public function test_crud(AbstractTable&IAutoincrementTable $table, string $clas $preLastEntity = $table->findRecent(1, 1); $this->assertEquals($e1, $preLastEntity[0]); - $this->assertTrue($table->deleteMany([$e1, $e2])); + $table->deleteMany([$e1, $e2]); if ($tableConfig->hasSoftDelete()) { /** @var Entities\TestAutoincrementSdEntity $deletedEntity1 */ diff --git a/tests/Table/CompositeTableTest.php b/tests/Table/CompositeTableTest.php index ee4a62b..afc9f97 100644 --- a/tests/Table/CompositeTableTest.php +++ b/tests/Table/CompositeTableTest.php @@ -78,10 +78,25 @@ public function test_crud(AbstractTable&ICompositeTable $table, string $class): ); $table->saveMany([$e1, $e2]); + $e1->resetChangedColumns(); + $e2->resetChangedColumns(); + $this->assertEntityExists($table, $e1); $this->assertEntityExists($table, $e2); - $this->assertTrue($table->deleteMany([$e1, $e2])); + if ($tableConfig->hasSoftDelete()) { + $e1->message = 'Exception'; + $exceptionThrown = false; + try { + $table->deleteMany([$e1, $e2]); + } catch (\Exception) { + $exceptionThrown = true; + } + $this->assertTrue($exceptionThrown); + $e1->message = Helpers\StringHelper::getUniqueName(); + } + + $table->deleteMany([$e1, $e2]); if ($tableConfig->hasSoftDelete()) { /** @var Entities\TestCompositeSdEntity $deletedEntity1 */ @@ -122,6 +137,10 @@ public function test_getMulti(): void $table->saveMany([$e1, $e2, $e3]); + $e1->resetChangedColumns(); + $e2->resetChangedColumns(); + $e3->resetChangedColumns(); + $multiResult = $table->findMulti([ ['user_id' => $e1->user_id, 'post_id' => $e1->post_id], ['user_id' => $e2->user_id, 'post_id' => $e2->post_id], diff --git a/tests/Table/UniqueTableTest.php b/tests/Table/UniqueTableTest.php index 72b277a..87b954f 100644 --- a/tests/Table/UniqueTableTest.php +++ b/tests/Table/UniqueTableTest.php @@ -65,6 +65,55 @@ public function test_crud(AbstractTable&IUniqueTable $table, string $class): voi } } + public function test_multiSave(): void + { + $e1 = new Entities\TestUniqueEntity( + id: uniqid(), + name: Helpers\StringHelper::getUniqueName(), + ); + $e2 = new Entities\TestUniqueEntity( + id: uniqid(), + name: Helpers\StringHelper::getUniqueName(), + ); + $e3 = new Entities\TestUniqueEntity( + id: uniqid(), + name: Helpers\StringHelper::getUniqueName(), + ); + $e4 = new Entities\TestUniqueEntity( + id: uniqid(), + name: Helpers\StringHelper::getUniqueName(), + ); + $table = new Tables\TestUniqueTable(); + $table->saveMany([$e1, $e2]); + + $this->assertEntityExists($table, $e1); + $this->assertEntityExists($table, $e2); + + $e1->resetChangedColumns(); + $e2->resetChangedColumns(); + + $e1->name = 'Exception'; + + $exceptionThrown = false; + try { + $table->saveMany([$e1, $e2, $e3, $e4]); + } catch (\Exception) { + $exceptionThrown = true; + } + $this->assertTrue($exceptionThrown); + $this->assertEntityNotExists($table, $e3); + $this->assertEntityNotExists($table, $e4); + + $e1->name = 'NonException'; + + $table->saveMany([$e1, $e2, $e3, $e4]); + + $this->assertEntityExists($table, $e1); + $this->assertEntityExists($table, $e2); + $this->assertEntityExists($table, $e3); + $this->assertEntityExists($table, $e4); + } + private function assertEntityExists(IUniqueTable $table, Entities\TestUniqueEntity $entity): void { $this->assertNotNull($table->findByPk($entity->id)); diff --git a/tests/TestStand/Tables/TestCompositeSdCachedTable.php b/tests/TestStand/Tables/TestCompositeSdCachedTable.php index 46846dc..f4938f1 100644 --- a/tests/TestStand/Tables/TestCompositeSdCachedTable.php +++ b/tests/TestStand/Tables/TestCompositeSdCachedTable.php @@ -15,6 +15,14 @@ public function __construct(\Psr\SimpleCache\CacheInterface $cache) (new TestCompositeSdTable())->init(); } + public function save(AbstractEntity|TestCompositeSdEntity &$entity): void + { + if ($entity->message === 'Exception') { + throw new \Exception('Test Exception'); + } + parent::save($entity); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestCompositeSdEntity::schema()); diff --git a/tests/TestStand/Tables/TestUniqueTable.php b/tests/TestStand/Tables/TestUniqueTable.php index 137a24a..7d26fca 100644 --- a/tests/TestStand/Tables/TestUniqueTable.php +++ b/tests/TestStand/Tables/TestUniqueTable.php @@ -6,6 +6,7 @@ use Composite\DB\TableConfig; use Composite\DB\Tests\TestStand\Entities\TestUniqueEntity; use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable; +use Composite\Entity\AbstractEntity; class TestUniqueTable extends AbstractTable implements IUniqueTable { @@ -15,6 +16,14 @@ public function __construct() $this->init(); } + public function save(AbstractEntity|TestUniqueEntity &$entity): void + { + if ($entity->name === 'Exception') { + throw new \Exception('Test Exception'); + } + parent::save($entity); + } + protected function getConfig(): TableConfig { return TableConfig::fromEntitySchema(TestUniqueEntity::schema()); From e4543eb348371a94575caec9522515032153db89 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 18 Jun 2023 12:49:21 +0100 Subject: [PATCH 41/69] PHPStan fixes --- src/AbstractTable.php | 12 +++++----- src/MultiQuery/MultiInsert.php | 36 +++++++++++++++++++--------- src/TableConfig.php | 1 + tests/MultiQuery/MultiInsertTest.php | 4 ++-- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 9a63d2b..22b2720 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -132,9 +132,9 @@ public function saveMany(array $entities): void tableName: $this->getTableName(), rows: $chunk, ); - if ($multiInsert->sql) { - $stmt = $this->getConnection()->prepare($multiInsert->sql); - $stmt->executeQuery($multiInsert->parameters); + if ($multiInsert->getSql()) { + $stmt = $this->getConnection()->prepare($multiInsert->getSql()); + $stmt->executeQuery($multiInsert->getParameters()); } } } @@ -223,7 +223,7 @@ protected function findOneInternal(array $where): ?array /** * @param array> $pkList - * @return array + * @return array> * @throws DbException * @throws EntityException * @throws \Doctrine\DBAL\Exception @@ -253,7 +253,7 @@ protected function findMultiInternal(array $pkList): array $query = $this->select(); $expressions = []; foreach ($pkList as $i => $pkArray) { - if (!is_array($pkArray) || array_is_list($pkArray)) { + if (!is_array($pkArray)) { throw new DbException('For tables with composite keys, input array must consist associative arrays'); } $pkOrExpr = []; @@ -276,7 +276,7 @@ protected function findMultiInternal(array $pkList): array /** * @param array $whereParams * @param array|string $orderBy - * @return array + * @return list> * @throws \Doctrine\DBAL\Exception */ protected function findAllInternal( diff --git a/src/MultiQuery/MultiInsert.php b/src/MultiQuery/MultiInsert.php index b2138e4..04de6a8 100644 --- a/src/MultiQuery/MultiInsert.php +++ b/src/MultiQuery/MultiInsert.php @@ -4,33 +4,47 @@ class MultiInsert { - public readonly string $sql; - public readonly array $parameters; + private string $sql = ''; + /** @var array */ + private array $parameters = []; + /** + * @param string $tableName + * @param list> $rows + */ public function __construct(string $tableName, array $rows) { if (!$rows) { - $this->sql = ''; - $this->parameters = []; return; } $firstRow = reset($rows); - $columnNames = array_map(fn ($columnName) => "`$columnName`", array_keys($firstRow)); - $sql = "INSERT INTO `$tableName` (" . implode(', ', $columnNames) . ") VALUES "; - $valuesSql = $parameters = []; + $columnNames = array_map(fn($columnName) => "`$columnName`", array_keys($firstRow)); + $this->sql = "INSERT INTO `$tableName` (" . implode(', ', $columnNames) . ") VALUES "; + $valuesSql = []; $index = 0; foreach ($rows as $row) { $valuePlaceholder = []; foreach ($row as $column => $value) { $valuePlaceholder[] = ":$column$index"; - $parameters["$column$index"] = $value; + $this->parameters["$column$index"] = $value; } $valuesSql[] = '(' . implode(', ', $valuePlaceholder) . ')'; $index++; } - $sql .= implode(', ', $valuesSql); - $this->sql = $sql . ';'; - $this->parameters = $parameters; + $this->sql .= implode(', ', $valuesSql) . ';'; + } + + public function getSql(): string + { + return $this->sql; + } + + /** + * @return array + */ + public function getParameters(): array + { + return $this->parameters; } } \ No newline at end of file diff --git a/src/TableConfig.php b/src/TableConfig.php index d11ea63..a33b693 100644 --- a/src/TableConfig.php +++ b/src/TableConfig.php @@ -9,6 +9,7 @@ class TableConfig { + /** @var array */ private readonly array $entityTraits; /** diff --git a/tests/MultiQuery/MultiInsertTest.php b/tests/MultiQuery/MultiInsertTest.php index 012736f..e2c1237 100644 --- a/tests/MultiQuery/MultiInsertTest.php +++ b/tests/MultiQuery/MultiInsertTest.php @@ -13,8 +13,8 @@ public function test_multiInsertQuery($tableName, $rows, $expectedSql, $expected { $multiInserter = new MultiInsert($tableName, $rows); - $this->assertEquals($expectedSql, $multiInserter->sql); - $this->assertEquals($expectedParameters, $multiInserter->parameters); + $this->assertEquals($expectedSql, $multiInserter->getSql()); + $this->assertEquals($expectedParameters, $multiInserter->getParameters()); } public static function multiInsertQuery_dataProvider() From 946da1640c1a8081c00aaf91008ce248e1ea5d19 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 1 Jul 2023 22:15:53 +0100 Subject: [PATCH 42/69] Move multiselect to separate class MultiSelecy --- src/AbstractTable.php | 44 ++------------------- src/MultiQuery/MultiSelect.php | 64 +++++++++++++++++++++++++++++++ tests/Table/AbstractTableTest.php | 44 +++++++++++++++++++++ 3 files changed, 112 insertions(+), 40 deletions(-) create mode 100644 src/MultiQuery/MultiSelect.php diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 22b2720..cb9b850 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -3,6 +3,7 @@ namespace Composite\DB; use Composite\DB\MultiQuery\MultiInsert; +use Composite\DB\MultiQuery\MultiSelect; use Composite\Entity\Helpers\DateTimeHelper; use Composite\Entity\AbstractEntity; use Composite\DB\Exceptions\DbException; @@ -225,7 +226,6 @@ protected function findOneInternal(array $where): ?array * @param array> $pkList * @return array> * @throws DbException - * @throws EntityException * @throws \Doctrine\DBAL\Exception */ protected function findMultiInternal(array $pkList): array @@ -233,44 +233,8 @@ protected function findMultiInternal(array $pkList): array if (!$pkList) { return []; } - /** @var class-string $class */ - $class = $this->config->entityClass; - - $pkColumns = []; - foreach ($this->config->primaryKeys as $primaryKeyName) { - $pkColumns[$primaryKeyName] = $class::schema()->getColumn($primaryKeyName); - } - if (count($pkColumns) === 1) { - if (!array_is_list($pkList)) { - throw new DbException('Input argument $pkList must be list'); - } - /** @var \Composite\Entity\Columns\AbstractColumn $pkColumn */ - $pkColumn = reset($pkColumns); - $preparedPkValues = array_map(fn ($pk) => $pkColumn->uncast($pk), $pkList); - $query = $this->select(); - $this->buildWhere($query, [$pkColumn->name => $preparedPkValues]); - } else { - $query = $this->select(); - $expressions = []; - foreach ($pkList as $i => $pkArray) { - if (!is_array($pkArray)) { - throw new DbException('For tables with composite keys, input array must consist associative arrays'); - } - $pkOrExpr = []; - foreach ($pkArray as $pkName => $pkValue) { - if (is_string($pkName) && isset($pkColumns[$pkName])) { - $preparedPkValue = $pkColumns[$pkName]->cast($pkValue); - $pkOrExpr[] = $query->expr()->eq($pkName, ':' . $pkName . $i); - $query->setParameter($pkName . $i, $preparedPkValue); - } - } - if ($pkOrExpr) { - $expressions[] = $query->expr()->and(...$pkOrExpr); - } - } - $query->where($query->expr()->or(...$expressions)); - } - return $query->executeQuery()->fetchAllAssociative(); + $multiSelect = new MultiSelect($this->getConnection(), $this->config, $pkList); + return $multiSelect->getQueryBuilder()->executeQuery()->fetchAllAssociative(); } /** @@ -395,7 +359,7 @@ protected function select(string $select = '*'): QueryBuilder /** * @param array $where */ - private function buildWhere(QueryBuilder $query, array $where): void + private function buildWhere(\Doctrine\DBAL\Query\QueryBuilder $query, array $where): void { foreach ($where as $column => $value) { if ($value === null) { diff --git a/src/MultiQuery/MultiSelect.php b/src/MultiQuery/MultiSelect.php new file mode 100644 index 0000000..59459eb --- /dev/null +++ b/src/MultiQuery/MultiSelect.php @@ -0,0 +1,64 @@ +createQueryBuilder()->select('*')->from($tableConfig->tableName); + /** @var class-string $class */ + $class = $tableConfig->entityClass; + + $pkColumns = []; + foreach ($tableConfig->primaryKeys as $primaryKeyName) { + $pkColumns[$primaryKeyName] = $class::schema()->getColumn($primaryKeyName); + } + + if (count($pkColumns) === 1) { + if (!array_is_list($condition)) { + throw new DbException('Input argument $pkList must be list'); + } + /** @var \Composite\Entity\Columns\AbstractColumn $pkColumn */ + $pkColumn = reset($pkColumns); + $preparedPkValues = array_map(fn ($pk) => $pkColumn->uncast($pk), $condition); + $query->andWhere($query->expr()->in($pkColumn->name, $preparedPkValues)); + } else { + $expressions = []; + foreach ($condition as $i => $pkArray) { + if (!is_array($pkArray)) { + throw new DbException('For tables with composite keys, input array must consist associative arrays'); + } + $pkOrExpr = []; + foreach ($pkArray as $pkName => $pkValue) { + if (is_string($pkName) && isset($pkColumns[$pkName])) { + $preparedPkValue = $pkColumns[$pkName]->cast($pkValue); + $pkOrExpr[] = $query->expr()->eq($pkName, ':' . $pkName . $i); + $query->setParameter($pkName . $i, $preparedPkValue); + } + } + if ($pkOrExpr) { + $expressions[] = $query->expr()->and(...$pkOrExpr); + } + } + $query->where($query->expr()->or(...$expressions)); + } + $this->queryBuilder = $query; + } + + public function getQueryBuilder(): QueryBuilder + { + return $this->queryBuilder; + } +} \ No newline at end of file diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php index 6923fdb..c426c56 100644 --- a/tests/Table/AbstractTableTest.php +++ b/tests/Table/AbstractTableTest.php @@ -101,4 +101,48 @@ public function test_illegalCreateEntity(): void $empty = $table->buildEntities(['abc']); $this->assertEmpty($empty); } + + /** + * @dataProvider buildWhere_dataProvider + */ + public function test_buildWhere($where, $expectedSQL, $expectedParams) + { + $table = new Tables\TestStrictTable(); + + $selectReflection = new \ReflectionMethod($table, 'select'); + $selectReflection->setAccessible(true); + + $queryBuilder = $selectReflection->invoke($table); + + $buildWhereReflection = new \ReflectionMethod($table, 'buildWhere'); + $buildWhereReflection->setAccessible(true); + + $buildWhereReflection->invokeArgs($table, [$queryBuilder, $where]); + + $this->assertEquals($expectedSQL, $queryBuilder->getSQL()); + } + + public static function buildWhere_dataProvider(): array + { + return [ + // Test when value is null + [ + ['column1' => null], + 'SELECT * FROM Strict WHERE column1 IS NULL', + [] + ], + // Test when value is an array + [ + ['column1' => [1, 2, 3]], + 'SELECT * FROM Strict WHERE column1 IN (1, 2, 3)', + [1, 2, 3] + ], + // Test when value is a single value + [ + ['column1' => 'value1'], + 'SELECT * FROM Strict WHERE column1 = :column1', + ['value1'] + ], + ]; + } } \ No newline at end of file From 06cd5570eb60cc424c5230e8d214e5d146f9fb7d Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 14 Oct 2023 12:40:16 +0100 Subject: [PATCH 43/69] Add .github folder to export-ignore --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index af43e6b..1989899 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ /.gitattributes export-ignore /.gitignore export-ignore +/.github export-ignore /doc export-ignore /phpunit.xml export-ignore /tests export-ignore From 6e5824dd119593ce64db31032b6475a93bb12ecc Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 14 Oct 2023 14:03:02 +0100 Subject: [PATCH 44/69] Add UUID support --- composer.json | 2 +- src/AbstractCachedTable.php | 4 +-- src/AbstractTable.php | 3 +- tests/Table/AbstractCachedTableTest.php | 28 ++++++++++--------- tests/Table/AbstractTableTest.php | 17 ++++++----- tests/Table/UniqueTableTest.php | 11 ++++---- tests/TestStand/Entities/TestUniqueEntity.php | 3 +- tests/TestStand/Interfaces/IUniqueTable.php | 3 +- .../Tables/TestUniqueCachedTable.php | 3 +- .../Tables/TestUniqueSdCachedTable.php | 3 +- tests/TestStand/Tables/TestUniqueSdTable.php | 5 ++-- tests/TestStand/Tables/TestUniqueTable.php | 3 +- 12 files changed, 49 insertions(+), 36 deletions(-) diff --git a/composer.json b/composer.json index 1cea962..5bfced2 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "php": "^8.1", "ext-pdo": "*", "psr/simple-cache": "1 - 3", - "compositephp/entity": "^0.1.8", + "compositephp/entity": "^0.1.9", "doctrine/dbal": "^3.5" }, "require-dev": { diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index 44e0936..857f9b9 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -5,6 +5,7 @@ use Composite\DB\Exceptions\DbException; use Composite\Entity\AbstractEntity; use Psr\SimpleCache\CacheInterface; +use Ramsey\Uuid\UuidInterface; abstract class AbstractCachedTable extends AbstractTable { @@ -196,9 +197,8 @@ protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $t /** * @param string|int|array|AbstractEntity $keyOrEntity - * @throws \Composite\Entity\Exceptions\EntityException */ - protected function getOneCacheKey(string|int|array|AbstractEntity $keyOrEntity): string + protected function getOneCacheKey(string|int|array|AbstractEntity|UuidInterface $keyOrEntity): string { if (!is_array($keyOrEntity)) { $condition = $this->getPkCondition($keyOrEntity); diff --git a/src/AbstractTable.php b/src/AbstractTable.php index cb9b850..822249b 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -11,6 +11,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Query\QueryBuilder; +use Ramsey\Uuid\UuidInterface; abstract class AbstractTable { @@ -330,7 +331,7 @@ final protected function createEntities(mixed $data, ?string $keyColumnName = nu * @return array * @throws EntityException */ - protected function getPkCondition(int|string|array|AbstractEntity $data): array + protected function getPkCondition(int|string|array|AbstractEntity|UuidInterface $data): array { $condition = []; if ($data instanceof AbstractEntity) { diff --git a/tests/Table/AbstractCachedTableTest.php b/tests/Table/AbstractCachedTableTest.php index 576d663..6c65846 100644 --- a/tests/Table/AbstractCachedTableTest.php +++ b/tests/Table/AbstractCachedTableTest.php @@ -9,12 +9,15 @@ use Composite\DB\Tests\TestStand\Tables; use Composite\Entity\AbstractEntity; use Composite\DB\Tests\Helpers; +use Ramsey\Uuid\Uuid; final class AbstractCachedTableTest extends \PHPUnit\Framework\TestCase { public static function getOneCacheKey_dataProvider(): array { $cache = Helpers\CacheHelper::getCache(); + $uuid = Uuid::uuid4(); + $uuidCacheKey = str_replace('-', '_', (string)$uuid); return [ [ new Tables\TestAutoincrementCachedTable($cache), @@ -28,16 +31,13 @@ public static function getOneCacheKey_dataProvider(): array ], [ new Tables\TestUniqueCachedTable($cache), - new Entities\TestUniqueEntity(id: '123abc', name: 'John'), - 'sqlite.TestUnique.v1.o.id_123abc', + new Entities\TestUniqueEntity(id: $uuid, name: 'John'), + 'sqlite.TestUnique.v1.o.id_' . $uuidCacheKey, ], [ - new Tables\TestUniqueCachedTable($cache), - new Entities\TestUniqueEntity( - id: implode('', array_fill(0, 100, 'a')), - name: 'John', - ), - 'ed66f06444d851a981a9ddcecbbf4d5860cd3131', + new Tables\TestCompositeCachedTable($cache), + new Entities\TestCompositeEntity(user_id: PHP_INT_MAX, post_id: PHP_INT_MAX, message: 'Text'), + '69b5bbf599d78f0274feb5cb0e6424f35cca0b57', ], ]; } @@ -45,7 +45,7 @@ public static function getOneCacheKey_dataProvider(): array /** * @dataProvider getOneCacheKey_dataProvider */ - public function test_getOneCacheKey(AbstractTable $table, AbstractEntity $object, string $expected): void + public function test_getOneCacheKey(AbstractCachedTable $table, AbstractEntity $object, string $expected): void { $reflectionMethod = new \ReflectionMethod($table, 'getOneCacheKey'); $actual = $reflectionMethod->invoke($table, $object); @@ -194,6 +194,8 @@ public function test_getCustomCacheKey(array $parts, string $expected): void public static function collectCacheKeysByEntity_dataProvider(): array { + $uuid = Uuid::uuid4(); + $uuidCacheKey = str_replace('-', '_', (string)$uuid); return [ [ new Entities\TestAutoincrementEntity(name: 'foo'), @@ -215,21 +217,21 @@ public static function collectCacheKeysByEntity_dataProvider(): array ], ], [ - new Entities\TestUniqueEntity(id: '123abc', name: 'foo'), + new Entities\TestUniqueEntity(id: $uuid, name: 'foo'), new Tables\TestUniqueCachedTable(Helpers\CacheHelper::getCache()), [ 'sqlite.TestUnique.v1.l.name_eq_foo', 'sqlite.TestUnique.v1.c.name_eq_foo', - 'sqlite.TestUnique.v1.o.id_123abc', + 'sqlite.TestUnique.v1.o.id_' . $uuidCacheKey, ], ], [ - Entities\TestUniqueEntity::fromArray(['id' => '456def', 'name' => 'bar']), + Entities\TestUniqueEntity::fromArray(['id' => $uuid, 'name' => 'bar']), new Tables\TestUniqueCachedTable(Helpers\CacheHelper::getCache()), [ 'sqlite.TestUnique.v1.l.name_eq_bar', 'sqlite.TestUnique.v1.c.name_eq_bar', - 'sqlite.TestUnique.v1.o.id_456def', + 'sqlite.TestUnique.v1.o.id_' . $uuidCacheKey, ], ], ]; diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php index c426c56..5c45eca 100644 --- a/tests/Table/AbstractTableTest.php +++ b/tests/Table/AbstractTableTest.php @@ -7,11 +7,14 @@ use Composite\DB\Tests\TestStand\Tables; use Composite\Entity\AbstractEntity; use Composite\Entity\Exceptions\EntityException; +use Ramsey\Uuid\Uuid; +use Ramsey\Uuid\UuidInterface; final class AbstractTableTest extends \PHPUnit\Framework\TestCase { public static function getPkCondition_dataProvider(): array { + $uuid = Uuid::uuid4(); return [ [ new Tables\TestAutoincrementTable(), @@ -35,13 +38,13 @@ public static function getPkCondition_dataProvider(): array ], [ new Tables\TestUniqueTable(), - new Entities\TestUniqueEntity(id: '123abc', name: 'John'), - ['id' => '123abc'], + new Entities\TestUniqueEntity(id: $uuid, name: 'John'), + ['id' => $uuid->toString()], ], [ new Tables\TestUniqueTable(), - '123abc', - ['id' => '123abc'], + $uuid, + ['id' => $uuid->toString()], ], [ new Tables\TestAutoincrementSdTable(), @@ -55,8 +58,8 @@ public static function getPkCondition_dataProvider(): array ], [ new Tables\TestUniqueSdTable(), - new Entities\TestUniqueSdEntity(id: '123abc', name: 'John'), - ['id' => '123abc'], + new Entities\TestUniqueSdEntity(id: $uuid, name: 'John'), + ['id' => $uuid->toString()], ], ]; } @@ -64,7 +67,7 @@ public static function getPkCondition_dataProvider(): array /** * @dataProvider getPkCondition_dataProvider */ - public function test_getPkCondition(AbstractTable $table, int|string|array|AbstractEntity $object, array $expected): void + public function test_getPkCondition(AbstractTable $table, int|string|array|AbstractEntity|UuidInterface $object, array $expected): void { $reflectionMethod = new \ReflectionMethod($table, 'getPkCondition'); $actual = $reflectionMethod->invoke($table, $object); diff --git a/tests/Table/UniqueTableTest.php b/tests/Table/UniqueTableTest.php index 87b954f..e9cd434 100644 --- a/tests/Table/UniqueTableTest.php +++ b/tests/Table/UniqueTableTest.php @@ -8,6 +8,7 @@ use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Tables; use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable; +use Ramsey\Uuid\Uuid; final class UniqueTableTest extends \PHPUnit\Framework\TestCase { @@ -43,7 +44,7 @@ public function test_crud(AbstractTable&IUniqueTable $table, string $class): voi $tableConfig = TableConfig::fromEntitySchema($class::schema()); $entity = new $class( - id: uniqid(), + id: Uuid::uuid4(), name: Helpers\StringHelper::getUniqueName(), ); $this->assertEntityNotExists($table, $entity); @@ -68,19 +69,19 @@ public function test_crud(AbstractTable&IUniqueTable $table, string $class): voi public function test_multiSave(): void { $e1 = new Entities\TestUniqueEntity( - id: uniqid(), + id: Uuid::uuid4(), name: Helpers\StringHelper::getUniqueName(), ); $e2 = new Entities\TestUniqueEntity( - id: uniqid(), + id: Uuid::uuid4(), name: Helpers\StringHelper::getUniqueName(), ); $e3 = new Entities\TestUniqueEntity( - id: uniqid(), + id: Uuid::uuid4(), name: Helpers\StringHelper::getUniqueName(), ); $e4 = new Entities\TestUniqueEntity( - id: uniqid(), + id: Uuid::uuid4(), name: Helpers\StringHelper::getUniqueName(), ); $table = new Tables\TestUniqueTable(); diff --git a/tests/TestStand/Entities/TestUniqueEntity.php b/tests/TestStand/Entities/TestUniqueEntity.php index 7b676bd..2a6138b 100644 --- a/tests/TestStand/Entities/TestUniqueEntity.php +++ b/tests/TestStand/Entities/TestUniqueEntity.php @@ -5,13 +5,14 @@ use Composite\DB\Attributes\{PrimaryKey}; use Composite\DB\Attributes\Table; use Composite\Entity\AbstractEntity; +use Ramsey\Uuid\UuidInterface; #[Table(connection: 'sqlite', name: 'TestUnique')] class TestUniqueEntity extends AbstractEntity { public function __construct( #[PrimaryKey] - public readonly string $id, + public readonly UuidInterface $id, public string $name, public readonly \DateTimeImmutable $created_at = new \DateTimeImmutable(), ) {} diff --git a/tests/TestStand/Interfaces/IUniqueTable.php b/tests/TestStand/Interfaces/IUniqueTable.php index b0e09dd..197beaf 100644 --- a/tests/TestStand/Interfaces/IUniqueTable.php +++ b/tests/TestStand/Interfaces/IUniqueTable.php @@ -4,10 +4,11 @@ use Composite\DB\Tests\TestStand\Entities\TestCompositeEntity; use Composite\DB\Tests\TestStand\Entities\TestUniqueEntity; +use Ramsey\Uuid\UuidInterface; interface IUniqueTable { - public function findByPk(string $id): ?TestUniqueEntity; + public function findByPk(UuidInterface $id): ?TestUniqueEntity; /** * @return TestCompositeEntity[] */ diff --git a/tests/TestStand/Tables/TestUniqueCachedTable.php b/tests/TestStand/Tables/TestUniqueCachedTable.php index 46c5e55..7cb7fa3 100644 --- a/tests/TestStand/Tables/TestUniqueCachedTable.php +++ b/tests/TestStand/Tables/TestUniqueCachedTable.php @@ -7,6 +7,7 @@ use Composite\DB\Tests\TestStand\Entities\TestUniqueEntity; use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable; use Composite\Entity\AbstractEntity; +use Ramsey\Uuid\UuidInterface; class TestUniqueCachedTable extends AbstractCachedTable implements IUniqueTable { @@ -29,7 +30,7 @@ protected function getFlushCacheKeys(TestUniqueEntity|AbstractEntity $entity): a ]; } - public function findByPk(string $id): ?TestUniqueEntity + public function findByPk(UuidInterface $id): ?TestUniqueEntity { return $this->createEntity($this->findByPkInternal($id)); } diff --git a/tests/TestStand/Tables/TestUniqueSdCachedTable.php b/tests/TestStand/Tables/TestUniqueSdCachedTable.php index 8c60a0c..016133d 100644 --- a/tests/TestStand/Tables/TestUniqueSdCachedTable.php +++ b/tests/TestStand/Tables/TestUniqueSdCachedTable.php @@ -7,6 +7,7 @@ use Composite\DB\Tests\TestStand\Entities\TestUniqueSdEntity; use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable; use Composite\Entity\AbstractEntity; +use Ramsey\Uuid\UuidInterface; class TestUniqueSdCachedTable extends AbstractCachedTable implements IUniqueTable { @@ -29,7 +30,7 @@ protected function getFlushCacheKeys(TestUniqueSdEntity|AbstractEntity $entity): ]; } - public function findByPk(string $id): ?TestUniqueSdEntity + public function findByPk(UuidInterface $id): ?TestUniqueSdEntity { return $this->createEntity($this->findByPkInternal($id)); } diff --git a/tests/TestStand/Tables/TestUniqueSdTable.php b/tests/TestStand/Tables/TestUniqueSdTable.php index 33cbf62..df078b9 100644 --- a/tests/TestStand/Tables/TestUniqueSdTable.php +++ b/tests/TestStand/Tables/TestUniqueSdTable.php @@ -4,6 +4,7 @@ use Composite\DB\TableConfig; use Composite\DB\Tests\TestStand\Entities\TestUniqueSdEntity; +use Ramsey\Uuid\UuidInterface; class TestUniqueSdTable extends TestUniqueTable { @@ -18,7 +19,7 @@ protected function getConfig(): TableConfig return TableConfig::fromEntitySchema(TestUniqueSdEntity::schema()); } - public function findByPk(string $id): ?TestUniqueSdEntity + public function findByPk(UuidInterface $id): ?TestUniqueSdEntity { return $this->createEntity($this->findByPkInternal($id)); } @@ -40,7 +41,7 @@ public function init(): bool " CREATE TABLE IF NOT EXISTS {$this->getTableName()} ( - `id` VARCHAR(255) NOT NULL, + `id` VARCHAR(32) NOT NULL, `name` VARCHAR(255) NOT NULL, `created_at` TIMESTAMP NOT NULL, `deleted_at` TIMESTAMP NULL DEFAULT NULL, diff --git a/tests/TestStand/Tables/TestUniqueTable.php b/tests/TestStand/Tables/TestUniqueTable.php index 7d26fca..7e223d9 100644 --- a/tests/TestStand/Tables/TestUniqueTable.php +++ b/tests/TestStand/Tables/TestUniqueTable.php @@ -7,6 +7,7 @@ use Composite\DB\Tests\TestStand\Entities\TestUniqueEntity; use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable; use Composite\Entity\AbstractEntity; +use Ramsey\Uuid\UuidInterface; class TestUniqueTable extends AbstractTable implements IUniqueTable { @@ -29,7 +30,7 @@ protected function getConfig(): TableConfig return TableConfig::fromEntitySchema(TestUniqueEntity::schema()); } - public function findByPk(string $id): ?TestUniqueEntity + public function findByPk(UuidInterface $id): ?TestUniqueEntity { return $this->createEntity($this->findByPkInternal($id)); } From 0667a416e4eb7c87f1978fec7f04ff51e30b5f0b Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 21 Oct 2023 14:07:41 +0100 Subject: [PATCH 45/69] - Improve OptimisticLock to work with 1 query - Add orderBy param to findOneInternal method --- src/AbstractTable.php | 64 +++++++++++-------- src/Exceptions/LockException.php | 7 ++ src/Traits/OptimisticLock.php | 12 +++- .../Tables/TestOptimisticLockTable.php | 2 +- tests/Traits/OptimisticLockTest.php | 2 +- 5 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 src/Exceptions/LockException.php diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 822249b..5901139 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -73,22 +73,23 @@ public function save(AbstractEntity &$entity): void $changedColumns['updated_at'] = DateTimeHelper::dateTimeToString($entity->updated_at); } - if ($this->config->hasOptimisticLock() && isset($entity->version)) { - $currentVersion = $entity->version; + + if ($this->config->hasOptimisticLock() + && method_exists($entity, 'getVersion') + && method_exists($entity, 'incrementVersion')) { + $where['lock_version'] = $entity->getVersion(); + $entity->incrementVersion(); + $changedColumns['lock_version'] = $entity->getVersion(); + try { $connection->beginTransaction(); - $connection->update( + $versionUpdated = $connection->update( $this->getTableName(), $changedColumns, $where ); - $versionUpdated = $connection->update( - $this->getTableName(), - ['version' => $currentVersion + 1], - $where + ['version' => $currentVersion] - ); if (!$versionUpdated) { - throw new DbException('Failed to update entity version, concurrency modification, rolling back.'); + throw new Exceptions\LockException('Failed to update entity version, concurrency modification, rolling back.'); } $connection->commit(); } catch (\Throwable $e) { @@ -213,13 +214,15 @@ protected function findByPkInternal(mixed $pk): ?array /** * @param array $where + * @param array|string $orderBy * @return array|null * @throws \Doctrine\DBAL\Exception */ - protected function findOneInternal(array $where): ?array + protected function findOneInternal(array $where, array|string $orderBy = []): ?array { $query = $this->select(); $this->buildWhere($query, $where); + $this->applyOrderBy($query, $orderBy); return $query->fetchAssociative() ?: null; } @@ -259,22 +262,7 @@ protected function findAllInternal( $query->setParameter($param, $value); } } - if ($orderBy) { - if (is_array($orderBy)) { - foreach ($orderBy as $column => $direction) { - $query->addOrderBy($column, $direction); - } - } else { - foreach (explode(',', $orderBy) as $orderByPart) { - $orderByPart = trim($orderByPart); - if (preg_match('/(.+)\s(asc|desc)$/i', $orderByPart, $orderByPartMatch)) { - $query->addOrderBy($orderByPartMatch[1], $orderByPartMatch[2]); - } else { - $query->addOrderBy($orderByPart); - } - } - } - } + $this->applyOrderBy($query, $orderBy); if ($limit > 0) { $query->setMaxResults($limit); } @@ -398,4 +386,28 @@ private function formatData(array $data): array } return $data; } + + /** + * @param array|string $orderBy + */ + private function applyOrderBy(QueryBuilder $query, string|array $orderBy): void + { + if (!$orderBy) { + return; + } + if (is_array($orderBy)) { + foreach ($orderBy as $column => $direction) { + $query->addOrderBy($column, $direction); + } + } else { + foreach (explode(',', $orderBy) as $orderByPart) { + $orderByPart = trim($orderByPart); + if (preg_match('/(.+)\s(asc|desc)$/i', $orderByPart, $orderByPartMatch)) { + $query->addOrderBy($orderByPartMatch[1], $orderByPartMatch[2]); + } else { + $query->addOrderBy($orderByPart); + } + } + } + } } diff --git a/src/Exceptions/LockException.php b/src/Exceptions/LockException.php new file mode 100644 index 0000000..650194d --- /dev/null +++ b/src/Exceptions/LockException.php @@ -0,0 +1,7 @@ +lock_version; + } + + public function incrementVersion(): void + { + $this->lock_version++; + } } diff --git a/tests/TestStand/Tables/TestOptimisticLockTable.php b/tests/TestStand/Tables/TestOptimisticLockTable.php index cf318fd..8303514 100644 --- a/tests/TestStand/Tables/TestOptimisticLockTable.php +++ b/tests/TestStand/Tables/TestOptimisticLockTable.php @@ -32,7 +32,7 @@ public function init(): bool ( `id` INTEGER NOT NULL CONSTRAINT TestAutoincrement_pk PRIMARY KEY AUTOINCREMENT, `name` VARCHAR(255) NOT NULL, - `version` INTEGER NOT NULL DEFAULT 1, + `lock_version` INTEGER NOT NULL DEFAULT 1, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL ); " diff --git a/tests/Traits/OptimisticLockTest.php b/tests/Traits/OptimisticLockTest.php index b64e973..9856e25 100644 --- a/tests/Traits/OptimisticLockTest.php +++ b/tests/Traits/OptimisticLockTest.php @@ -60,7 +60,7 @@ public function test_trait(): void $this->assertTrue($db->rollBack()); $olEntity3 = $olTable1->findByPk($olEntity1->id); - $this->assertEquals(1, $olEntity3->version); + $this->assertEquals(1, $olEntity3->getVersion()); $this->assertEquals('John', $olEntity3->name); } } \ No newline at end of file From 63416c6b48d07478efb916a6b9b57e9f603e92c4 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 21 Oct 2023 23:07:23 +0100 Subject: [PATCH 46/69] Optimize save method --- src/AbstractTable.php | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 5901139..63da7fe 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -72,36 +72,20 @@ public function save(AbstractEntity &$entity): void $entity->updated_at = new \DateTimeImmutable(); $changedColumns['updated_at'] = DateTimeHelper::dateTimeToString($entity->updated_at); } - - if ($this->config->hasOptimisticLock() && method_exists($entity, 'getVersion') && method_exists($entity, 'incrementVersion')) { $where['lock_version'] = $entity->getVersion(); $entity->incrementVersion(); $changedColumns['lock_version'] = $entity->getVersion(); - - try { - $connection->beginTransaction(); - $versionUpdated = $connection->update( - $this->getTableName(), - $changedColumns, - $where - ); - if (!$versionUpdated) { - throw new Exceptions\LockException('Failed to update entity version, concurrency modification, rolling back.'); - } - $connection->commit(); - } catch (\Throwable $e) { - $connection->rollBack(); - throw $e; - } - } else { - $connection->update( - $this->getTableName(), - $changedColumns, - $where - ); + } + $entityUpdated = $connection->update( + table: $this->getTableName(), + data: $changedColumns, + criteria: $where, + ); + if ($this->config->hasOptimisticLock() && !$entityUpdated) { + throw new Exceptions\LockException('Failed to update entity version, concurrency modification, rolling back.'); } $entity->resetChangedColumns(); } From 384610ff107d2971fd377d85c854dd69ad4a7774 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 28 Oct 2023 11:55:42 +0100 Subject: [PATCH 47/69] Add CombinedTransaction saveMany and deleteMany --- src/CombinedTransaction.php | 62 ++++++++++++++++--------- tests/Table/CombinedTransactionTest.php | 28 ++++++++++- 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/src/CombinedTransaction.php b/src/CombinedTransaction.php index 1b53586..bb8a4a5 100644 --- a/src/CombinedTransaction.php +++ b/src/CombinedTransaction.php @@ -19,18 +19,29 @@ class CombinedTransaction */ public function save(AbstractTable $table, AbstractEntity &$entity): void { - try { - $connectionName = $table->getConnectionName(); - if (empty($this->transactions[$connectionName])) { - $connection = ConnectionManager::getConnection($connectionName); - $connection->beginTransaction(); - $this->transactions[$connectionName] = $connection; - } + $this->doInTransaction($table, function () use ($table, &$entity) { $table->save($entity); - } catch (\Throwable $e) { - $this->rollback(); - throw new Exceptions\DbException($e->getMessage(), 500, $e); - } + }); + } + + /** + * @param AbstractTable $table + * @param AbstractEntity[] $entities + * @throws DbException + */ + public function saveMany(AbstractTable $table, array $entities): void + { + $this->doInTransaction($table, fn () => $table->saveMany($entities)); + } + + /** + * @param AbstractTable $table + * @param AbstractEntity[] $entities + * @throws DbException + */ + public function deleteMany(AbstractTable $table, array $entities): void + { + $this->doInTransaction($table, fn () => $table->deleteMany($entities)); } /** @@ -38,18 +49,9 @@ public function save(AbstractTable $table, AbstractEntity &$entity): void */ public function delete(AbstractTable $table, AbstractEntity &$entity): void { - try { - $connectionName = $table->getConnectionName(); - if (empty($this->transactions[$connectionName])) { - $connection = ConnectionManager::getConnection($connectionName); - $connection->beginTransaction(); - $this->transactions[$connectionName] = $connection; - } + $this->doInTransaction($table, function () use ($table, &$entity) { $table->delete($entity); - } catch (\Throwable $e) { - $this->rollback(); - throw new Exceptions\DbException($e->getMessage(), 500, $e); - } + }); } public function rollback(): void @@ -107,6 +109,22 @@ public function releaseLock(): void $this->cache->delete($this->lockKey); } + private function doInTransaction(AbstractTable $table, callable $callback): void + { + try { + $connectionName = $table->getConnectionName(); + if (empty($this->transactions[$connectionName])) { + $connection = ConnectionManager::getConnection($connectionName); + $connection->beginTransaction(); + $this->transactions[$connectionName] = $connection; + } + $callback(); + } catch (\Throwable $e) { + $this->rollback(); + throw new Exceptions\DbException($e->getMessage(), 500, $e); + } + } + /** * @param string[] $keyParts * @return string diff --git a/tests/Table/CombinedTransactionTest.php b/tests/Table/CombinedTransactionTest.php index 63ffff5..1e67a2c 100644 --- a/tests/Table/CombinedTransactionTest.php +++ b/tests/Table/CombinedTransactionTest.php @@ -20,7 +20,7 @@ public function test_transactionCommit(): void $e1 = new Entities\TestAutoincrementEntity(name: 'Foo'); $saveTransaction->save($autoIncrementTable, $e1); - $e2 = new Entities\TestCompositeEntity(user_id: $e1->id, post_id: mt_rand(1, 1000), message: 'Bar'); + $e2 = new Entities\TestCompositeEntity(user_id: $e1->id, post_id: mt_rand(1, 1000000), message: 'Bar'); $saveTransaction->save($compositeTable, $e2); $saveTransaction->commit(); @@ -37,6 +37,32 @@ public function test_transactionCommit(): void $this->assertNull($compositeTable->findOne($e2->user_id, $e2->post_id)); } + public function test_saveDeleteMany(): void + { + $autoIncrementTable = new Tables\TestAutoincrementTable(); + $compositeTable = new Tables\TestCompositeTable(); + + $saveTransaction = new CombinedTransaction(); + + $e1 = new Entities\TestAutoincrementEntity(name: 'Foo'); + $saveTransaction->save($autoIncrementTable, $e1); + + $e2 = new Entities\TestCompositeEntity(user_id: $e1->id, post_id: mt_rand(1, 1000000), message: 'Foo'); + $e3 = new Entities\TestCompositeEntity(user_id: $e1->id, post_id: mt_rand(1, 1000000), message: 'Bar'); + $saveTransaction->saveMany($compositeTable, [$e2, $e3]); + + $saveTransaction->commit(); + + $this->assertNotNull($autoIncrementTable->findByPk($e1->id)); + $this->assertNotNull($compositeTable->findOne($e2->user_id, $e2->post_id)); + $this->assertNotNull($compositeTable->findOne($e3->user_id, $e3->post_id)); + + $deleteTransaction = new CombinedTransaction(); + $deleteTransaction->delete($autoIncrementTable, $e1); + $deleteTransaction->deleteMany($compositeTable, [$e2, $e3]); + $deleteTransaction->commit(); + } + public function test_transactionRollback(): void { $autoIncrementTable = new Tables\TestAutoincrementTable(); From 5e9e64294e87daa83a7eeaedcecafb1eaf6454ed Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 28 Oct 2023 12:11:19 +0100 Subject: [PATCH 48/69] Get rid of "Internal" word in find...() and count...() AbstractTable protected methods --- doc/cache.md | 8 ++++---- doc/example.md | 6 +++--- doc/table.md | 8 ++++---- src/AbstractCachedTable.php | 20 +++++++++---------- src/AbstractTable.php | 14 ++++++------- .../Tables/TestAutoincrementCachedTable.php | 12 +++++------ .../Tables/TestAutoincrementSdCachedTable.php | 10 +++++----- .../Tables/TestAutoincrementSdTable.php | 8 ++++---- .../Tables/TestAutoincrementTable.php | 12 +++++------ .../Tables/TestCompositeCachedTable.php | 6 +++--- .../Tables/TestCompositeSdCachedTable.php | 6 +++--- .../TestStand/Tables/TestCompositeSdTable.php | 6 +++--- tests/TestStand/Tables/TestCompositeTable.php | 8 ++++---- .../Tables/TestOptimisticLockTable.php | 2 +- .../Tables/TestUniqueCachedTable.php | 6 +++--- .../Tables/TestUniqueSdCachedTable.php | 6 +++--- tests/TestStand/Tables/TestUniqueSdTable.php | 4 ++-- tests/TestStand/Tables/TestUniqueTable.php | 6 +++--- tests/TestStand/Tables/TestUpdateAtTable.php | 2 +- 19 files changed, 75 insertions(+), 75 deletions(-) diff --git a/doc/cache.md b/doc/cache.md index e3ca6ab..7b64d1f 100644 --- a/doc/cache.md +++ b/doc/cache.md @@ -6,7 +6,7 @@ To start using auto-cache feature you need: to `Composite\DB\AbstractCachedTable` 3. Implement method `getFlushCacheKeys()` 4. Change all internal select methods to their cached versions (example: `findByPkInternal()` -to `findByPkCachedInternal()` etc.) +to `_findByPkCached()` etc.) You can also generate cached version of your table with console command: @@ -46,7 +46,7 @@ class PostsTable extends AbstractCachedTable public function findByPk(int $id): ?Post { - return $this->createEntity($this->findByPkInternalCached($id)); + return $this->createEntity($this->_findByPkCached($id)); } /** @@ -54,7 +54,7 @@ class PostsTable extends AbstractCachedTable */ public function findAllFeatured(): array { - return $this->createEntities($this->findAllInternal( + return $this->createEntities($this->_findAll( 'is_featured = :is_featured', ['is_featured' => true], )); @@ -62,7 +62,7 @@ class PostsTable extends AbstractCachedTable public function countAllFeatured(): int { - return $this->countAllCachedInternal( + return $this->_countAllCached( 'is_featured = :is_featured', ['is_featured' => true], ); diff --git a/doc/example.md b/doc/example.md index 096ea6f..22a63fc 100644 --- a/doc/example.md +++ b/doc/example.md @@ -43,7 +43,7 @@ class UsersTable extends \Composite\DB\AbstractTable public function findByPk(int $id): ?User { - return $this->createEntity($this->findByPkInternal($id)); + return $this->createEntity($this->_findByPk($id)); } /** @@ -51,7 +51,7 @@ class UsersTable extends \Composite\DB\AbstractTable */ public function findAllActive(): array { - return $this->createEntities($this->findAllInternal( + return $this->createEntities($this->_findAll( 'status = :status', ['status' => Status::ACTIVE->name], )); @@ -59,7 +59,7 @@ class UsersTable extends \Composite\DB\AbstractTable public function countAllActive(): int { - return $this->countAllInternal( + return $this->_countAll( 'status = :status', ['status' => Status::ACTIVE->name], ); diff --git a/doc/table.md b/doc/table.md index c80f0d8..fbd7d1b 100644 --- a/doc/table.md +++ b/doc/table.md @@ -38,7 +38,7 @@ class UsersTable extends AbstractTable public function findOne(int $id): ?User { - return $this->createEntity($this->findOneInternal($id)); + return $this->createEntity($this->_findOne($id)); } /** @@ -46,12 +46,12 @@ class UsersTable extends AbstractTable */ public function findAll(): array { - return $this->createEntities($this->findAllInternal()); + return $this->createEntities($this->_findAll()); } public function countAll(): int { - return $this->countAllInternal(); + return $this->_countAll(); } } ``` @@ -67,7 +67,7 @@ Example with internal helper: */ public function findAllActiveAdults(): array { - $rows = $this->findAllInternal( + $rows = $this->_findAll( 'age > :age AND status = :status', ['age' => 18, 'status' => Status::ACTIVE->name], ); diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index 857f9b9..cca50ad 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -95,9 +95,9 @@ private function collectCacheKeysByEntity(AbstractEntity $entity): array /** * @return array|null */ - protected function findByPkCachedInternal(mixed $pk, null|int|\DateInterval $ttl = null): ?array + protected function _findByPkCached(mixed $pk, null|int|\DateInterval $ttl = null): ?array { - return $this->findOneCachedInternal($this->getPkCondition($pk), $ttl); + return $this->_findOneCached($this->getPkCondition($pk), $ttl); } /** @@ -105,11 +105,11 @@ protected function findByPkCachedInternal(mixed $pk, null|int|\DateInterval $ttl * @param int|\DateInterval|null $ttl * @return array|null */ - protected function findOneCachedInternal(array $condition, null|int|\DateInterval $ttl = null): ?array + protected function _findOneCached(array $condition, null|int|\DateInterval $ttl = null): ?array { return $this->getCached( $this->getOneCacheKey($condition), - fn() => $this->findOneInternal($condition), + fn() => $this->_findOne($condition), $ttl, ) ?: null; } @@ -119,7 +119,7 @@ protected function findOneCachedInternal(array $condition, null|int|\DateInterva * @param array|string $orderBy * @return array[] */ - protected function findAllCachedInternal( + protected function _findAllCached( string $whereString = '', array $whereParams = [], array|string $orderBy = [], @@ -129,7 +129,7 @@ protected function findAllCachedInternal( { return $this->getCached( $this->getListCacheKey($whereString, $whereParams, $orderBy, $limit), - fn() => $this->findAllInternal(whereString: $whereString, whereParams: $whereParams, orderBy: $orderBy, limit: $limit), + fn() => $this->_findAll(whereString: $whereString, whereParams: $whereParams, orderBy: $orderBy, limit: $limit), $ttl, ); } @@ -137,7 +137,7 @@ protected function findAllCachedInternal( /** * @param array $whereParams */ - protected function countAllCachedInternal( + protected function _countAllCached( string $whereString = '', array $whereParams = [], null|int|\DateInterval $ttl = null, @@ -145,7 +145,7 @@ protected function countAllCachedInternal( { return (int)$this->getCached( $this->getCountCacheKey($whereString, $whereParams), - fn() => $this->countAllInternal(whereString: $whereString, whereParams: $whereParams), + fn() => $this->_countAll(whereString: $whereString, whereParams: $whereParams), $ttl, ); } @@ -169,7 +169,7 @@ protected function getCached(string $cacheKey, callable $dataCallback, null|int| * @return array> * @throws \Psr\SimpleCache\InvalidArgumentException */ - protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $ttl = null): array + protected function _findMultiCached(array $ids, null|int|\DateInterval $ttl = null): array { $result = $cacheKeys = $foundIds = []; foreach ($ids as $id) { @@ -188,7 +188,7 @@ protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $t } $ids = array_diff($ids, $foundIds); foreach ($ids as $id) { - if ($row = $this->findByPkCachedInternal($id, $ttl)) { + if ($row = $this->_findByPkCached($id, $ttl)) { $result[] = $row; } } diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 63da7fe..4362f42 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -173,7 +173,7 @@ public function deleteMany(array $entities): void * @param array $whereParams * @throws \Doctrine\DBAL\Exception */ - protected function countAllInternal(string $whereString = '', array $whereParams = []): int + protected function _countAll(string $whereString = '', array $whereParams = []): int { $query = $this->select('COUNT(*)'); if ($whereString) { @@ -190,10 +190,10 @@ protected function countAllInternal(string $whereString = '', array $whereParams * @throws EntityException * @throws \Doctrine\DBAL\Exception */ - protected function findByPkInternal(mixed $pk): ?array + protected function _findByPk(mixed $pk): ?array { $where = $this->getPkCondition($pk); - return $this->findOneInternal($where); + return $this->_findOne($where); } /** @@ -202,7 +202,7 @@ protected function findByPkInternal(mixed $pk): ?array * @return array|null * @throws \Doctrine\DBAL\Exception */ - protected function findOneInternal(array $where, array|string $orderBy = []): ?array + protected function _findOne(array $where, array|string $orderBy = []): ?array { $query = $this->select(); $this->buildWhere($query, $where); @@ -216,7 +216,7 @@ protected function findOneInternal(array $where, array|string $orderBy = []): ?a * @throws DbException * @throws \Doctrine\DBAL\Exception */ - protected function findMultiInternal(array $pkList): array + protected function _findMulti(array $pkList): array { if (!$pkList) { return []; @@ -231,7 +231,7 @@ protected function findMultiInternal(array $pkList): array * @return list> * @throws \Doctrine\DBAL\Exception */ - protected function findAllInternal( + protected function _findAll( string $whereString = '', array $whereParams = [], array|string $orderBy = [], @@ -332,7 +332,7 @@ protected function select(string $select = '*'): QueryBuilder /** * @param array $where */ - private function buildWhere(\Doctrine\DBAL\Query\QueryBuilder $query, array $where): void + private function buildWhere(QueryBuilder $query, array $where): void { foreach ($where as $column => $value) { if ($value === null) { diff --git a/tests/TestStand/Tables/TestAutoincrementCachedTable.php b/tests/TestStand/Tables/TestAutoincrementCachedTable.php index 5776f92..90f4386 100644 --- a/tests/TestStand/Tables/TestAutoincrementCachedTable.php +++ b/tests/TestStand/Tables/TestAutoincrementCachedTable.php @@ -39,12 +39,12 @@ protected function getFlushCacheKeys(TestAutoincrementEntity|AbstractEntity $ent public function findByPk(int $id): ?TestAutoincrementEntity { - return $this->createEntity($this->findByPkCachedInternal($id)); + return $this->createEntity($this->_findByPkCached($id)); } public function findOneByName(string $name): ?TestAutoincrementEntity { - return $this->createEntity($this->findOneCachedInternal(['name' => $name])); + return $this->createEntity($this->_findOneCached(['name' => $name])); } /** @@ -52,7 +52,7 @@ public function findOneByName(string $name): ?TestAutoincrementEntity */ public function findAllByName(string $name): array { - return $this->createEntities($this->findAllCachedInternal( + return $this->createEntities($this->_findAllCached( 'name = :name', ['name' => $name], )); @@ -63,7 +63,7 @@ public function findAllByName(string $name): array */ public function findRecent(int $limit, int $offset): array { - return $this->createEntities($this->findAllInternal( + return $this->createEntities($this->_findAll( orderBy: ['id' => 'DESC'], limit: $limit, offset: $offset, @@ -72,7 +72,7 @@ public function findRecent(int $limit, int $offset): array public function countAllByName(string $name): int { - return $this->countAllCachedInternal( + return $this->_countAllCached( 'name = :name', ['name' => $name], ); @@ -83,7 +83,7 @@ public function countAllByName(string $name): int */ public function findMulti(array $ids): array { - return $this->createEntities($this->findMultiCachedInternal($ids)); + return $this->createEntities($this->_findMultiCached($ids)); } public function truncate(): void diff --git a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php index 66645f4..c646e9a 100644 --- a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php +++ b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php @@ -39,12 +39,12 @@ protected function getFlushCacheKeys(TestAutoincrementSdEntity|AbstractEntity $e public function findByPk(int $id): ?TestAutoincrementSdEntity { - return $this->createEntity($this->findByPkInternal($id)); + return $this->createEntity($this->_findByPk($id)); } public function findOneByName(string $name): ?TestAutoincrementSdEntity { - return $this->createEntity($this->findOneCachedInternal(['name' => $name])); + return $this->createEntity($this->_findOneCached(['name' => $name])); } /** @@ -52,7 +52,7 @@ public function findOneByName(string $name): ?TestAutoincrementSdEntity */ public function findAllByName(string $name): array { - return $this->createEntities($this->findAllCachedInternal( + return $this->createEntities($this->_findAllCached( 'name = :name', ['name' => $name, 'deleted_at' => null], )); @@ -63,7 +63,7 @@ public function findAllByName(string $name): array */ public function findRecent(int $limit, int $offset): array { - return $this->createEntities($this->findAllInternal( + return $this->createEntities($this->_findAll( orderBy: 'id DESC', limit: $limit, offset: $offset, @@ -72,7 +72,7 @@ public function findRecent(int $limit, int $offset): array public function countAllByName(string $name): int { - return $this->countAllCachedInternal( + return $this->_countAllCached( 'name = :name', ['name' => $name], ); diff --git a/tests/TestStand/Tables/TestAutoincrementSdTable.php b/tests/TestStand/Tables/TestAutoincrementSdTable.php index b9ce555..c45e826 100644 --- a/tests/TestStand/Tables/TestAutoincrementSdTable.php +++ b/tests/TestStand/Tables/TestAutoincrementSdTable.php @@ -20,12 +20,12 @@ protected function getConfig(): TableConfig public function findByPk(int $id): ?TestAutoincrementSdEntity { - return $this->createEntity($this->findByPkInternal($id)); + return $this->createEntity($this->_findByPk($id)); } public function findOneByName(string $name): ?TestAutoincrementSdEntity { - return $this->createEntity($this->findOneInternal(['name' => $name, 'deleted_at' => null])); + return $this->createEntity($this->_findOne(['name' => $name, 'deleted_at' => null])); } /** @@ -33,7 +33,7 @@ public function findOneByName(string $name): ?TestAutoincrementSdEntity */ public function findAllByName(string $name): array { - return $this->createEntities($this->findAllInternal( + return $this->createEntities($this->_findAll( 'name = :name', ['name' => $name] )); @@ -44,7 +44,7 @@ public function findAllByName(string $name): array */ public function findRecent(int $limit, int $offset): array { - return $this->createEntities($this->findAllInternal( + return $this->createEntities($this->_findAll( orderBy: 'id DESC', limit: $limit, offset: $offset, diff --git a/tests/TestStand/Tables/TestAutoincrementTable.php b/tests/TestStand/Tables/TestAutoincrementTable.php index c0768e6..86c9a50 100644 --- a/tests/TestStand/Tables/TestAutoincrementTable.php +++ b/tests/TestStand/Tables/TestAutoincrementTable.php @@ -22,12 +22,12 @@ protected function getConfig(): TableConfig public function findByPk(int $id): ?TestAutoincrementEntity { - return $this->createEntity($this->findByPkInternal($id)); + return $this->createEntity($this->_findByPk($id)); } public function findOneByName(string $name): ?TestAutoincrementEntity { - return $this->createEntity($this->findOneInternal(['name' => $name])); + return $this->createEntity($this->_findOne(['name' => $name])); } /** @@ -35,7 +35,7 @@ public function findOneByName(string $name): ?TestAutoincrementEntity */ public function findAllByName(string $name): array { - return $this->createEntities($this->findAllInternal( + return $this->createEntities($this->_findAll( whereString: 'name = :name', whereParams: ['name' => $name], orderBy: 'id', @@ -47,7 +47,7 @@ public function findAllByName(string $name): array */ public function findRecent(int $limit, int $offset): array { - return $this->createEntities($this->findAllInternal( + return $this->createEntities($this->_findAll( orderBy: ['id' => 'DESC'], limit: $limit, offset: $offset, @@ -56,7 +56,7 @@ public function findRecent(int $limit, int $offset): array public function countAllByName(string $name): int { - return $this->countAllInternal( + return $this->_countAll( 'name = :name', ['name' => $name] ); @@ -69,7 +69,7 @@ public function countAllByName(string $name): int */ public function findMulti(array $ids): array { - return $this->createEntities($this->findMultiInternal($ids), 'id'); + return $this->createEntities($this->_findMulti($ids), 'id'); } public function init(): bool diff --git a/tests/TestStand/Tables/TestCompositeCachedTable.php b/tests/TestStand/Tables/TestCompositeCachedTable.php index 936d0a9..532294f 100644 --- a/tests/TestStand/Tables/TestCompositeCachedTable.php +++ b/tests/TestStand/Tables/TestCompositeCachedTable.php @@ -30,7 +30,7 @@ protected function getFlushCacheKeys(TestCompositeEntity|AbstractEntity $entity) public function findOne(int $user_id, int $post_id): ?TestCompositeEntity { - return $this->createEntity($this->findOneCachedInternal([ + return $this->createEntity($this->_findOneCached([ 'user_id' => $user_id, 'post_id' => $post_id, ])); @@ -43,7 +43,7 @@ public function findAllByUser(int $userId): array { return array_map( fn (array $data) => TestCompositeEntity::fromArray($data), - $this->findAllCachedInternal( + $this->_findAllCached( 'user_id = :user_id', ['user_id' => $userId], ) @@ -52,7 +52,7 @@ public function findAllByUser(int $userId): array public function countAllByUser(int $userId): int { - return $this->countAllCachedInternal( + return $this->_countAllCached( 'user_id = :user_id', ['user_id' => $userId], ); diff --git a/tests/TestStand/Tables/TestCompositeSdCachedTable.php b/tests/TestStand/Tables/TestCompositeSdCachedTable.php index f4938f1..aefee62 100644 --- a/tests/TestStand/Tables/TestCompositeSdCachedTable.php +++ b/tests/TestStand/Tables/TestCompositeSdCachedTable.php @@ -38,7 +38,7 @@ protected function getFlushCacheKeys(TestCompositeSdEntity|AbstractEntity $entit public function findOne(int $user_id, int $post_id): ?TestCompositeSdEntity { - return $this->createEntity($this->findOneCachedInternal([ + return $this->createEntity($this->_findOneCached([ 'user_id' => $user_id, 'post_id' => $post_id, ])); @@ -51,7 +51,7 @@ public function findAllByUser(int $userId): array { return array_map( fn (array $data) => TestCompositeSdEntity::fromArray($data), - $this->findAllCachedInternal( + $this->_findAllCached( 'user_id = :user_id', ['user_id' => $userId], ) @@ -60,7 +60,7 @@ public function findAllByUser(int $userId): array public function countAllByUser(int $userId): int { - return $this->countAllCachedInternal( + return $this->_countAllCached( 'user_id = :user_id', ['user_id' => $userId], ); diff --git a/tests/TestStand/Tables/TestCompositeSdTable.php b/tests/TestStand/Tables/TestCompositeSdTable.php index 28d3123..3217f73 100644 --- a/tests/TestStand/Tables/TestCompositeSdTable.php +++ b/tests/TestStand/Tables/TestCompositeSdTable.php @@ -20,7 +20,7 @@ protected function getConfig(): TableConfig public function findOne(int $user_id, int $post_id): ?TestCompositeSdEntity { - return $this->createEntity($this->findOneInternal([ + return $this->createEntity($this->_findOne([ 'user_id' => $user_id, 'post_id' => $post_id, ])); @@ -31,7 +31,7 @@ public function findOne(int $user_id, int $post_id): ?TestCompositeSdEntity */ public function findAllByUser(int $userId): array { - return $this->createEntities($this->findAllInternal( + return $this->createEntities($this->_findAll( 'user_id = :user_id', ['user_id' => $userId], )); @@ -39,7 +39,7 @@ public function findAllByUser(int $userId): array public function countAllByUser(int $userId): int { - return $this->countAllInternal( + return $this->_countAll( 'user_id = :user_id', ['user_id' => $userId], ); diff --git a/tests/TestStand/Tables/TestCompositeTable.php b/tests/TestStand/Tables/TestCompositeTable.php index 359e450..50639be 100644 --- a/tests/TestStand/Tables/TestCompositeTable.php +++ b/tests/TestStand/Tables/TestCompositeTable.php @@ -32,7 +32,7 @@ public function delete(AbstractEntity|TestCompositeEntity &$entity): void public function findOne(int $user_id, int $post_id): ?TestCompositeEntity { - return $this->createEntity($this->findOneInternal(['user_id' => $user_id, 'post_id' => $post_id])); + return $this->createEntity($this->_findOne(['user_id' => $user_id, 'post_id' => $post_id])); } /** @@ -40,7 +40,7 @@ public function findOne(int $user_id, int $post_id): ?TestCompositeEntity */ public function findAllByUser(int $userId): array { - return $this->createEntities($this->findAllInternal( + return $this->createEntities($this->_findAll( 'user_id = :user_id', ['user_id' => $userId], )); @@ -48,7 +48,7 @@ public function findAllByUser(int $userId): array public function countAllByUser(int $userId): int { - return $this->countAllInternal( + return $this->_countAll( 'user_id = :user_id', ['user_id' => $userId, 'deleted_at' => null], ); @@ -61,7 +61,7 @@ public function countAllByUser(int $userId): int */ public function findMulti(array $ids): array { - return $this->createEntities($this->findMultiInternal($ids), 'post_id'); + return $this->createEntities($this->_findMulti($ids), 'post_id'); } public function init(): bool diff --git a/tests/TestStand/Tables/TestOptimisticLockTable.php b/tests/TestStand/Tables/TestOptimisticLockTable.php index 8303514..375a61a 100644 --- a/tests/TestStand/Tables/TestOptimisticLockTable.php +++ b/tests/TestStand/Tables/TestOptimisticLockTable.php @@ -21,7 +21,7 @@ protected function getConfig(): TableConfig public function findByPk(int $id): ?TestOptimisticLockEntity { - return $this->createEntity($this->findByPkInternal($id)); + return $this->createEntity($this->_findByPk($id)); } public function init(): bool diff --git a/tests/TestStand/Tables/TestUniqueCachedTable.php b/tests/TestStand/Tables/TestUniqueCachedTable.php index 7cb7fa3..cdcaccb 100644 --- a/tests/TestStand/Tables/TestUniqueCachedTable.php +++ b/tests/TestStand/Tables/TestUniqueCachedTable.php @@ -32,7 +32,7 @@ protected function getFlushCacheKeys(TestUniqueEntity|AbstractEntity $entity): a public function findByPk(UuidInterface $id): ?TestUniqueEntity { - return $this->createEntity($this->findByPkInternal($id)); + return $this->createEntity($this->_findByPk($id)); } /** @@ -40,7 +40,7 @@ public function findByPk(UuidInterface $id): ?TestUniqueEntity */ public function findAllByName(string $name): array { - return $this->createEntities($this->findAllCachedInternal( + return $this->createEntities($this->_findAllCached( 'name = :name', ['name' => $name], )); @@ -48,7 +48,7 @@ public function findAllByName(string $name): array public function countAllByName(string $name): int { - return $this->countAllCachedInternal( + return $this->_countAllCached( 'name = :name', ['name' => $name], ); diff --git a/tests/TestStand/Tables/TestUniqueSdCachedTable.php b/tests/TestStand/Tables/TestUniqueSdCachedTable.php index 016133d..f5877ec 100644 --- a/tests/TestStand/Tables/TestUniqueSdCachedTable.php +++ b/tests/TestStand/Tables/TestUniqueSdCachedTable.php @@ -32,7 +32,7 @@ protected function getFlushCacheKeys(TestUniqueSdEntity|AbstractEntity $entity): public function findByPk(UuidInterface $id): ?TestUniqueSdEntity { - return $this->createEntity($this->findByPkInternal($id)); + return $this->createEntity($this->_findByPk($id)); } /** @@ -40,7 +40,7 @@ public function findByPk(UuidInterface $id): ?TestUniqueSdEntity */ public function findAllByName(string $name): array { - return $this->createEntities($this->findAllCachedInternal( + return $this->createEntities($this->_findAllCached( 'name = :name', ['name' => $name], )); @@ -48,7 +48,7 @@ public function findAllByName(string $name): array public function countAllByName(string $name): int { - return $this->countAllCachedInternal( + return $this->_countAllCached( 'name = :name', ['name' => $name], ); diff --git a/tests/TestStand/Tables/TestUniqueSdTable.php b/tests/TestStand/Tables/TestUniqueSdTable.php index df078b9..5f61e0d 100644 --- a/tests/TestStand/Tables/TestUniqueSdTable.php +++ b/tests/TestStand/Tables/TestUniqueSdTable.php @@ -21,7 +21,7 @@ protected function getConfig(): TableConfig public function findByPk(UuidInterface $id): ?TestUniqueSdEntity { - return $this->createEntity($this->findByPkInternal($id)); + return $this->createEntity($this->_findByPk($id)); } /** @@ -29,7 +29,7 @@ public function findByPk(UuidInterface $id): ?TestUniqueSdEntity */ public function findAllByName(string $name): array { - return $this->createEntities($this->findAllInternal( + return $this->createEntities($this->_findAll( 'name = :name', ['name' => $name], )); diff --git a/tests/TestStand/Tables/TestUniqueTable.php b/tests/TestStand/Tables/TestUniqueTable.php index 7e223d9..befb866 100644 --- a/tests/TestStand/Tables/TestUniqueTable.php +++ b/tests/TestStand/Tables/TestUniqueTable.php @@ -32,7 +32,7 @@ protected function getConfig(): TableConfig public function findByPk(UuidInterface $id): ?TestUniqueEntity { - return $this->createEntity($this->findByPkInternal($id)); + return $this->createEntity($this->_findByPk($id)); } /** @@ -40,7 +40,7 @@ public function findByPk(UuidInterface $id): ?TestUniqueEntity */ public function findAllByName(string $name): array { - return $this->createEntities($this->findAllInternal( + return $this->createEntities($this->_findAll( 'name = :name', ['name' => $name], )); @@ -48,7 +48,7 @@ public function findAllByName(string $name): array public function countAllByName(string $name): int { - return $this->countAllInternal( + return $this->_countAll( 'name = :name', ['name' => $name], ); diff --git a/tests/TestStand/Tables/TestUpdateAtTable.php b/tests/TestStand/Tables/TestUpdateAtTable.php index 2fb74c4..48c7c2a 100644 --- a/tests/TestStand/Tables/TestUpdateAtTable.php +++ b/tests/TestStand/Tables/TestUpdateAtTable.php @@ -21,7 +21,7 @@ protected function getConfig(): TableConfig public function findByPk(string $id): ?TestUpdatedAtEntity { - return $this->createEntity($this->findByPkInternal($id)); + return $this->createEntity($this->_findByPk($id)); } public function init(): bool From e68dd36ea67e7c9513da091764597229b3c08aa3 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 28 Oct 2023 18:26:20 +0100 Subject: [PATCH 49/69] Rework $where param, add flexibility to use simple assoc array or flexible Where class --- src/AbstractCachedTable.php | 53 +++++------- src/AbstractTable.php | 62 +++++++++----- src/Where.php | 16 ++++ tests/Table/AbstractCachedTableTest.php | 5 +- tests/Table/AbstractTableTest.php | 84 ++++++++++++++----- tests/Table/AutoIncrementTableTest.php | 12 +++ tests/Table/CompositeTableTest.php | 46 +--------- tests/Table/UniqueTableTest.php | 20 +---- .../Entities/Enums/TestBackedEnum.php | 12 +++ .../TestStand/Entities/Enums/TestUnitEnum.php | 12 +++ .../Entities/TestCompositeEntity.php | 1 + .../Entities/TestCompositeSdEntity.php | 12 --- tests/TestStand/Entities/TestUniqueEntity.php | 1 + .../TestStand/Entities/TestUniqueSdEntity.php | 12 --- .../Tables/TestAutoincrementCachedTable.php | 25 +++--- .../Tables/TestAutoincrementSdCachedTable.php | 24 ++++-- .../Tables/TestAutoincrementSdTable.php | 5 +- .../Tables/TestAutoincrementTable.php | 18 ++-- .../Tables/TestCompositeCachedTable.php | 14 +--- .../Tables/TestCompositeSdCachedTable.php | 73 ---------------- .../TestStand/Tables/TestCompositeSdTable.php | 65 -------------- tests/TestStand/Tables/TestCompositeTable.php | 12 +-- .../Tables/TestUniqueCachedTable.php | 13 ++- .../Tables/TestUniqueSdCachedTable.php | 61 -------------- tests/TestStand/Tables/TestUniqueSdTable.php | 54 ------------ tests/TestStand/Tables/TestUniqueTable.php | 13 ++- 26 files changed, 254 insertions(+), 471 deletions(-) create mode 100644 src/Where.php create mode 100644 tests/TestStand/Entities/Enums/TestBackedEnum.php create mode 100644 tests/TestStand/Entities/Enums/TestUnitEnum.php delete mode 100644 tests/TestStand/Entities/TestCompositeSdEntity.php delete mode 100644 tests/TestStand/Entities/TestUniqueSdEntity.php delete mode 100644 tests/TestStand/Tables/TestCompositeSdCachedTable.php delete mode 100644 tests/TestStand/Tables/TestCompositeSdTable.php delete mode 100644 tests/TestStand/Tables/TestUniqueSdCachedTable.php delete mode 100644 tests/TestStand/Tables/TestUniqueSdTable.php diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index cca50ad..dd3a2b7 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -3,6 +3,7 @@ namespace Composite\DB; use Composite\DB\Exceptions\DbException; +use Composite\DB\Tests\TestStand\Entities\TestAutoincrementEntity; use Composite\Entity\AbstractEntity; use Psr\SimpleCache\CacheInterface; use Ramsey\Uuid\UuidInterface; @@ -115,37 +116,35 @@ protected function _findOneCached(array $condition, null|int|\DateInterval $ttl } /** - * @param array $whereParams + * @param array|Where $where * @param array|string $orderBy * @return array[] */ protected function _findAllCached( - string $whereString = '', - array $whereParams = [], + array|Where $where = [], array|string $orderBy = [], ?int $limit = null, null|int|\DateInterval $ttl = null, ): array { return $this->getCached( - $this->getListCacheKey($whereString, $whereParams, $orderBy, $limit), - fn() => $this->_findAll(whereString: $whereString, whereParams: $whereParams, orderBy: $orderBy, limit: $limit), + $this->getListCacheKey($where, $orderBy, $limit), + fn() => $this->_findAll(where: $where, orderBy: $orderBy, limit: $limit), $ttl, ); } /** - * @param array $whereParams + * @param array|Where $where */ - protected function _countAllCached( - string $whereString = '', - array $whereParams = [], + protected function _countByAllCached( + array|Where $where = [], null|int|\DateInterval $ttl = null, ): int { return (int)$this->getCached( - $this->getCountCacheKey($whereString, $whereParams), - fn() => $this->_countAll(whereString: $whereString, whereParams: $whereParams), + $this->getCountCacheKey($where), + fn() => $this->_countAll(where: $where), $ttl, ); } @@ -209,37 +208,35 @@ protected function getOneCacheKey(string|int|array|AbstractEntity|UuidInterface } /** - * @param array $whereParams + * @param array|Where $where * @param array|string $orderBy */ protected function getListCacheKey( - string $whereString = '', - array $whereParams = [], + array|Where $where = [], array|string $orderBy = [], ?int $limit = null ): string { - $wherePart = $this->prepareWhereKey($whereString, $whereParams); + $wherePart = is_array($where) ? $where : $this->prepareWhereKey($where); return $this->buildCacheKey( 'l', - $wherePart ?? 'all', + $wherePart ?: 'all', $orderBy ? ['ob' => $orderBy] : null, $limit ? ['limit' => $limit] : null, ); } /** - * @param array $whereParams + * @param array|Where $where */ protected function getCountCacheKey( - string $whereString = '', - array $whereParams = [], + array|Where $where = [], ): string { - $wherePart = $this->prepareWhereKey($whereString, $whereParams); + $wherePart = is_array($where) ? $where : $this->prepareWhereKey($where); return $this->buildCacheKey( 'c', - $wherePart ?? 'all', + $wherePart ?: 'all', ); } @@ -280,18 +277,12 @@ private function formatStringForCacheKey(string $string): string return trim((string)preg_replace('/_+/', '_', $string), '_'); } - /** - * @param array $whereParams - */ - private function prepareWhereKey(string $whereString, array $whereParams): ?string + private function prepareWhereKey(Where $where): string { - if (!$whereString) { - return null; - } return str_replace( - array_map(fn (string $key): string => ':' . $key, array_keys($whereParams)), - array_values($whereParams), - $whereString, + array_map(fn (string $key): string => ':' . $key, array_keys($where->params)), + array_values($where->params), + $where->string, ); } } diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 4362f42..59e89e3 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -7,7 +7,6 @@ use Composite\Entity\Helpers\DateTimeHelper; use Composite\Entity\AbstractEntity; use Composite\DB\Exceptions\DbException; -use Composite\Entity\Exceptions\EntityException; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Query\QueryBuilder; @@ -15,6 +14,8 @@ abstract class AbstractTable { + private const COMPARISON_SIGNS = ['=', '!=', '>', '<', '>=', '<=', '<>']; + protected readonly TableConfig $config; private ?QueryBuilder $selectQuery = null; @@ -170,15 +171,17 @@ public function deleteMany(array $entities): void } /** - * @param array $whereParams + * @param array|Where $where * @throws \Doctrine\DBAL\Exception */ - protected function _countAll(string $whereString = '', array $whereParams = []): int + protected function _countAll(array|Where $where = []): int { $query = $this->select('COUNT(*)'); - if ($whereString) { - $query->where($whereString); - foreach ($whereParams as $param => $value) { + if (is_array($where)) { + $this->buildWhere($query, $where); + } else { + $query->where($where->string); + foreach ($where->params as $param => $value) { $query->setParameter($param, $value); } } @@ -187,7 +190,6 @@ protected function _countAll(string $whereString = '', array $whereParams = []): /** * @return array|null - * @throws EntityException * @throws \Doctrine\DBAL\Exception */ protected function _findByPk(mixed $pk): ?array @@ -226,23 +228,24 @@ protected function _findMulti(array $pkList): array } /** - * @param array $whereParams + * @param array|Where $where * @param array|string $orderBy * @return list> * @throws \Doctrine\DBAL\Exception */ protected function _findAll( - string $whereString = '', - array $whereParams = [], + array|Where $where = [], array|string $orderBy = [], ?int $limit = null, ?int $offset = null, ): array { $query = $this->select(); - if ($whereString) { - $query->where($whereString); - foreach ($whereParams as $param => $value) { + if (is_array($where)) { + $this->buildWhere($query, $where); + } else { + $query->where($where->string); + foreach ($where->params as $param => $value) { $query->setParameter($param, $value); } } @@ -301,7 +304,6 @@ final protected function createEntities(mixed $data, ?string $keyColumnName = nu /** * @param int|string|array|AbstractEntity $data * @return array - * @throws EntityException */ protected function getPkCondition(int|string|array|AbstractEntity|UuidInterface $data): array { @@ -335,14 +337,34 @@ protected function select(string $select = '*'): QueryBuilder private function buildWhere(QueryBuilder $query, array $where): void { foreach ($where as $column => $value) { - if ($value === null) { - $query->andWhere("$column IS NULL"); + if ($value instanceof \BackedEnum) { + $value = $value->value; + } elseif ($value instanceof \UnitEnum) { + $value = $value->name; + } + + if (is_null($value)) { + $query->andWhere($column . ' IS NULL'); + } elseif (is_array($value) && count($value) === 2 && \in_array($value[0], self::COMPARISON_SIGNS)) { + $comparisonSign = $value[0]; + $comparisonValue = $value[1]; + + // Handle special case of "!= null" + if ($comparisonSign === '!=' && is_null($comparisonValue)) { + $query->andWhere($column . ' IS NOT NULL'); + } else { + $query->andWhere($column . ' ' . $comparisonSign . ' :' . $column) + ->setParameter($column, $comparisonValue); + } } elseif (is_array($value)) { - $query - ->andWhere($query->expr()->in($column, $value)); + $placeholders = []; + foreach ($value as $index => $val) { + $placeholders[] = ':' . $column . $index; + $query->setParameter($column . $index, $val); + } + $query->andWhere($column . ' IN(' . implode(', ', $placeholders) . ')'); } else { - $query - ->andWhere("$column = :" . $column) + $query->andWhere($column . ' = :' . $column) ->setParameter($column, $value); } } diff --git a/src/Where.php b/src/Where.php new file mode 100644 index 0000000..41b545f --- /dev/null +++ b/src/Where.php @@ -0,0 +1,16 @@ + 0" + * @param array $params params with placeholders, which used in $string, example: ['user_id' => 123], + */ + public function __construct( + public readonly string $string, + public readonly array $params, + ) { + } +} \ No newline at end of file diff --git a/tests/Table/AbstractCachedTableTest.php b/tests/Table/AbstractCachedTableTest.php index 6c65846..8ca3877 100644 --- a/tests/Table/AbstractCachedTableTest.php +++ b/tests/Table/AbstractCachedTableTest.php @@ -7,6 +7,7 @@ use Composite\DB\Exceptions\DbException; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Tables; +use Composite\DB\Where; use Composite\Entity\AbstractEntity; use Composite\DB\Tests\Helpers; use Ramsey\Uuid\Uuid; @@ -91,7 +92,7 @@ public function test_getCountCacheKey(string $whereString, array $whereParams, s { $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()); $reflectionMethod = new \ReflectionMethod($table, 'getCountCacheKey'); - $actual = $reflectionMethod->invoke($table, $whereString, $whereParams); + $actual = $reflectionMethod->invoke($table, new Where($whereString, $whereParams)); $this->assertEquals($expected, $actual); } @@ -150,7 +151,7 @@ public function test_getListCacheKey(string $whereString, array $whereArray, arr { $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()); $reflectionMethod = new \ReflectionMethod($table, 'getListCacheKey'); - $actual = $reflectionMethod->invoke($table, $whereString, $whereArray, $orderBy, $limit); + $actual = $reflectionMethod->invoke($table, new Where($whereString, $whereArray), $orderBy, $limit); $this->assertEquals($expected, $actual); } diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php index 5c45eca..cb48987 100644 --- a/tests/Table/AbstractTableTest.php +++ b/tests/Table/AbstractTableTest.php @@ -51,16 +51,6 @@ public static function getPkCondition_dataProvider(): array Entities\TestAutoincrementSdEntity::fromArray(['id' => 123, 'name' => 'John']), ['id' => 123], ], - [ - new Tables\TestCompositeSdTable(), - new Entities\TestCompositeSdEntity(user_id: 123, post_id: 456, message: 'Text'), - ['user_id' => 123, 'post_id' => 456], - ], - [ - new Tables\TestUniqueSdTable(), - new Entities\TestUniqueSdEntity(id: $uuid, name: 'John'), - ['id' => $uuid->toString()], - ], ]; } @@ -123,29 +113,81 @@ public function test_buildWhere($where, $expectedSQL, $expectedParams) $buildWhereReflection->invokeArgs($table, [$queryBuilder, $where]); $this->assertEquals($expectedSQL, $queryBuilder->getSQL()); + $this->assertEquals($expectedParams, $queryBuilder->getParameters()); } public static function buildWhere_dataProvider(): array { return [ - // Test when value is null + // Scalar value [ - ['column1' => null], - 'SELECT * FROM Strict WHERE column1 IS NULL', + ['column' => 1], + 'SELECT * FROM Strict WHERE column = :column', + ['column' => 1] + ], + + // Null value + [ + ['column' => null], + 'SELECT * FROM Strict WHERE column IS NULL', [] ], - // Test when value is an array + + // Greater than comparison [ - ['column1' => [1, 2, 3]], - 'SELECT * FROM Strict WHERE column1 IN (1, 2, 3)', - [1, 2, 3] + ['column' => ['>', 0]], + 'SELECT * FROM Strict WHERE column > :column', + ['column' => 0] ], - // Test when value is a single value + + // Less than comparison [ - ['column1' => 'value1'], - 'SELECT * FROM Strict WHERE column1 = :column1', - ['value1'] + ['column' => ['<', 5]], + 'SELECT * FROM Strict WHERE column < :column', + ['column' => 5] ], + + // Greater than or equal to comparison + [ + ['column' => ['>=', 3]], + 'SELECT * FROM Strict WHERE column >= :column', + ['column' => 3] + ], + + // Less than or equal to comparison + [ + ['column' => ['<=', 7]], + 'SELECT * FROM Strict WHERE column <= :column', + ['column' => 7] + ], + + // Not equal to comparison with scalar value + [ + ['column' => ['<>', 10]], + 'SELECT * FROM Strict WHERE column <> :column', + ['column' => 10] + ], + + // Not equal to comparison with null + [ + ['column' => ['!=', null]], + 'SELECT * FROM Strict WHERE column IS NOT NULL', + [] + ], + + // IN condition + [ + ['column' => [1, 2, 3]], + 'SELECT * FROM Strict WHERE column IN(:column0, :column1, :column2)', + ['column0' => 1, 'column1' => 2, 'column2' => 3] + ], + + // Multiple conditions + [ + ['column1' => 1, 'column2' => null, 'column3' => ['>', 5]], + 'SELECT * FROM Strict WHERE (column1 = :column1) AND (column2 IS NULL) AND (column3 > :column3)', + ['column1' => 1, 'column3' => 5] + ] ]; } } \ No newline at end of file diff --git a/tests/Table/AutoIncrementTableTest.php b/tests/Table/AutoIncrementTableTest.php index ec85178..98690f2 100644 --- a/tests/Table/AutoIncrementTableTest.php +++ b/tests/Table/AutoIncrementTableTest.php @@ -85,6 +85,18 @@ public function test_crud(AbstractTable&IAutoincrementTable $table, string $clas $preLastEntity = $table->findRecent(1, 1); $this->assertEquals($e1, $preLastEntity[0]); + if ($tableConfig->hasSoftDelete()) { + $e1->name = 'Exception'; + $exceptionThrown = false; + try { + $table->deleteMany([$e1, $e2]); + } catch (\Exception) { + $exceptionThrown = true; + } + $this->assertTrue($exceptionThrown); + $e1->name = Helpers\StringHelper::getUniqueName(); + } + $table->deleteMany([$e1, $e2]); if ($tableConfig->hasSoftDelete()) { diff --git a/tests/Table/CompositeTableTest.php b/tests/Table/CompositeTableTest.php index afc9f97..224e604 100644 --- a/tests/Table/CompositeTableTest.php +++ b/tests/Table/CompositeTableTest.php @@ -19,29 +19,20 @@ public static function crud_dataProvider(): array new Tables\TestCompositeTable(), Entities\TestCompositeEntity::class, ], - [ - new Tables\TestCompositeSdTable(), - Entities\TestCompositeSdEntity::class, - ], [ new Tables\TestCompositeCachedTable(Helpers\CacheHelper::getCache()), Entities\TestCompositeEntity::class, ], - [ - new Tables\TestCompositeSdCachedTable(Helpers\CacheHelper::getCache()), - Entities\TestCompositeSdEntity::class, - ], ]; } /** - * @param class-string $class + * @param class-string $class * @dataProvider crud_dataProvider */ public function test_crud(AbstractTable&ICompositeTable $table, string $class): void { $table->truncate(); - $tableConfig = TableConfig::fromEntitySchema($class::schema()); $entity = new $class( user_id: mt_rand(1, 1000000), @@ -57,14 +48,7 @@ public function test_crud(AbstractTable&ICompositeTable $table, string $class): $this->assertEntityExists($table, $entity); $table->delete($entity); - - if ($tableConfig->hasSoftDelete()) { - /** @var Entities\TestCompositeSdEntity $deletedEntity */ - $deletedEntity = $table->findOne(user_id: $entity->user_id, post_id: $entity->post_id); - $this->assertTrue($deletedEntity->isDeleted()); - } else { - $this->assertEntityNotExists($table, $entity); - } + $this->assertEntityNotExists($table, $entity); $e1 = new $class( user_id: mt_rand(1, 1000000), @@ -84,32 +68,10 @@ public function test_crud(AbstractTable&ICompositeTable $table, string $class): $this->assertEntityExists($table, $e1); $this->assertEntityExists($table, $e2); - if ($tableConfig->hasSoftDelete()) { - $e1->message = 'Exception'; - $exceptionThrown = false; - try { - $table->deleteMany([$e1, $e2]); - } catch (\Exception) { - $exceptionThrown = true; - } - $this->assertTrue($exceptionThrown); - $e1->message = Helpers\StringHelper::getUniqueName(); - } - $table->deleteMany([$e1, $e2]); - if ($tableConfig->hasSoftDelete()) { - /** @var Entities\TestCompositeSdEntity $deletedEntity1 */ - $deletedEntity1 = $table->findOne(user_id: $e1->user_id, post_id: $e1->post_id); - $this->assertTrue($deletedEntity1->isDeleted()); - - /** @var Entities\TestCompositeSdEntity $deletedEntity2 */ - $deletedEntity2 = $table->findOne(user_id: $e2->user_id, post_id: $e2->post_id); - $this->assertTrue($deletedEntity2->isDeleted()); - } else { - $this->assertEntityNotExists($table, $e1); - $this->assertEntityNotExists($table, $e2); - } + $this->assertEntityNotExists($table, $e1); + $this->assertEntityNotExists($table, $e2); } public function test_getMulti(): void diff --git a/tests/Table/UniqueTableTest.php b/tests/Table/UniqueTableTest.php index e9cd434..d4073fd 100644 --- a/tests/Table/UniqueTableTest.php +++ b/tests/Table/UniqueTableTest.php @@ -19,29 +19,20 @@ public static function crud_dataProvider(): array new Tables\TestUniqueTable(), Entities\TestUniqueEntity::class, ], - [ - new Tables\TestUniqueSdTable(), - Entities\TestUniqueSdEntity::class, - ], [ new Tables\TestUniqueCachedTable(Helpers\CacheHelper::getCache()), Entities\TestUniqueEntity::class, ], - [ - new Tables\TestUniqueSdCachedTable(Helpers\CacheHelper::getCache()), - Entities\TestUniqueSdEntity::class, - ], ]; } /** - * @param class-string $class + * @param class-string $class * @dataProvider crud_dataProvider */ public function test_crud(AbstractTable&IUniqueTable $table, string $class): void { $table->truncate(); - $tableConfig = TableConfig::fromEntitySchema($class::schema()); $entity = new $class( id: Uuid::uuid4(), @@ -56,14 +47,7 @@ public function test_crud(AbstractTable&IUniqueTable $table, string $class): voi $this->assertEntityExists($table, $entity); $table->delete($entity); - - if ($tableConfig->hasSoftDelete()) { - /** @var Entities\TestUniqueSdEntity $deletedEntity */ - $deletedEntity = $table->findByPk($entity->id); - $this->assertTrue($deletedEntity->isDeleted()); - } else { - $this->assertEntityNotExists($table, $entity); - } + $this->assertEntityNotExists($table, $entity); } public function test_multiSave(): void diff --git a/tests/TestStand/Entities/Enums/TestBackedEnum.php b/tests/TestStand/Entities/Enums/TestBackedEnum.php new file mode 100644 index 0000000..107d79f --- /dev/null +++ b/tests/TestStand/Entities/Enums/TestBackedEnum.php @@ -0,0 +1,12 @@ +getOneCacheKey(['name' => $entity->name]), - $this->getListCacheKey('name = :name', ['name' => $entity->name]), - $this->getCountCacheKey('name = :name', ['name' => $entity->name]), + $this->getListCacheKey(new Where('name = :name', ['name' => $entity->name])), + $this->getCountCacheKey(new Where('name = :name', ['name' => $entity->name])), ]; $oldName = $entity->getOldValue('name'); if (!$entity->isNew() && $oldName !== $entity->name) { $keys[] = $this->getOneCacheKey(['name' => $oldName]); - $keys[] = $this->getListCacheKey('name = :name', ['name' => $oldName]); - $keys[] = $this->getCountCacheKey('name = :name', ['name' => $oldName]); + $keys[] = $this->getListCacheKey(new Where('name = :name', ['name' => $oldName])); + $keys[] = $this->getCountCacheKey(new Where('name = :name', ['name' => $oldName])); } return $keys; } @@ -47,14 +48,21 @@ public function findOneByName(string $name): ?TestAutoincrementEntity return $this->createEntity($this->_findOneCached(['name' => $name])); } + public function delete(TestAutoincrementEntity|AbstractEntity &$entity): void + { + if ($entity->name === 'Exception') { + throw new \Exception('Test Exception'); + } + parent::delete($entity); + } + /** * @return TestAutoincrementEntity[] */ public function findAllByName(string $name): array { return $this->createEntities($this->_findAllCached( - 'name = :name', - ['name' => $name], + new Where('name = :name', ['name' => $name]) )); } @@ -72,10 +80,7 @@ public function findRecent(int $limit, int $offset): array public function countAllByName(string $name): int { - return $this->_countAllCached( - 'name = :name', - ['name' => $name], - ); + return $this->_countByAllCached(new Where('name = :name', ['name' => $name])); } /** diff --git a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php index c646e9a..4f432b7 100644 --- a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php +++ b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php @@ -6,6 +6,7 @@ use Composite\DB\TableConfig; use Composite\DB\Tests\TestStand\Entities\TestAutoincrementSdEntity; use Composite\DB\Tests\TestStand\Interfaces\IAutoincrementTable; +use Composite\DB\Where; use Composite\Entity\AbstractEntity; class TestAutoincrementSdCachedTable extends AbstractCachedTable implements IAutoincrementTable @@ -25,14 +26,14 @@ protected function getFlushCacheKeys(TestAutoincrementSdEntity|AbstractEntity $e { $keys = [ $this->getOneCacheKey(['name' => $entity->name]), - $this->getListCacheKey('name = :name', ['name' => $entity->name]), - $this->getCountCacheKey('name = :name', ['name' => $entity->name]), + $this->getListCacheKey(new Where('name = :name', ['name' => $entity->name])), + $this->getCountCacheKey(new Where('name = :name', ['name' => $entity->name])), ]; $oldName = $entity->getOldValue('name'); if ($oldName !== null && $oldName !== $entity->name) { $keys[] = $this->getOneCacheKey(['name' => $oldName]); - $keys[] = $this->getListCacheKey('name = :name', ['name' => $oldName]); - $keys[] = $this->getCountCacheKey('name = :name', ['name' => $oldName]); + $keys[] = $this->getListCacheKey(new Where('name = :name', ['name' => $oldName])); + $keys[] = $this->getCountCacheKey(new Where('name = :name', ['name' => $oldName])); } return $keys; } @@ -47,14 +48,21 @@ public function findOneByName(string $name): ?TestAutoincrementSdEntity return $this->createEntity($this->_findOneCached(['name' => $name])); } + public function delete(TestAutoincrementSdEntity|AbstractEntity &$entity): void + { + if ($entity->name === 'Exception') { + throw new \Exception('Test Exception'); + } + parent::delete($entity); + } + /** * @return TestAutoincrementSdEntity[] */ public function findAllByName(string $name): array { return $this->createEntities($this->_findAllCached( - 'name = :name', - ['name' => $name, 'deleted_at' => null], + new Where('name = :name', ['name' => $name, 'deleted_at' => null]), )); } @@ -72,10 +80,10 @@ public function findRecent(int $limit, int $offset): array public function countAllByName(string $name): int { - return $this->_countAllCached( + return $this->_countByAllCached(new Where( 'name = :name', ['name' => $name], - ); + )); } public function truncate(): void diff --git a/tests/TestStand/Tables/TestAutoincrementSdTable.php b/tests/TestStand/Tables/TestAutoincrementSdTable.php index c45e826..519f063 100644 --- a/tests/TestStand/Tables/TestAutoincrementSdTable.php +++ b/tests/TestStand/Tables/TestAutoincrementSdTable.php @@ -4,6 +4,7 @@ use Composite\DB\TableConfig; use Composite\DB\Tests\TestStand\Entities\TestAutoincrementSdEntity; +use Composite\DB\Where; class TestAutoincrementSdTable extends TestAutoincrementTable { @@ -34,8 +35,7 @@ public function findOneByName(string $name): ?TestAutoincrementSdEntity public function findAllByName(string $name): array { return $this->createEntities($this->_findAll( - 'name = :name', - ['name' => $name] + new Where('name = :name', ['name' => $name]) )); } @@ -45,6 +45,7 @@ public function findAllByName(string $name): array public function findRecent(int $limit, int $offset): array { return $this->createEntities($this->_findAll( + where: ['deleted_at' => null], orderBy: 'id DESC', limit: $limit, offset: $offset, diff --git a/tests/TestStand/Tables/TestAutoincrementTable.php b/tests/TestStand/Tables/TestAutoincrementTable.php index 86c9a50..de0e994 100644 --- a/tests/TestStand/Tables/TestAutoincrementTable.php +++ b/tests/TestStand/Tables/TestAutoincrementTable.php @@ -6,6 +6,8 @@ use Composite\DB\TableConfig; use Composite\DB\Tests\TestStand\Entities\TestAutoincrementEntity; use Composite\DB\Tests\TestStand\Interfaces\IAutoincrementTable; +use Composite\DB\Where; +use Composite\Entity\AbstractEntity; class TestAutoincrementTable extends AbstractTable implements IAutoincrementTable { @@ -30,14 +32,21 @@ public function findOneByName(string $name): ?TestAutoincrementEntity return $this->createEntity($this->_findOne(['name' => $name])); } + public function delete(AbstractEntity|TestAutoincrementEntity &$entity): void + { + if ($entity->name === 'Exception') { + throw new \Exception('Test Exception'); + } + parent::delete($entity); + } + /** * @return TestAutoincrementEntity[] */ public function findAllByName(string $name): array { return $this->createEntities($this->_findAll( - whereString: 'name = :name', - whereParams: ['name' => $name], + where: new Where('name = :name', ['name' => $name]), orderBy: 'id', )); } @@ -56,10 +65,7 @@ public function findRecent(int $limit, int $offset): array public function countAllByName(string $name): int { - return $this->_countAll( - 'name = :name', - ['name' => $name] - ); + return $this->_countAll(new Where('name = :name', ['name' => $name])); } /** diff --git a/tests/TestStand/Tables/TestCompositeCachedTable.php b/tests/TestStand/Tables/TestCompositeCachedTable.php index 532294f..57f3203 100644 --- a/tests/TestStand/Tables/TestCompositeCachedTable.php +++ b/tests/TestStand/Tables/TestCompositeCachedTable.php @@ -23,8 +23,8 @@ protected function getConfig(): TableConfig protected function getFlushCacheKeys(TestCompositeEntity|AbstractEntity $entity): array { return [ - $this->getListCacheKey('user_id = :user_id', ['user_id' => $entity->user_id]), - $this->getCountCacheKey('user_id = :user_id', ['user_id' => $entity->user_id]), + $this->getListCacheKey(['user_id' => $entity->user_id]), + $this->getCountCacheKey(['user_id' => $entity->user_id]), ]; } @@ -43,19 +43,13 @@ public function findAllByUser(int $userId): array { return array_map( fn (array $data) => TestCompositeEntity::fromArray($data), - $this->_findAllCached( - 'user_id = :user_id', - ['user_id' => $userId], - ) + $this->_findAllCached(['user_id' => $userId]) ); } public function countAllByUser(int $userId): int { - return $this->_countAllCached( - 'user_id = :user_id', - ['user_id' => $userId], - ); + return $this->_countByAllCached(['user_id' => $userId]); } public function truncate(): void diff --git a/tests/TestStand/Tables/TestCompositeSdCachedTable.php b/tests/TestStand/Tables/TestCompositeSdCachedTable.php deleted file mode 100644 index aefee62..0000000 --- a/tests/TestStand/Tables/TestCompositeSdCachedTable.php +++ /dev/null @@ -1,73 +0,0 @@ -init(); - } - - public function save(AbstractEntity|TestCompositeSdEntity &$entity): void - { - if ($entity->message === 'Exception') { - throw new \Exception('Test Exception'); - } - parent::save($entity); - } - - protected function getConfig(): TableConfig - { - return TableConfig::fromEntitySchema(TestCompositeSdEntity::schema()); - } - - protected function getFlushCacheKeys(TestCompositeSdEntity|AbstractEntity $entity): array - { - return [ - $this->getListCacheKey('user_id = :user_id', ['user_id' => $entity->user_id]), - $this->getCountCacheKey('user_id = :user_id', ['user_id' => $entity->user_id]), - ]; - } - - public function findOne(int $user_id, int $post_id): ?TestCompositeSdEntity - { - return $this->createEntity($this->_findOneCached([ - 'user_id' => $user_id, - 'post_id' => $post_id, - ])); - } - - /** - * @return TestCompositeSdEntity[] - */ - public function findAllByUser(int $userId): array - { - return array_map( - fn (array $data) => TestCompositeSdEntity::fromArray($data), - $this->_findAllCached( - 'user_id = :user_id', - ['user_id' => $userId], - ) - ); - } - - public function countAllByUser(int $userId): int - { - return $this->_countAllCached( - 'user_id = :user_id', - ['user_id' => $userId], - ); - } - - public function truncate(): void - { - $this->getConnection()->executeStatement("DELETE FROM {$this->getTableName()} WHERE 1"); - } -} \ No newline at end of file diff --git a/tests/TestStand/Tables/TestCompositeSdTable.php b/tests/TestStand/Tables/TestCompositeSdTable.php deleted file mode 100644 index 3217f73..0000000 --- a/tests/TestStand/Tables/TestCompositeSdTable.php +++ /dev/null @@ -1,65 +0,0 @@ -init(); - } - - protected function getConfig(): TableConfig - { - return TableConfig::fromEntitySchema(TestCompositeSdEntity::schema()); - } - - public function findOne(int $user_id, int $post_id): ?TestCompositeSdEntity - { - return $this->createEntity($this->_findOne([ - 'user_id' => $user_id, - 'post_id' => $post_id, - ])); - } - - /** - * @return TestCompositeSdEntity[] - */ - public function findAllByUser(int $userId): array - { - return $this->createEntities($this->_findAll( - 'user_id = :user_id', - ['user_id' => $userId], - )); - } - - public function countAllByUser(int $userId): int - { - return $this->_countAll( - 'user_id = :user_id', - ['user_id' => $userId], - ); - } - - public function init(): bool - { - $this->getConnection()->executeStatement( - " - CREATE TABLE IF NOT EXISTS {$this->getTableName()} - ( - `user_id` integer not null, - `post_id` integer not null, - `message` VARCHAR(255) DEFAULT '' NOT NULL, - `created_at` TIMESTAMP NOT NULL, - `deleted_at` TIMESTAMP NULL DEFAULT NULL, - CONSTRAINT TestCompositeSd PRIMARY KEY (`user_id`, `post_id`, `deleted_at`) - ); - " - ); - return true; - } -} \ No newline at end of file diff --git a/tests/TestStand/Tables/TestCompositeTable.php b/tests/TestStand/Tables/TestCompositeTable.php index 50639be..2fe6910 100644 --- a/tests/TestStand/Tables/TestCompositeTable.php +++ b/tests/TestStand/Tables/TestCompositeTable.php @@ -3,6 +3,7 @@ namespace Composite\DB\Tests\TestStand\Tables; use Composite\DB\TableConfig; +use Composite\DB\Tests\TestStand\Entities\Enums\TestUnitEnum; use Composite\DB\Tests\TestStand\Entities\TestCompositeEntity; use Composite\DB\Tests\TestStand\Interfaces\ICompositeTable; use Composite\Entity\AbstractEntity; @@ -40,18 +41,12 @@ public function findOne(int $user_id, int $post_id): ?TestCompositeEntity */ public function findAllByUser(int $userId): array { - return $this->createEntities($this->_findAll( - 'user_id = :user_id', - ['user_id' => $userId], - )); + return $this->createEntities($this->_findAll(['user_id' => $userId, 'status' => TestUnitEnum::ACTIVE])); } public function countAllByUser(int $userId): int { - return $this->_countAll( - 'user_id = :user_id', - ['user_id' => $userId, 'deleted_at' => null], - ); + return $this->_countAll(['user_id' => $userId]); } /** @@ -73,6 +68,7 @@ public function init(): bool `user_id` integer not null, `post_id` integer not null, `message` VARCHAR(255) DEFAULT '' NOT NULL, + `status` VARCHAR(16) DEFAULT 'ACTIVE' NOT NULL, `created_at` TIMESTAMP NOT NULL, CONSTRAINT TestComposite PRIMARY KEY (`user_id`, `post_id`) ); diff --git a/tests/TestStand/Tables/TestUniqueCachedTable.php b/tests/TestStand/Tables/TestUniqueCachedTable.php index cdcaccb..cd7c901 100644 --- a/tests/TestStand/Tables/TestUniqueCachedTable.php +++ b/tests/TestStand/Tables/TestUniqueCachedTable.php @@ -6,6 +6,7 @@ use Composite\DB\TableConfig; use Composite\DB\Tests\TestStand\Entities\TestUniqueEntity; use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable; +use Composite\DB\Where; use Composite\Entity\AbstractEntity; use Ramsey\Uuid\UuidInterface; @@ -25,8 +26,8 @@ protected function getConfig(): TableConfig protected function getFlushCacheKeys(TestUniqueEntity|AbstractEntity $entity): array { return [ - $this->getListCacheKey('name = :name', ['name' => $entity->name]), - $this->getCountCacheKey('name = :name', ['name' => $entity->name]), + $this->getListCacheKey(new Where('name = :name', ['name' => $entity->name])), + $this->getCountCacheKey(new Where('name = :name', ['name' => $entity->name])), ]; } @@ -41,17 +42,13 @@ public function findByPk(UuidInterface $id): ?TestUniqueEntity public function findAllByName(string $name): array { return $this->createEntities($this->_findAllCached( - 'name = :name', - ['name' => $name], + new Where('name = :name', ['name' => $name]) )); } public function countAllByName(string $name): int { - return $this->_countAllCached( - 'name = :name', - ['name' => $name], - ); + return $this->_countByAllCached(new Where('name = :name', ['name' => $name])); } public function truncate(): void diff --git a/tests/TestStand/Tables/TestUniqueSdCachedTable.php b/tests/TestStand/Tables/TestUniqueSdCachedTable.php deleted file mode 100644 index f5877ec..0000000 --- a/tests/TestStand/Tables/TestUniqueSdCachedTable.php +++ /dev/null @@ -1,61 +0,0 @@ -init(); - } - - protected function getConfig(): TableConfig - { - return TableConfig::fromEntitySchema(TestUniqueSdEntity::schema()); - } - - protected function getFlushCacheKeys(TestUniqueSdEntity|AbstractEntity $entity): array - { - return [ - $this->getListCacheKey('name = :name', ['name' => $entity->name]), - $this->getCountCacheKey('name = :name', ['name' => $entity->name]), - ]; - } - - public function findByPk(UuidInterface $id): ?TestUniqueSdEntity - { - return $this->createEntity($this->_findByPk($id)); - } - - /** - * @return TestUniqueSdEntity[] - */ - public function findAllByName(string $name): array - { - return $this->createEntities($this->_findAllCached( - 'name = :name', - ['name' => $name], - )); - } - - public function countAllByName(string $name): int - { - return $this->_countAllCached( - 'name = :name', - ['name' => $name], - ); - } - - public function truncate(): void - { - $this->getConnection()->executeStatement("DELETE FROM {$this->getTableName()} WHERE 1"); - } -} \ No newline at end of file diff --git a/tests/TestStand/Tables/TestUniqueSdTable.php b/tests/TestStand/Tables/TestUniqueSdTable.php deleted file mode 100644 index 5f61e0d..0000000 --- a/tests/TestStand/Tables/TestUniqueSdTable.php +++ /dev/null @@ -1,54 +0,0 @@ -init(); - } - - protected function getConfig(): TableConfig - { - return TableConfig::fromEntitySchema(TestUniqueSdEntity::schema()); - } - - public function findByPk(UuidInterface $id): ?TestUniqueSdEntity - { - return $this->createEntity($this->_findByPk($id)); - } - - /** - * @return TestUniqueSdEntity[] - */ - public function findAllByName(string $name): array - { - return $this->createEntities($this->_findAll( - 'name = :name', - ['name' => $name], - )); - } - - public function init(): bool - { - $this->getConnection()->executeStatement( - " - CREATE TABLE IF NOT EXISTS {$this->getTableName()} - ( - `id` VARCHAR(32) NOT NULL, - `name` VARCHAR(255) NOT NULL, - `created_at` TIMESTAMP NOT NULL, - `deleted_at` TIMESTAMP NULL DEFAULT NULL, - CONSTRAINT TestUniqueSd PRIMARY KEY (`id`, `deleted_at`) - ); - " - ); - return true; - } -} \ No newline at end of file diff --git a/tests/TestStand/Tables/TestUniqueTable.php b/tests/TestStand/Tables/TestUniqueTable.php index befb866..0ad6264 100644 --- a/tests/TestStand/Tables/TestUniqueTable.php +++ b/tests/TestStand/Tables/TestUniqueTable.php @@ -4,8 +4,10 @@ use Composite\DB\AbstractTable; use Composite\DB\TableConfig; +use Composite\DB\Tests\TestStand\Entities\Enums\TestBackedEnum; use Composite\DB\Tests\TestStand\Entities\TestUniqueEntity; use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable; +use Composite\DB\Where; use Composite\Entity\AbstractEntity; use Ramsey\Uuid\UuidInterface; @@ -40,18 +42,12 @@ public function findByPk(UuidInterface $id): ?TestUniqueEntity */ public function findAllByName(string $name): array { - return $this->createEntities($this->_findAll( - 'name = :name', - ['name' => $name], - )); + return $this->createEntities($this->_findAll(['name' => $name, 'status' => TestBackedEnum::ACTIVE])); } public function countAllByName(string $name): int { - return $this->_countAll( - 'name = :name', - ['name' => $name], - ); + return $this->_countAll(new Where('name = :name', ['name' => $name])); } public function init(): bool @@ -62,6 +58,7 @@ public function init(): bool ( `id` VARCHAR(255) NOT NULL CONSTRAINT TestUnique_pk PRIMARY KEY, `name` VARCHAR(255) NOT NULL, + `status` VARCHAR(16) DEFAULT 'Active' NOT NULL, `created_at` TIMESTAMP NOT NULL ); " From c81df32ae1ce7bfc709ffa9188bc25c3d95353da Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 28 Oct 2023 18:26:39 +0100 Subject: [PATCH 50/69] Display warnings in unit tests --- phpunit.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit.xml b/phpunit.xml index f68b1fa..9b42ad0 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,6 +3,7 @@ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" bootstrap="tests/bootstrap.php" executionOrder="depends,defects" beStrictAboutOutputDuringTests="true" failOnRisky="true" failOnWarning="true" colors="true" cacheDirectory=".phpunit.cache" requireCoverageMetadata="false" + displayDetailsOnTestsThatTriggerWarnings="true" beStrictAboutCoverageMetadata="true"> From 93cc75b9c059d8c1f71e20ed3795a27061258f79 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 28 Oct 2023 18:26:59 +0100 Subject: [PATCH 51/69] Update documentation --- doc/cache.md | 1 - doc/example.md | 6 ++---- doc/table.md | 23 ++++++++++++++++++++--- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/doc/cache.md b/doc/cache.md index 7b64d1f..84b123d 100644 --- a/doc/cache.md +++ b/doc/cache.md @@ -55,7 +55,6 @@ class PostsTable extends AbstractCachedTable public function findAllFeatured(): array { return $this->createEntities($this->_findAll( - 'is_featured = :is_featured', ['is_featured' => true], )); } diff --git a/doc/example.md b/doc/example.md index 22a63fc..8709821 100644 --- a/doc/example.md +++ b/doc/example.md @@ -52,16 +52,14 @@ class UsersTable extends \Composite\DB\AbstractTable public function findAllActive(): array { return $this->createEntities($this->_findAll( - 'status = :status', - ['status' => Status::ACTIVE->name], + ['status' => Status::ACTIVE], )); } public function countAllActive(): int { return $this->_countAll( - 'status = :status', - ['status' => Status::ACTIVE->name], + ['status' => Status::ACTIVE], ); } diff --git a/doc/table.md b/doc/table.md index fbd7d1b..e6aec2e 100644 --- a/doc/table.md +++ b/doc/table.md @@ -68,14 +68,31 @@ Example with internal helper: public function findAllActiveAdults(): array { $rows = $this->_findAll( - 'age > :age AND status = :status', - ['age' => 18, 'status' => Status::ACTIVE->name], + new Where( + 'age > :age AND status = :status', + ['age' => 18, 'status' => Status::ACTIVE->name], + ) ); return $this->createEntities($rows); } ``` -Example with pure query builder +Or it might be simplified to: +```php +/** + * @return User[] + */ +public function findAllActiveAdults(): array +{ + $rows = $this->_findAll([ + 'age' => ['>', 18], + 'status' => Status:ACTIVE, + ]); + return $this->createEntities($rows); +} +``` + +Or you can use standard Doctrine QueryBuilder ```php /** * @return User[] From ccc8ca5688040fe0a18a092e928806f4b948911a Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 28 Oct 2023 19:20:08 +0100 Subject: [PATCH 52/69] Minor improve AbstractCachedTableTest --- tests/Table/AbstractCachedTableTest.php | 55 ++++++++++++++++--------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/tests/Table/AbstractCachedTableTest.php b/tests/Table/AbstractCachedTableTest.php index 8ca3877..f73c4ec 100644 --- a/tests/Table/AbstractCachedTableTest.php +++ b/tests/Table/AbstractCachedTableTest.php @@ -58,41 +58,44 @@ public static function getCountCacheKey_dataProvider(): array { return [ [ - '', [], 'sqlite.TestAutoincrement.v1.c.all', ], [ - 'name = :name', - ['name' => 'John'], + new Where('name = :name', ['name' => 'John']), 'sqlite.TestAutoincrement.v1.c.name_eq_john', ], [ - ' name = :name ', ['name' => 'John'], + 'sqlite.TestAutoincrement.v1.c.name_john', + ], + [ + new Where(' name = :name ', ['name' => 'John']), 'sqlite.TestAutoincrement.v1.c.name_eq_john', ], [ - 'name=:name', - ['name' => 'John'], + new Where('name=:name', ['name' => 'John']), 'sqlite.TestAutoincrement.v1.c.name_eq_john', ], [ - 'name = :name AND id > :id', - ['name' => 'John', 'id' => 10], + new Where('name = :name AND id > :id', ['name' => 'John', 'id' => 10]), 'sqlite.TestAutoincrement.v1.c.name_eq_john_and_id_gt_10', ], + [ + ['name' => 'John', 'id' => ['>', 10]], + 'sqlite.TestAutoincrement.v1.c.name_john_id_gt_10', + ], ]; } /** * @dataProvider getCountCacheKey_dataProvider */ - public function test_getCountCacheKey(string $whereString, array $whereParams, string $expected): void + public function test_getCountCacheKey(array|Where $where, string $expected): void { $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()); $reflectionMethod = new \ReflectionMethod($table, 'getCountCacheKey'); - $actual = $reflectionMethod->invoke($table, new Where($whereString, $whereParams)); + $actual = $reflectionMethod->invoke($table, $where); $this->assertEquals($expected, $actual); } @@ -100,43 +103,55 @@ public static function getListCacheKey_dataProvider(): array { return [ [ - '', [], [], null, 'sqlite.TestAutoincrement.v1.l.all', ], [ - '', [], [], 10, 'sqlite.TestAutoincrement.v1.l.all.limit_10', ], [ - '', [], ['id' => 'DESC'], 10, 'sqlite.TestAutoincrement.v1.l.all.ob_id_desc.limit_10', ], [ - 'name = :name', + new Where('name = :name', ['name' => 'John']), + [], + null, + 'sqlite.TestAutoincrement.v1.l.name_eq_john', + ], + [ ['name' => 'John'], [], null, + 'sqlite.TestAutoincrement.v1.l.name_john', + ], + [ + new Where('name = :name', ['name' => 'John']), + [], + null, 'sqlite.TestAutoincrement.v1.l.name_eq_john', ], [ - 'name = :name AND id > :id', - ['name' => 'John', 'id' => 10], + new Where('name = :name AND id > :id', ['name' => 'John', 'id' => 10]), [], null, 'sqlite.TestAutoincrement.v1.l.name_eq_john_and_id_gt_10', ], [ - 'name = :name AND id > :id', - ['name' => 'John', 'id' => 10], + ['name' => 'John', 'id' => ['>', 10]], + [], + null, + 'sqlite.TestAutoincrement.v1.l.name_john_id_gt_10', + ], + [ + new Where('name = :name AND id > :id', ['name' => 'John', 'id' => 10]), ['id' => 'ASC'], 20, 'bbcf331b765b682da02c4d21dbaa3342bf2c3f18', //sha1('sqlite.TestAutoincrement.v1.l.name_eq_john_and_id_gt_10.ob_id_asc.limit_20') @@ -147,11 +162,11 @@ public static function getListCacheKey_dataProvider(): array /** * @dataProvider getListCacheKey_dataProvider */ - public function test_getListCacheKey(string $whereString, array $whereArray, array $orderBy, ?int $limit, string $expected): void + public function test_getListCacheKey(array|Where $where, array $orderBy, ?int $limit, string $expected): void { $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()); $reflectionMethod = new \ReflectionMethod($table, 'getListCacheKey'); - $actual = $reflectionMethod->invoke($table, new Where($whereString, $whereArray), $orderBy, $limit); + $actual = $reflectionMethod->invoke($table, $where, $orderBy, $limit); $this->assertEquals($expected, $actual); } From 9a26bbe4dff1d3f10dea1e6d5fa4637232e9142c Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 28 Oct 2023 19:37:20 +0100 Subject: [PATCH 53/69] Add .scrutinizer.yml to export-ignore --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index 1989899..d09a5f7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ /.gitattributes export-ignore /.gitignore export-ignore /.github export-ignore +/.scrutinizer.yml export-ignore /doc export-ignore /phpunit.xml export-ignore /tests export-ignore From 1faf615424bcf6f4638a1f69ffd35cf296cacd7a Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 28 Oct 2023 20:04:13 +0100 Subject: [PATCH 54/69] Add possibility to use Where class in _findOne internal method --- src/AbstractTable.php | 76 +++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 59e89e3..49f225b 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -199,12 +199,12 @@ protected function _findByPk(mixed $pk): ?array } /** - * @param array $where + * @param array|Where $where * @param array|string $orderBy * @return array|null * @throws \Doctrine\DBAL\Exception */ - protected function _findOne(array $where, array|string $orderBy = []): ?array + protected function _findOne(array|Where $where, array|string $orderBy = []): ?array { $query = $this->select(); $this->buildWhere($query, $where); @@ -241,14 +241,7 @@ protected function _findAll( ): array { $query = $this->select(); - if (is_array($where)) { - $this->buildWhere($query, $where); - } else { - $query->where($where->string); - foreach ($where->params as $param => $value) { - $query->setParameter($param, $value); - } - } + $this->buildWhere($query, $where); $this->applyOrderBy($query, $orderBy); if ($limit > 0) { $query->setMaxResults($limit); @@ -332,40 +325,47 @@ protected function select(string $select = '*'): QueryBuilder } /** - * @param array $where + * @param array|Where $where */ - private function buildWhere(QueryBuilder $query, array $where): void + private function buildWhere(QueryBuilder $query, array|Where $where): void { - foreach ($where as $column => $value) { - if ($value instanceof \BackedEnum) { - $value = $value->value; - } elseif ($value instanceof \UnitEnum) { - $value = $value->name; - } + if (is_array($where)) { + foreach ($where as $column => $value) { + if ($value instanceof \BackedEnum) { + $value = $value->value; + } elseif ($value instanceof \UnitEnum) { + $value = $value->name; + } - if (is_null($value)) { - $query->andWhere($column . ' IS NULL'); - } elseif (is_array($value) && count($value) === 2 && \in_array($value[0], self::COMPARISON_SIGNS)) { - $comparisonSign = $value[0]; - $comparisonValue = $value[1]; + if (is_null($value)) { + $query->andWhere($column . ' IS NULL'); + } elseif (is_array($value) && count($value) === 2 && \in_array($value[0], self::COMPARISON_SIGNS)) { + $comparisonSign = $value[0]; + $comparisonValue = $value[1]; - // Handle special case of "!= null" - if ($comparisonSign === '!=' && is_null($comparisonValue)) { - $query->andWhere($column . ' IS NOT NULL'); + // Handle special case of "!= null" + if ($comparisonSign === '!=' && is_null($comparisonValue)) { + $query->andWhere($column . ' IS NOT NULL'); + } else { + $query->andWhere($column . ' ' . $comparisonSign . ' :' . $column) + ->setParameter($column, $comparisonValue); + } + } elseif (is_array($value)) { + $placeholders = []; + foreach ($value as $index => $val) { + $placeholders[] = ':' . $column . $index; + $query->setParameter($column . $index, $val); + } + $query->andWhere($column . ' IN(' . implode(', ', $placeholders) . ')'); } else { - $query->andWhere($column . ' ' . $comparisonSign . ' :' . $column) - ->setParameter($column, $comparisonValue); - } - } elseif (is_array($value)) { - $placeholders = []; - foreach ($value as $index => $val) { - $placeholders[] = ':' . $column . $index; - $query->setParameter($column . $index, $val); + $query->andWhere($column . ' = :' . $column) + ->setParameter($column, $value); } - $query->andWhere($column . ' IN(' . implode(', ', $placeholders) . ')'); - } else { - $query->andWhere($column . ' = :' . $column) - ->setParameter($column, $value); + } + } else { + $query->where($where->string); + foreach ($where->params as $param => $value) { + $query->setParameter($param, $value); } } } From 7a6a66ad6c57b0131f88bfeb4db942c105905823 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 28 Oct 2023 20:10:02 +0100 Subject: [PATCH 55/69] Rename Where.string to Where.condition --- src/AbstractCachedTable.php | 2 +- src/AbstractTable.php | 4 ++-- src/Where.php | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index dd3a2b7..6a6022c 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -282,7 +282,7 @@ private function prepareWhereKey(Where $where): string return str_replace( array_map(fn (string $key): string => ':' . $key, array_keys($where->params)), array_values($where->params), - $where->string, + $where->condition, ); } } diff --git a/src/AbstractTable.php b/src/AbstractTable.php index 49f225b..dad1381 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -180,7 +180,7 @@ protected function _countAll(array|Where $where = []): int if (is_array($where)) { $this->buildWhere($query, $where); } else { - $query->where($where->string); + $query->where($where->condition); foreach ($where->params as $param => $value) { $query->setParameter($param, $value); } @@ -363,7 +363,7 @@ private function buildWhere(QueryBuilder $query, array|Where $where): void } } } else { - $query->where($where->string); + $query->where($where->condition); foreach ($where->params as $param => $value) { $query->setParameter($param, $value); } diff --git a/src/Where.php b/src/Where.php index 41b545f..147b3aa 100644 --- a/src/Where.php +++ b/src/Where.php @@ -5,11 +5,11 @@ class Where { /** - * @param string $string free format where string, example: "user_id = :user_id OR user_id > 0" - * @param array $params params with placeholders, which used in $string, example: ['user_id' => 123], + * @param string $condition free format where string, example: "user_id = :user_id OR user_id > 0" + * @param array $params params with placeholders, which used in $condition, example: ['user_id' => 123], */ public function __construct( - public readonly string $string, + public readonly string $condition, public readonly array $params, ) { } From e0446683211efe7b43abf63ffac3a265b0c68791 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 11 Nov 2023 21:52:38 +0000 Subject: [PATCH 56/69] phpstan fixes --- src/MultiQuery/MultiSelect.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/MultiQuery/MultiSelect.php b/src/MultiQuery/MultiSelect.php index 59459eb..c5171a7 100644 --- a/src/MultiQuery/MultiSelect.php +++ b/src/MultiQuery/MultiSelect.php @@ -12,6 +12,10 @@ class MultiSelect { private readonly QueryBuilder $queryBuilder; + /** + * @param array|array $condition + * @throws DbException + */ public function __construct( Connection $connection, TableConfig $tableConfig, @@ -32,7 +36,7 @@ public function __construct( } /** @var \Composite\Entity\Columns\AbstractColumn $pkColumn */ $pkColumn = reset($pkColumns); - $preparedPkValues = array_map(fn ($pk) => $pkColumn->uncast($pk), $condition); + $preparedPkValues = array_map(fn ($pk) => (string)$pkColumn->uncast($pk), $condition); $query->andWhere($query->expr()->in($pkColumn->name, $preparedPkValues)); } else { $expressions = []; From 019af835d163bf5357b2381ba28ddff136805156 Mon Sep 17 00:00:00 2001 From: Ivan Vasilkov Date: Sat, 18 Nov 2023 23:15:29 +0000 Subject: [PATCH 57/69] remove redundant use --- tests/Table/UniqueTableTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Table/UniqueTableTest.php b/tests/Table/UniqueTableTest.php index d4073fd..186ff97 100644 --- a/tests/Table/UniqueTableTest.php +++ b/tests/Table/UniqueTableTest.php @@ -3,7 +3,6 @@ namespace Composite\DB\Tests\Table; use Composite\DB\AbstractTable; -use Composite\DB\TableConfig; use Composite\DB\Tests\Helpers; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Tables; From b477a3a795dfdc8707499daac22a6bc0b8d43e0d Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 19 Nov 2023 10:03:06 +0000 Subject: [PATCH 58/69] Move raw selects to separate trait and build entities by default --- doc/cache.md | 6 +- doc/example.md | 6 +- doc/table.md | 10 +- src/AbstractCachedTable.php | 43 +++--- src/AbstractTable.php | 146 +++++------------- src/SelectRawTrait.php | 131 ++++++++++++++++ tests/Table/AbstractCachedTableTest.php | 4 +- .../Tables/TestAutoincrementCachedTable.php | 16 +- .../Tables/TestAutoincrementSdCachedTable.php | 12 +- .../Tables/TestAutoincrementSdTable.php | 12 +- .../Tables/TestAutoincrementTable.php | 14 +- .../Tables/TestCompositeCachedTable.php | 9 +- tests/TestStand/Tables/TestCompositeTable.php | 6 +- .../Tables/TestOptimisticLockTable.php | 2 +- .../Tables/TestUniqueCachedTable.php | 6 +- tests/TestStand/Tables/TestUniqueTable.php | 4 +- tests/TestStand/Tables/TestUpdateAtTable.php | 2 +- 17 files changed, 237 insertions(+), 192 deletions(-) create mode 100644 src/SelectRawTrait.php diff --git a/doc/cache.md b/doc/cache.md index 84b123d..a5d6340 100644 --- a/doc/cache.md +++ b/doc/cache.md @@ -46,7 +46,7 @@ class PostsTable extends AbstractCachedTable public function findByPk(int $id): ?Post { - return $this->createEntity($this->_findByPkCached($id)); + return $this->_findByPkCached($id); } /** @@ -54,9 +54,7 @@ class PostsTable extends AbstractCachedTable */ public function findAllFeatured(): array { - return $this->createEntities($this->_findAll( - ['is_featured' => true], - )); + return $this->_findAll(['is_featured' => true]); } public function countAllFeatured(): int diff --git a/doc/example.md b/doc/example.md index 8709821..4709c67 100644 --- a/doc/example.md +++ b/doc/example.md @@ -43,7 +43,7 @@ class UsersTable extends \Composite\DB\AbstractTable public function findByPk(int $id): ?User { - return $this->createEntity($this->_findByPk($id)); + return $this->_findByPk($id); } /** @@ -51,9 +51,7 @@ class UsersTable extends \Composite\DB\AbstractTable */ public function findAllActive(): array { - return $this->createEntities($this->_findAll( - ['status' => Status::ACTIVE], - )); + return $this->_findAll(['status' => Status::ACTIVE]); } public function countAllActive(): int diff --git a/doc/table.md b/doc/table.md index e6aec2e..852c55b 100644 --- a/doc/table.md +++ b/doc/table.md @@ -38,7 +38,7 @@ class UsersTable extends AbstractTable public function findOne(int $id): ?User { - return $this->createEntity($this->_findOne($id)); + return $this->_findByPk($id); } /** @@ -46,7 +46,7 @@ class UsersTable extends AbstractTable */ public function findAll(): array { - return $this->createEntities($this->_findAll()); + return $this->_findAll(); } public function countAll(): int @@ -67,13 +67,12 @@ Example with internal helper: */ public function findAllActiveAdults(): array { - $rows = $this->_findAll( + return $this->_findAll( new Where( 'age > :age AND status = :status', ['age' => 18, 'status' => Status::ACTIVE->name], ) ); - return $this->createEntities($rows); } ``` @@ -84,11 +83,10 @@ Or it might be simplified to: */ public function findAllActiveAdults(): array { - $rows = $this->_findAll([ + return $this->_findAll([ 'age' => ['>', 18], 'status' => Status:ACTIVE, ]); - return $this->createEntities($rows); } ``` diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index 6a6022c..590403e 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -2,14 +2,14 @@ namespace Composite\DB; -use Composite\DB\Exceptions\DbException; -use Composite\DB\Tests\TestStand\Entities\TestAutoincrementEntity; use Composite\Entity\AbstractEntity; use Psr\SimpleCache\CacheInterface; use Ramsey\Uuid\UuidInterface; abstract class AbstractCachedTable extends AbstractTable { + use SelectRawTrait; + protected const CACHE_VERSION = 1; public function __construct( @@ -94,44 +94,47 @@ private function collectCacheKeysByEntity(AbstractEntity $entity): array } /** - * @return array|null + * @return AbstractEntity|null */ - protected function _findByPkCached(mixed $pk, null|int|\DateInterval $ttl = null): ?array + protected function _findByPkCached(mixed $pk, null|int|\DateInterval $ttl = null): mixed { return $this->_findOneCached($this->getPkCondition($pk), $ttl); } /** - * @param array $condition + * @param array $where * @param int|\DateInterval|null $ttl - * @return array|null + * @return AbstractEntity|null */ - protected function _findOneCached(array $condition, null|int|\DateInterval $ttl = null): ?array + protected function _findOneCached(array $where, null|int|\DateInterval $ttl = null): mixed { - return $this->getCached( - $this->getOneCacheKey($condition), - fn() => $this->_findOne($condition), + $row = $this->getCached( + $this->getOneCacheKey($where), + fn() => $this->_findOneRaw($where), $ttl, - ) ?: null; + ); + return $this->createEntity($row); } /** * @param array|Where $where * @param array|string $orderBy - * @return array[] + * @return array|array */ protected function _findAllCached( array|Where $where = [], array|string $orderBy = [], ?int $limit = null, null|int|\DateInterval $ttl = null, + ?string $keyColumnName = null, ): array { - return $this->getCached( + $rows = $this->getCached( $this->getListCacheKey($where, $orderBy, $limit), - fn() => $this->_findAll(where: $where, orderBy: $orderBy, limit: $limit), + fn() => $this->_findAllRaw(where: $where, orderBy: $orderBy, limit: $limit), $ttl, ); + return $this->createEntities($rows, $keyColumnName); } /** @@ -165,10 +168,14 @@ protected function getCached(string $cacheKey, callable $dataCallback, null|int| /** * @param mixed[] $ids * @param int|\DateInterval|null $ttl - * @return array> + * @return array|array * @throws \Psr\SimpleCache\InvalidArgumentException */ - protected function _findMultiCached(array $ids, null|int|\DateInterval $ttl = null): array + protected function _findMultiCached( + array $ids, + null|int|\DateInterval $ttl = null, + ?string $keyColumnName = null, + ): array { $result = $cacheKeys = $foundIds = []; foreach ($ids as $id) { @@ -191,7 +198,7 @@ protected function _findMultiCached(array $ids, null|int|\DateInterval $ttl = nu $result[] = $row; } } - return $result; + return $this->createEntities($result, $keyColumnName); } /** @@ -271,7 +278,7 @@ protected function buildCacheKey(mixed ...$parts): string private function formatStringForCacheKey(string $string): string { - $string = mb_strtolower($string); + $string = strtolower($string); $string = str_replace(['!=', '<>', '>', '<', '='], ['_not_', '_not_', '_gt_', '_lt_', '_eq_'], $string); $string = (string)preg_replace('/\W/', '_', $string); return trim((string)preg_replace('/_+/', '_', $string), '_'); diff --git a/src/AbstractTable.php b/src/AbstractTable.php index dad1381..df058a1 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -9,15 +9,13 @@ use Composite\DB\Exceptions\DbException; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; -use Doctrine\DBAL\Query\QueryBuilder; use Ramsey\Uuid\UuidInterface; abstract class AbstractTable { - private const COMPARISON_SIGNS = ['=', '!=', '>', '<', '>=', '<=', '<>']; + use SelectRawTrait; protected readonly TableConfig $config; - private ?QueryBuilder $selectQuery = null; abstract protected function getConfig(): TableConfig; @@ -189,10 +187,10 @@ protected function _countAll(array|Where $where = []): int } /** - * @return array|null * @throws \Doctrine\DBAL\Exception + * @return AbstractEntity|null */ - protected function _findByPk(mixed $pk): ?array + protected function _findByPk(mixed $pk): mixed { $where = $this->getPkCondition($pk); return $this->_findOne($where); @@ -201,55 +199,54 @@ protected function _findByPk(mixed $pk): ?array /** * @param array|Where $where * @param array|string $orderBy - * @return array|null + * @return AbstractEntity|null * @throws \Doctrine\DBAL\Exception */ - protected function _findOne(array|Where $where, array|string $orderBy = []): ?array + protected function _findOne(array|Where $where, array|string $orderBy = []): mixed { - $query = $this->select(); - $this->buildWhere($query, $where); - $this->applyOrderBy($query, $orderBy); - return $query->fetchAssociative() ?: null; + return $this->createEntity($this->_findOneRaw($where, $orderBy)); } /** * @param array> $pkList - * @return array> + * @return array| array * @throws DbException * @throws \Doctrine\DBAL\Exception */ - protected function _findMulti(array $pkList): array + protected function _findMulti(array $pkList, ?string $keyColumnName = null): array { if (!$pkList) { return []; } $multiSelect = new MultiSelect($this->getConnection(), $this->config, $pkList); - return $multiSelect->getQueryBuilder()->executeQuery()->fetchAllAssociative(); + return $this->createEntities( + $multiSelect->getQueryBuilder()->executeQuery()->fetchAllAssociative(), + $keyColumnName, + ); } /** * @param array|Where $where * @param array|string $orderBy - * @return list> - * @throws \Doctrine\DBAL\Exception + * @return array| array */ protected function _findAll( array|Where $where = [], array|string $orderBy = [], ?int $limit = null, ?int $offset = null, + ?string $keyColumnName = null, ): array { - $query = $this->select(); - $this->buildWhere($query, $where); - $this->applyOrderBy($query, $orderBy); - if ($limit > 0) { - $query->setMaxResults($limit); - } - if ($offset > 0) { - $query->setFirstResult($offset); - } - return $query->executeQuery()->fetchAllAssociative(); + return $this->createEntities( + data: $this->_findAllRaw( + where: $where, + orderBy: $orderBy, + limit: $limit, + offset: $offset, + ), + keyColumnName: $keyColumnName, + ); } final protected function createEntity(mixed $data): mixed @@ -279,13 +276,18 @@ final protected function createEntities(mixed $data, ?string $keyColumnName = nu $entityClass = $this->config->entityClass; $result = []; foreach ($data as $datum) { - if (!is_array($datum)) { - continue; - } - if ($keyColumnName && isset($datum[$keyColumnName])) { - $result[$datum[$keyColumnName]] = $entityClass::fromArray($datum); - } else { - $result[] = $entityClass::fromArray($datum); + if (is_array($datum)) { + if ($keyColumnName && isset($datum[$keyColumnName])) { + $result[$datum[$keyColumnName]] = $entityClass::fromArray($datum); + } else { + $result[] = $entityClass::fromArray($datum); + } + } elseif ($datum instanceof $this->config->entityClass) { + if ($keyColumnName && property_exists($datum, $keyColumnName)) { + $result[$datum->{$keyColumnName}] = $datum; + } else { + $result[] = $datum; + } } } } catch (\Throwable) { @@ -316,60 +318,6 @@ protected function getPkCondition(int|string|array|AbstractEntity|UuidInterface return $condition; } - protected function select(string $select = '*'): QueryBuilder - { - if ($this->selectQuery === null) { - $this->selectQuery = $this->getConnection()->createQueryBuilder()->from($this->getTableName()); - } - return (clone $this->selectQuery)->select($select); - } - - /** - * @param array|Where $where - */ - private function buildWhere(QueryBuilder $query, array|Where $where): void - { - if (is_array($where)) { - foreach ($where as $column => $value) { - if ($value instanceof \BackedEnum) { - $value = $value->value; - } elseif ($value instanceof \UnitEnum) { - $value = $value->name; - } - - if (is_null($value)) { - $query->andWhere($column . ' IS NULL'); - } elseif (is_array($value) && count($value) === 2 && \in_array($value[0], self::COMPARISON_SIGNS)) { - $comparisonSign = $value[0]; - $comparisonValue = $value[1]; - - // Handle special case of "!= null" - if ($comparisonSign === '!=' && is_null($comparisonValue)) { - $query->andWhere($column . ' IS NOT NULL'); - } else { - $query->andWhere($column . ' ' . $comparisonSign . ' :' . $column) - ->setParameter($column, $comparisonValue); - } - } elseif (is_array($value)) { - $placeholders = []; - foreach ($value as $index => $val) { - $placeholders[] = ':' . $column . $index; - $query->setParameter($column . $index, $val); - } - $query->andWhere($column . ' IN(' . implode(', ', $placeholders) . ')'); - } else { - $query->andWhere($column . ' = :' . $column) - ->setParameter($column, $value); - } - } - } else { - $query->where($where->condition); - foreach ($where->params as $param => $value) { - $query->setParameter($param, $value); - } - } - } - private function checkUpdatedAt(AbstractEntity $entity): void { if ($this->config->hasUpdatedAt() && property_exists($entity, 'updated_at') && $entity->updated_at === null) { @@ -392,28 +340,4 @@ private function formatData(array $data): array } return $data; } - - /** - * @param array|string $orderBy - */ - private function applyOrderBy(QueryBuilder $query, string|array $orderBy): void - { - if (!$orderBy) { - return; - } - if (is_array($orderBy)) { - foreach ($orderBy as $column => $direction) { - $query->addOrderBy($column, $direction); - } - } else { - foreach (explode(',', $orderBy) as $orderByPart) { - $orderByPart = trim($orderByPart); - if (preg_match('/(.+)\s(asc|desc)$/i', $orderByPart, $orderByPartMatch)) { - $query->addOrderBy($orderByPartMatch[1], $orderByPartMatch[2]); - } else { - $query->addOrderBy($orderByPart); - } - } - } - } } diff --git a/src/SelectRawTrait.php b/src/SelectRawTrait.php new file mode 100644 index 0000000..e1db259 --- /dev/null +++ b/src/SelectRawTrait.php @@ -0,0 +1,131 @@ +', '<', '>=', '<=', '<>']; + + private ?QueryBuilder $selectQuery = null; + + protected function select(string $select = '*'): QueryBuilder + { + if ($this->selectQuery === null) { + $this->selectQuery = $this->getConnection()->createQueryBuilder()->from($this->getTableName()); + } + return (clone $this->selectQuery)->select($select); + } + + /** + * @param array|Where $where + * @param array|string $orderBy + * @return array|null + * @throws \Doctrine\DBAL\Exception + */ + private function _findOneRaw(array|Where $where, array|string $orderBy = []): ?array + { + $query = $this->select(); + $this->buildWhere($query, $where); + $this->applyOrderBy($query, $orderBy); + return $query->fetchAssociative() ?: null; + } + + /** + * @param array|Where $where + * @param array|string $orderBy + * @return list> + * @throws \Doctrine\DBAL\Exception + */ + private function _findAllRaw( + array|Where $where = [], + array|string $orderBy = [], + ?int $limit = null, + ?int $offset = null, + ): array + { + $query = $this->select(); + $this->buildWhere($query, $where); + $this->applyOrderBy($query, $orderBy); + if ($limit > 0) { + $query->setMaxResults($limit); + } + if ($offset > 0) { + $query->setFirstResult($offset); + } + return $query->executeQuery()->fetchAllAssociative(); + } + + + /** + * @param array|Where $where + */ + private function buildWhere(QueryBuilder $query, array|Where $where): void + { + if (is_array($where)) { + foreach ($where as $column => $value) { + if ($value instanceof \BackedEnum) { + $value = $value->value; + } elseif ($value instanceof \UnitEnum) { + $value = $value->name; + } + + if (is_null($value)) { + $query->andWhere($column . ' IS NULL'); + } elseif (is_array($value) && count($value) === 2 && \in_array($value[0], $this->comparisonSigns)) { + $comparisonSign = $value[0]; + $comparisonValue = $value[1]; + + // Handle special case of "!= null" + if ($comparisonSign === '!=' && is_null($comparisonValue)) { + $query->andWhere($column . ' IS NOT NULL'); + } else { + $query->andWhere($column . ' ' . $comparisonSign . ' :' . $column) + ->setParameter($column, $comparisonValue); + } + } elseif (is_array($value)) { + $placeholders = []; + foreach ($value as $index => $val) { + $placeholders[] = ':' . $column . $index; + $query->setParameter($column . $index, $val); + } + $query->andWhere($column . ' IN(' . implode(', ', $placeholders) . ')'); + } else { + $query->andWhere($column . ' = :' . $column) + ->setParameter($column, $value); + } + } + } else { + $query->where($where->condition); + foreach ($where->params as $param => $value) { + $query->setParameter($param, $value); + } + } + } + + /** + * @param array|string $orderBy + */ + private function applyOrderBy(QueryBuilder $query, string|array $orderBy): void + { + if (!$orderBy) { + return; + } + if (is_array($orderBy)) { + foreach ($orderBy as $column => $direction) { + $query->addOrderBy($column, $direction); + } + } else { + foreach (explode(',', $orderBy) as $orderByPart) { + $orderByPart = trim($orderByPart); + if (preg_match('/(.+)\s(asc|desc)$/i', $orderByPart, $orderByPartMatch)) { + $query->addOrderBy($orderByPartMatch[1], $orderByPartMatch[2]); + } else { + $query->addOrderBy($orderByPart); + } + } + } + } +} \ No newline at end of file diff --git a/tests/Table/AbstractCachedTableTest.php b/tests/Table/AbstractCachedTableTest.php index f73c4ec..826ede0 100644 --- a/tests/Table/AbstractCachedTableTest.php +++ b/tests/Table/AbstractCachedTableTest.php @@ -272,8 +272,8 @@ public function test_findMulti(): void $table->save($e1); $table->save($e2); - $multi1 = $table->findMulti([$e1->id]); - $this->assertEquals($e1, $multi1[0]); + $multi1 = $table->findMulti([$e1->id], 'id'); + $this->assertEquals($e1, $multi1[$e1->id]); $multi2 = $table->findMulti([$e1->id, $e2->id]); $this->assertEquals($e1, $multi2[0]); diff --git a/tests/TestStand/Tables/TestAutoincrementCachedTable.php b/tests/TestStand/Tables/TestAutoincrementCachedTable.php index ebb3d96..fb05d76 100644 --- a/tests/TestStand/Tables/TestAutoincrementCachedTable.php +++ b/tests/TestStand/Tables/TestAutoincrementCachedTable.php @@ -40,12 +40,12 @@ protected function getFlushCacheKeys(TestAutoincrementEntity|AbstractEntity $ent public function findByPk(int $id): ?TestAutoincrementEntity { - return $this->createEntity($this->_findByPkCached($id)); + return $this->_findByPkCached($id); } public function findOneByName(string $name): ?TestAutoincrementEntity { - return $this->createEntity($this->_findOneCached(['name' => $name])); + return $this->_findOneCached(['name' => $name]); } public function delete(TestAutoincrementEntity|AbstractEntity &$entity): void @@ -61,9 +61,7 @@ public function delete(TestAutoincrementEntity|AbstractEntity &$entity): void */ public function findAllByName(string $name): array { - return $this->createEntities($this->_findAllCached( - new Where('name = :name', ['name' => $name]) - )); + return $this->_findAllCached(new Where('name = :name', ['name' => $name])); } /** @@ -71,11 +69,11 @@ public function findAllByName(string $name): array */ public function findRecent(int $limit, int $offset): array { - return $this->createEntities($this->_findAll( + return $this->_findAll( orderBy: ['id' => 'DESC'], limit: $limit, offset: $offset, - )); + ); } public function countAllByName(string $name): int @@ -86,9 +84,9 @@ public function countAllByName(string $name): int /** * @return TestAutoincrementEntity[] */ - public function findMulti(array $ids): array + public function findMulti(array $ids, ?string $keyColumnName = null): array { - return $this->createEntities($this->_findMultiCached($ids)); + return $this->_findMultiCached(ids: $ids, keyColumnName: $keyColumnName); } public function truncate(): void diff --git a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php index 4f432b7..c1134f0 100644 --- a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php +++ b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php @@ -40,12 +40,12 @@ protected function getFlushCacheKeys(TestAutoincrementSdEntity|AbstractEntity $e public function findByPk(int $id): ?TestAutoincrementSdEntity { - return $this->createEntity($this->_findByPk($id)); + return $this->_findByPk($id); } public function findOneByName(string $name): ?TestAutoincrementSdEntity { - return $this->createEntity($this->_findOneCached(['name' => $name])); + return $this->_findOneCached(['name' => $name]); } public function delete(TestAutoincrementSdEntity|AbstractEntity &$entity): void @@ -61,9 +61,7 @@ public function delete(TestAutoincrementSdEntity|AbstractEntity &$entity): void */ public function findAllByName(string $name): array { - return $this->createEntities($this->_findAllCached( - new Where('name = :name', ['name' => $name, 'deleted_at' => null]), - )); + return $this->_findAllCached(new Where('name = :name', ['name' => $name, 'deleted_at' => null])); } /** @@ -71,11 +69,11 @@ public function findAllByName(string $name): array */ public function findRecent(int $limit, int $offset): array { - return $this->createEntities($this->_findAll( + return $this->_findAll( orderBy: 'id DESC', limit: $limit, offset: $offset, - )); + ); } public function countAllByName(string $name): int diff --git a/tests/TestStand/Tables/TestAutoincrementSdTable.php b/tests/TestStand/Tables/TestAutoincrementSdTable.php index 519f063..f878db9 100644 --- a/tests/TestStand/Tables/TestAutoincrementSdTable.php +++ b/tests/TestStand/Tables/TestAutoincrementSdTable.php @@ -21,12 +21,12 @@ protected function getConfig(): TableConfig public function findByPk(int $id): ?TestAutoincrementSdEntity { - return $this->createEntity($this->_findByPk($id)); + return $this->_findByPk($id); } public function findOneByName(string $name): ?TestAutoincrementSdEntity { - return $this->createEntity($this->_findOne(['name' => $name, 'deleted_at' => null])); + return $this->_findOne(['name' => $name, 'deleted_at' => null]); } /** @@ -34,9 +34,7 @@ public function findOneByName(string $name): ?TestAutoincrementSdEntity */ public function findAllByName(string $name): array { - return $this->createEntities($this->_findAll( - new Where('name = :name', ['name' => $name]) - )); + return $this->_findAll(new Where('name = :name', ['name' => $name])); } /** @@ -44,12 +42,12 @@ public function findAllByName(string $name): array */ public function findRecent(int $limit, int $offset): array { - return $this->createEntities($this->_findAll( + return $this->_findAll( where: ['deleted_at' => null], orderBy: 'id DESC', limit: $limit, offset: $offset, - )); + ); } public function init(): bool diff --git a/tests/TestStand/Tables/TestAutoincrementTable.php b/tests/TestStand/Tables/TestAutoincrementTable.php index de0e994..7b6ac40 100644 --- a/tests/TestStand/Tables/TestAutoincrementTable.php +++ b/tests/TestStand/Tables/TestAutoincrementTable.php @@ -24,12 +24,12 @@ protected function getConfig(): TableConfig public function findByPk(int $id): ?TestAutoincrementEntity { - return $this->createEntity($this->_findByPk($id)); + return $this->_findByPk($id); } public function findOneByName(string $name): ?TestAutoincrementEntity { - return $this->createEntity($this->_findOne(['name' => $name])); + return $this->_findOne(['name' => $name]); } public function delete(AbstractEntity|TestAutoincrementEntity &$entity): void @@ -45,10 +45,10 @@ public function delete(AbstractEntity|TestAutoincrementEntity &$entity): void */ public function findAllByName(string $name): array { - return $this->createEntities($this->_findAll( + return $this->_findAll( where: new Where('name = :name', ['name' => $name]), orderBy: 'id', - )); + ); } /** @@ -56,11 +56,11 @@ public function findAllByName(string $name): array */ public function findRecent(int $limit, int $offset): array { - return $this->createEntities($this->_findAll( + return $this->_findAll( orderBy: ['id' => 'DESC'], limit: $limit, offset: $offset, - )); + ); } public function countAllByName(string $name): int @@ -75,7 +75,7 @@ public function countAllByName(string $name): int */ public function findMulti(array $ids): array { - return $this->createEntities($this->_findMulti($ids), 'id'); + return $this->_findMulti($ids, 'id'); } public function init(): bool diff --git a/tests/TestStand/Tables/TestCompositeCachedTable.php b/tests/TestStand/Tables/TestCompositeCachedTable.php index 57f3203..6b5996a 100644 --- a/tests/TestStand/Tables/TestCompositeCachedTable.php +++ b/tests/TestStand/Tables/TestCompositeCachedTable.php @@ -30,10 +30,10 @@ protected function getFlushCacheKeys(TestCompositeEntity|AbstractEntity $entity) public function findOne(int $user_id, int $post_id): ?TestCompositeEntity { - return $this->createEntity($this->_findOneCached([ + return $this->_findOneCached([ 'user_id' => $user_id, 'post_id' => $post_id, - ])); + ]); } /** @@ -41,10 +41,7 @@ public function findOne(int $user_id, int $post_id): ?TestCompositeEntity */ public function findAllByUser(int $userId): array { - return array_map( - fn (array $data) => TestCompositeEntity::fromArray($data), - $this->_findAllCached(['user_id' => $userId]) - ); + return $this->_findAllCached(['user_id' => $userId]); } public function countAllByUser(int $userId): int diff --git a/tests/TestStand/Tables/TestCompositeTable.php b/tests/TestStand/Tables/TestCompositeTable.php index 2fe6910..64e90d4 100644 --- a/tests/TestStand/Tables/TestCompositeTable.php +++ b/tests/TestStand/Tables/TestCompositeTable.php @@ -33,7 +33,7 @@ public function delete(AbstractEntity|TestCompositeEntity &$entity): void public function findOne(int $user_id, int $post_id): ?TestCompositeEntity { - return $this->createEntity($this->_findOne(['user_id' => $user_id, 'post_id' => $post_id])); + return $this->_findOne(['user_id' => $user_id, 'post_id' => $post_id]); } /** @@ -41,7 +41,7 @@ public function findOne(int $user_id, int $post_id): ?TestCompositeEntity */ public function findAllByUser(int $userId): array { - return $this->createEntities($this->_findAll(['user_id' => $userId, 'status' => TestUnitEnum::ACTIVE])); + return $this->_findAll(['user_id' => $userId, 'status' => TestUnitEnum::ACTIVE]); } public function countAllByUser(int $userId): int @@ -56,7 +56,7 @@ public function countAllByUser(int $userId): int */ public function findMulti(array $ids): array { - return $this->createEntities($this->_findMulti($ids), 'post_id'); + return $this->_findMulti($ids, 'post_id'); } public function init(): bool diff --git a/tests/TestStand/Tables/TestOptimisticLockTable.php b/tests/TestStand/Tables/TestOptimisticLockTable.php index 375a61a..1ede3bc 100644 --- a/tests/TestStand/Tables/TestOptimisticLockTable.php +++ b/tests/TestStand/Tables/TestOptimisticLockTable.php @@ -21,7 +21,7 @@ protected function getConfig(): TableConfig public function findByPk(int $id): ?TestOptimisticLockEntity { - return $this->createEntity($this->_findByPk($id)); + return $this->_findByPk($id); } public function init(): bool diff --git a/tests/TestStand/Tables/TestUniqueCachedTable.php b/tests/TestStand/Tables/TestUniqueCachedTable.php index cd7c901..8fe714f 100644 --- a/tests/TestStand/Tables/TestUniqueCachedTable.php +++ b/tests/TestStand/Tables/TestUniqueCachedTable.php @@ -33,7 +33,7 @@ protected function getFlushCacheKeys(TestUniqueEntity|AbstractEntity $entity): a public function findByPk(UuidInterface $id): ?TestUniqueEntity { - return $this->createEntity($this->_findByPk($id)); + return $this->_findByPk($id); } /** @@ -41,9 +41,7 @@ public function findByPk(UuidInterface $id): ?TestUniqueEntity */ public function findAllByName(string $name): array { - return $this->createEntities($this->_findAllCached( - new Where('name = :name', ['name' => $name]) - )); + return $this->_findAllCached(new Where('name = :name', ['name' => $name])); } public function countAllByName(string $name): int diff --git a/tests/TestStand/Tables/TestUniqueTable.php b/tests/TestStand/Tables/TestUniqueTable.php index 0ad6264..8353194 100644 --- a/tests/TestStand/Tables/TestUniqueTable.php +++ b/tests/TestStand/Tables/TestUniqueTable.php @@ -34,7 +34,7 @@ protected function getConfig(): TableConfig public function findByPk(UuidInterface $id): ?TestUniqueEntity { - return $this->createEntity($this->_findByPk($id)); + return $this->_findByPk($id); } /** @@ -42,7 +42,7 @@ public function findByPk(UuidInterface $id): ?TestUniqueEntity */ public function findAllByName(string $name): array { - return $this->createEntities($this->_findAll(['name' => $name, 'status' => TestBackedEnum::ACTIVE])); + return $this->_findAll(['name' => $name, 'status' => TestBackedEnum::ACTIVE]); } public function countAllByName(string $name): int diff --git a/tests/TestStand/Tables/TestUpdateAtTable.php b/tests/TestStand/Tables/TestUpdateAtTable.php index 48c7c2a..ea114bd 100644 --- a/tests/TestStand/Tables/TestUpdateAtTable.php +++ b/tests/TestStand/Tables/TestUpdateAtTable.php @@ -21,7 +21,7 @@ protected function getConfig(): TableConfig public function findByPk(string $id): ?TestUpdatedAtEntity { - return $this->createEntity($this->_findByPk($id)); + return $this->_findByPk($id); } public function init(): bool From 04d24a2fe69236818538d9ca4be8483681d603f6 Mon Sep 17 00:00:00 2001 From: Composite PHP <38870693+compositephp@users.noreply.github.com> Date: Wed, 22 Nov 2023 10:31:04 +0000 Subject: [PATCH 59/69] Fix composite:generate-table example --- doc/migrations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/migrations.md b/doc/migrations.md index d6180e4..a5c65f5 100644 --- a/doc/migrations.md +++ b/doc/migrations.md @@ -77,7 +77,7 @@ The command examines the specific Entity and generates a [Table](https://github. This class acts as a gateway to a specific SQL table, providing user-friendly CRUD tools for interacting with SQL right off the bat. ```shell -php cli.php composite:generate-table connection_name TableName 'App\Models\EntityName' +php cli.php composite:generate-table 'App\Models\EntityName' ``` | Argument | Required | Description | @@ -90,4 +90,4 @@ Options: | Option | Description | |----------|--------------------------------------------| | --cached | Generate cached version of PHP Table class | -| --force | Overwrite existing file | \ No newline at end of file +| --force | Overwrite existing file | From f107e5cc402fc68ac7747c6ed5a1a6cc5d366e75 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 9 Dec 2023 16:57:41 +0000 Subject: [PATCH 60/69] Add migration commands description to documentation --- doc/migrations.md | 85 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/doc/migrations.md b/doc/migrations.md index a5c65f5..f772d25 100644 --- a/doc/migrations.md +++ b/doc/migrations.md @@ -2,9 +2,9 @@ > **_NOTE:_** This is experimental feature -Migrations enable you to maintain your database schema within your PHP entity classes. -Any modification made in your class triggers the generation of migration files. -These files execute SQL queries which synchronize the schema from the PHP class to the corresponding SQL table. +Migrations enable you to maintain your database schema within your PHP entity classes. +Any modification made in your class triggers the generation of migration files. +These files execute SQL queries which synchronize the schema from the PHP class to the corresponding SQL table. This mechanism ensures consistent alignment between your codebase and the database structure. ## Supported Databases @@ -26,6 +26,9 @@ You need to configure ConnectionManager, see instructions [here](configuration.m ### 3. Configure commands Add [symfony/console](https://symfony.com/doc/current/components/console.html) commands to your application: +- Composite\Sync\Commands\MigrateCommand +- Composite\Sync\Commands\MigrateNewCommand +- Composite\Sync\Commands\MigrateDownCommand - Composite\Sync\Commands\GenerateEntityCommand - Composite\Sync\Commands\GenerateTableCommand @@ -40,9 +43,15 @@ use Symfony\Component\Console\Application; //may be changed with .env file putenv('CONNECTIONS_CONFIG_FILE=/path/to/your/connections/config.php'); +putenv('ENTITIES_DIR=/path/to/your/source/dir'); // e.g. "./src" +putenv('MIGRATIONS_DIR=/path/to/your/migrations/dir'); // e.g. "./src/Migrations" +putenv('MIGRATIONS_NAMESPACE=Migrations\Namespace'); // e.g. "App\Migrations" $app = new Application(); $app->addCommands([ + new Commands\MigrateCommand(), + new Commands\MigrateNewCommand(), + new Commands\MigrateDownCommand(), new Commands\GenerateEntityCommand(), new Commands\GenerateTableCommand(), ]); @@ -50,6 +59,62 @@ $app->run(); ``` ## Available commands +* ### composite:migrate + +This command performs two primary functions depending on its usage context. Initially, when called for the first time, +it scans all entities located in the `ENTITIES_DIR` directory and generates migration files corresponding to these entities. +This initial step prepares the necessary migration scripts based on the current entity definitions. Upon its second +invocation, the command shifts its role to apply these generated migration scripts to the database. This two-step process +ensures that the database schema is synchronized with the entity definitions, first by preparing the migration scripts +and then by executing them to update the database. + +```shell +php cli.php composite:migrate +``` + +| Option | Short | Description | +|--------------|-------|-----------------------------------------------------------| +| --connection | -c | Check migrations for all entities with desired connection | +| --entity | -e | Check migrations only for entity class | +| --run | -r | Run migrations without asking for confirmation | +| --dry | -d | Dry run mode, no real SQL queries will be executed | + +* ### composite:migrate-new + +This command generates a new, empty migration file. The file is provided as a template for the user to fill with the +necessary database schema changes or updates. This command is typically used for initiating a new database migration, +where the user can define the specific changes to be applied to the database schema. The generated file needs to be +manually edited to include the desired migration logic before it can be executed with the migration commands. + +```shell +php cli.php composite:migrate-new +``` + +| Argument | Required | Description | +|-------------|----------|------------------------------------------| +| connection | No | Name of connection from your config file | +| description | No | Short description of desired changes | + +* ### composite:migrate-down + +This command rolls back the most recently applied migration. It is useful for undoing the last schema change made to +the database. This can be particularly helpful during development or testing phases, where you might need to revert +recent changes quickly. + +```shell +php cli.php composite:migrate-down +``` + +| Argument | Required | Description | +|------------|----------|---------------------------------------------------------------------------| +| connection | No | Name of connection from your config file | +| limit | No | Number of migrations should be rolled back from current state, default: 1 | + + +| Option | Short | Description | +|--------|-------|-----------------------------------------------------| +| --dry | -d | Dry run mode, no real SQL queries will be executed | + * ### composite:generate-entity The command examines the specific SQL table and generates an [Composite\Entity\AbstractEntity](https://github.com/compositephp/entity) PHP class. @@ -67,9 +132,9 @@ php cli.php composite:generate-entity connection_name TableName 'App\Models\Enti Options: -| Option | Description | -|---------|-------------------------| -| --force | Overwrite existing file | +| Option | Short | Description | +|---------|-------|-------------------------| +| --force | -f | Overwrite existing file | * ### composite:generate-table @@ -87,7 +152,7 @@ php cli.php composite:generate-table 'App\Models\EntityName' Options: -| Option | Description | -|----------|--------------------------------------------| -| --cached | Generate cached version of PHP Table class | -| --force | Overwrite existing file | +| Option | Short | Description | +|----------|-------|--------------------------------------------| +| --cached | -c | Generate cached version of PHP Table class | +| --force | -f | Overwrite existing file | \ No newline at end of file From 1ae4801a62d93ab74ac66e0abd2322b4b9c46821 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 23 Dec 2023 14:30:49 +0000 Subject: [PATCH 61/69] Bump composite/entity dependency version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5bfced2..ef84c75 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "php": "^8.1", "ext-pdo": "*", "psr/simple-cache": "1 - 3", - "compositephp/entity": "^0.1.9", + "compositephp/entity": "^v0.1.11", "doctrine/dbal": "^3.5" }, "require-dev": { From ba500606bece7c413715473d95e9e219f2fc54c8 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 23 Dec 2023 14:42:39 +0000 Subject: [PATCH 62/69] Performance optimizations --- src/AbstractCachedTable.php | 6 +- src/AbstractTable.php | 96 ++++++++++--------- src/Helpers/DatabaseSpecificTrait.php | 60 ++++++++++++ src/{ => Helpers}/SelectRawTrait.php | 3 +- src/MultiQuery/MultiInsert.php | 18 +++- tests/MultiQuery/MultiInsertTest.php | 10 +- tests/Table/AbstractTableTest.php | 12 ++- .../Tables/TestAutoincrementCachedTable.php | 2 +- .../Tables/TestAutoincrementSdCachedTable.php | 2 +- .../Tables/TestAutoincrementTable.php | 2 +- tests/TestStand/Tables/TestCompositeTable.php | 4 +- tests/TestStand/Tables/TestMySQLTable.php | 25 +++++ tests/TestStand/Tables/TestPostgresTable.php | 25 +++++ tests/TestStand/Tables/TestUniqueTable.php | 2 +- 14 files changed, 204 insertions(+), 63 deletions(-) create mode 100644 src/Helpers/DatabaseSpecificTrait.php rename src/{ => Helpers}/SelectRawTrait.php (98%) create mode 100644 tests/TestStand/Tables/TestMySQLTable.php create mode 100644 tests/TestStand/Tables/TestPostgresTable.php diff --git a/src/AbstractCachedTable.php b/src/AbstractCachedTable.php index 590403e..24ff5f9 100644 --- a/src/AbstractCachedTable.php +++ b/src/AbstractCachedTable.php @@ -8,7 +8,7 @@ abstract class AbstractCachedTable extends AbstractTable { - use SelectRawTrait; + use Helpers\SelectRawTrait; protected const CACHE_VERSION = 1; @@ -26,7 +26,7 @@ abstract protected function getFlushCacheKeys(AbstractEntity $entity): array; /** * @throws \Throwable */ - public function save(AbstractEntity &$entity): void + public function save(AbstractEntity $entity): void { $cacheKeys = $this->collectCacheKeysByEntity($entity); parent::save($entity); @@ -54,7 +54,7 @@ public function saveMany(array $entities): void /** * @throws \Throwable */ - public function delete(AbstractEntity &$entity): void + public function delete(AbstractEntity $entity): void { $cacheKeys = $this->collectCacheKeysByEntity($entity); parent::delete($entity); diff --git a/src/AbstractTable.php b/src/AbstractTable.php index df058a1..e144cd3 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -2,21 +2,22 @@ namespace Composite\DB; +use Composite\DB\Exceptions\DbException; use Composite\DB\MultiQuery\MultiInsert; use Composite\DB\MultiQuery\MultiSelect; -use Composite\Entity\Helpers\DateTimeHelper; use Composite\Entity\AbstractEntity; -use Composite\DB\Exceptions\DbException; +use Composite\Entity\Helpers\DateTimeHelper; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Ramsey\Uuid\UuidInterface; abstract class AbstractTable { - use SelectRawTrait; + use Helpers\SelectRawTrait; + use Helpers\DatabaseSpecificTrait; protected readonly TableConfig $config; + abstract protected function getConfig(): TableConfig; public function __construct() @@ -44,49 +45,51 @@ public function getConnectionName(): string * @return void * @throws \Throwable */ - public function save(AbstractEntity &$entity): void + public function save(AbstractEntity $entity): void { $this->config->checkEntity($entity); if ($entity->isNew()) { $connection = $this->getConnection(); $this->checkUpdatedAt($entity); - $insertData = $this->formatData($entity->toArray()); + $insertData = $this->prepareDataForSql($entity->toArray()); $this->getConnection()->insert($this->getTableName(), $insertData); - if ($this->config->autoIncrementKey) { - $insertData[$this->config->autoIncrementKey] = intval($connection->lastInsertId()); - $entity = $entity::fromArray($insertData); - } else { - $entity->resetChangedColumns(); + if ($this->config->autoIncrementKey && ($lastInsertedId = $connection->lastInsertId())) { + $insertData[$this->config->autoIncrementKey] = intval($lastInsertedId); + $entity::schema() + ->getColumn($this->config->autoIncrementKey) + ->setValue($entity, $insertData[$this->config->autoIncrementKey]); } + $entity->resetChangedColumns($insertData); } else { if (!$changedColumns = $entity->getChangedColumns()) { return; } - $connection = $this->getConnection(); - $where = $this->getPkCondition($entity); - + $changedColumns = $this->prepareDataForSql($changedColumns); if ($this->config->hasUpdatedAt() && property_exists($entity, 'updated_at')) { $entity->updated_at = new \DateTimeImmutable(); $changedColumns['updated_at'] = DateTimeHelper::dateTimeToString($entity->updated_at); } + $whereParams = $this->getPkCondition($entity); if ($this->config->hasOptimisticLock() && method_exists($entity, 'getVersion') && method_exists($entity, 'incrementVersion')) { - $where['lock_version'] = $entity->getVersion(); + $whereParams['lock_version'] = $entity->getVersion(); $entity->incrementVersion(); $changedColumns['lock_version'] = $entity->getVersion(); } - $entityUpdated = $connection->update( - table: $this->getTableName(), - data: $changedColumns, - criteria: $where, + $updateString = implode(', ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($changedColumns))); + $whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams))); + + $entityUpdated = (bool)$this->getConnection()->executeStatement( + sql: "UPDATE " . $this->escapeIdentifier($this->getTableName()) . " SET $updateString WHERE $whereString;", + params: array_merge(array_values($changedColumns), array_values($whereParams)), ); if ($this->config->hasOptimisticLock() && !$entityUpdated) { throw new Exceptions\LockException('Failed to update entity version, concurrency modification, rolling back.'); } - $entity->resetChangedColumns(); + $entity->resetChangedColumns($changedColumns); } } @@ -101,7 +104,7 @@ public function saveMany(array $entities): void if ($entity->isNew()) { $this->config->checkEntity($entity); $this->checkUpdatedAt($entity); - $rowsToInsert[] = $this->formatData($entity->toArray()); + $rowsToInsert[] = $this->prepareDataForSql($entity->toArray()); unset($entities[$i]); } } @@ -113,14 +116,15 @@ public function saveMany(array $entities): void } if ($rowsToInsert) { $chunks = array_chunk($rowsToInsert, 1000); + $connection = $this->getConnection(); foreach ($chunks as $chunk) { $multiInsert = new MultiInsert( + connection: $connection, tableName: $this->getTableName(), rows: $chunk, ); if ($multiInsert->getSql()) { - $stmt = $this->getConnection()->prepare($multiInsert->getSql()); - $stmt->executeQuery($multiInsert->getParameters()); + $connection->executeStatement($multiInsert->getSql(), $multiInsert->getParameters()); } } } @@ -135,7 +139,7 @@ public function saveMany(array $entities): void * @param AbstractEntity $entity * @throws \Throwable */ - public function delete(AbstractEntity &$entity): void + public function delete(AbstractEntity $entity): void { $this->config->checkEntity($entity); if ($this->config->hasSoftDelete()) { @@ -144,8 +148,12 @@ public function delete(AbstractEntity &$entity): void $this->save($entity); } } else { - $where = $this->getPkCondition($entity); - $this->getConnection()->delete($this->getTableName(), $where); + $whereParams = $this->getPkCondition($entity); + $whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams))); + $this->getConnection()->executeQuery( + sql: "DELETE FROM " . $this->escapeIdentifier($this->getTableName()) . " WHERE $whereString;", + params: array_values($whereParams), + ); } } @@ -192,8 +200,15 @@ protected function _countAll(array|Where $where = []): int */ protected function _findByPk(mixed $pk): mixed { - $where = $this->getPkCondition($pk); - return $this->_findOne($where); + $whereParams = $this->getPkCondition($pk); + $whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams))); + $row = $this->getConnection() + ->executeQuery( + sql: "SELECT * FROM " . $this->escapeIdentifier($this->getTableName()) . " WHERE $whereString;", + params: array_values($whereParams), + ) + ->fetchAssociative(); + return $this->createEntity($row); } /** @@ -304,7 +319,14 @@ protected function getPkCondition(int|string|array|AbstractEntity|UuidInterface { $condition = []; if ($data instanceof AbstractEntity) { - $data = $data->toArray(); + if ($data->isNew()) { + $data = $data->toArray(); + } else { + foreach ($this->config->primaryKeys as $key) { + $condition[$key] = $data->getOldValue($key); + } + return $condition; + } } if (is_array($data)) { foreach ($this->config->primaryKeys as $key) { @@ -324,20 +346,4 @@ private function checkUpdatedAt(AbstractEntity $entity): void $entity->updated_at = new \DateTimeImmutable(); } } - - /** - * @param array $data - * @return array - * @throws \Doctrine\DBAL\Exception - */ - private function formatData(array $data): array - { - $supportsBoolean = $this->getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform; - foreach ($data as $columnName => $value) { - if (is_bool($value) && !$supportsBoolean) { - $data[$columnName] = $value ? 1 : 0; - } - } - return $data; - } } diff --git a/src/Helpers/DatabaseSpecificTrait.php b/src/Helpers/DatabaseSpecificTrait.php new file mode 100644 index 0000000..14a72ff --- /dev/null +++ b/src/Helpers/DatabaseSpecificTrait.php @@ -0,0 +1,60 @@ +isPostgreSQL !== null) { + return; + } + $driver = $this->getConnection()->getDriver(); + if ($driver instanceof Driver\AbstractPostgreSQLDriver) { + $this->isPostgreSQL = true; + $this->isMySQL = $this->isSQLite = false; + } elseif ($driver instanceof Driver\AbstractSQLiteDriver) { + $this->isSQLite = true; + $this->isPostgreSQL = $this->isMySQL = false; + } elseif ($driver instanceof Driver\AbstractMySQLDriver) { + $this->isMySQL = true; + $this->isPostgreSQL = $this->isSQLite = false; + } else { + // @codeCoverageIgnoreStart + throw new DbException('Unsupported driver ' . $driver::class); + // @codeCoverageIgnoreEnd + } + } + + /** + * @param array $data + * @return array + */ + private function prepareDataForSql(array $data): array + { + $this->identifyPlatform(); + foreach ($data as $columnName => $value) { + if (is_bool($value) && !$this->isPostgreSQL) { + $data[$columnName] = $value ? 1 : 0; + } + } + return $data; + } + + protected function escapeIdentifier(string $key): string + { + $this->identifyPlatform(); + if ($this->isMySQL) { + return implode('.', array_map(fn ($part) => "`$part`", explode('.', $key))); + } else { + return '"' . $key . '"'; + } + } +} diff --git a/src/SelectRawTrait.php b/src/Helpers/SelectRawTrait.php similarity index 98% rename from src/SelectRawTrait.php rename to src/Helpers/SelectRawTrait.php index e1db259..b02bb89 100644 --- a/src/SelectRawTrait.php +++ b/src/Helpers/SelectRawTrait.php @@ -1,7 +1,8 @@ */ private array $parameters = []; @@ -12,13 +18,14 @@ class MultiInsert * @param string $tableName * @param list> $rows */ - public function __construct(string $tableName, array $rows) { + public function __construct(Connection $connection, string $tableName, array $rows) { if (!$rows) { return; } + $this->connection = $connection; $firstRow = reset($rows); - $columnNames = array_map(fn($columnName) => "`$columnName`", array_keys($firstRow)); - $this->sql = "INSERT INTO `$tableName` (" . implode(', ', $columnNames) . ") VALUES "; + $columnNames = array_map(fn ($columnName) => $this->escapeIdentifier($columnName), array_keys($firstRow)); + $this->sql = "INSERT INTO " . $this->escapeIdentifier($tableName) . " (" . implode(', ', $columnNames) . ") VALUES "; $valuesSql = []; $index = 0; @@ -47,4 +54,9 @@ public function getParameters(): array { return $this->parameters; } + + private function getConnection(): Connection + { + return $this->connection; + } } \ No newline at end of file diff --git a/tests/MultiQuery/MultiInsertTest.php b/tests/MultiQuery/MultiInsertTest.php index e2c1237..1cf09e4 100644 --- a/tests/MultiQuery/MultiInsertTest.php +++ b/tests/MultiQuery/MultiInsertTest.php @@ -2,6 +2,7 @@ namespace Composite\DB\Tests\MultiQuery; +use Composite\DB\ConnectionManager; use Composite\DB\MultiQuery\MultiInsert; class MultiInsertTest extends \PHPUnit\Framework\TestCase @@ -11,7 +12,8 @@ class MultiInsertTest extends \PHPUnit\Framework\TestCase */ public function test_multiInsertQuery($tableName, $rows, $expectedSql, $expectedParameters) { - $multiInserter = new MultiInsert($tableName, $rows); + $connection = ConnectionManager::getConnection('sqlite'); + $multiInserter = new MultiInsert($connection, $tableName, $rows); $this->assertEquals($expectedSql, $multiInserter->getSql()); $this->assertEquals($expectedParameters, $multiInserter->getParameters()); @@ -31,7 +33,7 @@ public static function multiInsertQuery_dataProvider() [ ['a' => 'value1_1', 'b' => 'value2_1'], ], - "INSERT INTO `testTable` (`a`, `b`) VALUES (:a0, :b0);", + 'INSERT INTO "testTable" ("a", "b") VALUES (:a0, :b0);', ['a0' => 'value1_1', 'b0' => 'value2_1'] ], [ @@ -40,7 +42,7 @@ public static function multiInsertQuery_dataProvider() ['a' => 'value1_1', 'b' => 'value2_1'], ['a' => 'value1_2', 'b' => 'value2_2'] ], - "INSERT INTO `testTable` (`a`, `b`) VALUES (:a0, :b0), (:a1, :b1);", + 'INSERT INTO "testTable" ("a", "b") VALUES (:a0, :b0), (:a1, :b1);', ['a0' => 'value1_1', 'b0' => 'value2_1', 'a1' => 'value1_2', 'b1' => 'value2_2'] ], [ @@ -49,7 +51,7 @@ public static function multiInsertQuery_dataProvider() ['column1' => 'value1_1'], ['column1' => 123] ], - "INSERT INTO `testTable` (`column1`) VALUES (:column10), (:column11);", + 'INSERT INTO "testTable" ("column1") VALUES (:column10), (:column11);', ['column10' => 'value1_1', 'column11' => 123] ] ]; diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php index cb48987..f5e77ce 100644 --- a/tests/Table/AbstractTableTest.php +++ b/tests/Table/AbstractTableTest.php @@ -98,7 +98,7 @@ public function test_illegalCreateEntity(): void /** * @dataProvider buildWhere_dataProvider */ - public function test_buildWhere($where, $expectedSQL, $expectedParams) + public function test_buildWhere($where, $expectedSQL, $expectedParams): void { $table = new Tables\TestStrictTable(); @@ -190,4 +190,14 @@ public static function buildWhere_dataProvider(): array ] ]; } + + public function test_databaseSpecific(): void + { + $mySQLTable = new Tables\TestMySQLTable(); + $this->assertEquals('`column`', $mySQLTable->escapeIdentifierPub('column')); + $this->assertEquals('`Database`.`Table`', $mySQLTable->escapeIdentifierPub('Database.Table')); + + $postgresTable = new Tables\TestPostgresTable(); + $this->assertEquals('"column"', $postgresTable->escapeIdentifierPub('column')); + } } \ No newline at end of file diff --git a/tests/TestStand/Tables/TestAutoincrementCachedTable.php b/tests/TestStand/Tables/TestAutoincrementCachedTable.php index fb05d76..1fba645 100644 --- a/tests/TestStand/Tables/TestAutoincrementCachedTable.php +++ b/tests/TestStand/Tables/TestAutoincrementCachedTable.php @@ -48,7 +48,7 @@ public function findOneByName(string $name): ?TestAutoincrementEntity return $this->_findOneCached(['name' => $name]); } - public function delete(TestAutoincrementEntity|AbstractEntity &$entity): void + public function delete(TestAutoincrementEntity|AbstractEntity $entity): void { if ($entity->name === 'Exception') { throw new \Exception('Test Exception'); diff --git a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php index c1134f0..07ba91d 100644 --- a/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php +++ b/tests/TestStand/Tables/TestAutoincrementSdCachedTable.php @@ -48,7 +48,7 @@ public function findOneByName(string $name): ?TestAutoincrementSdEntity return $this->_findOneCached(['name' => $name]); } - public function delete(TestAutoincrementSdEntity|AbstractEntity &$entity): void + public function delete(TestAutoincrementSdEntity|AbstractEntity $entity): void { if ($entity->name === 'Exception') { throw new \Exception('Test Exception'); diff --git a/tests/TestStand/Tables/TestAutoincrementTable.php b/tests/TestStand/Tables/TestAutoincrementTable.php index 7b6ac40..bef47de 100644 --- a/tests/TestStand/Tables/TestAutoincrementTable.php +++ b/tests/TestStand/Tables/TestAutoincrementTable.php @@ -32,7 +32,7 @@ public function findOneByName(string $name): ?TestAutoincrementEntity return $this->_findOne(['name' => $name]); } - public function delete(AbstractEntity|TestAutoincrementEntity &$entity): void + public function delete(AbstractEntity|TestAutoincrementEntity $entity): void { if ($entity->name === 'Exception') { throw new \Exception('Test Exception'); diff --git a/tests/TestStand/Tables/TestCompositeTable.php b/tests/TestStand/Tables/TestCompositeTable.php index 64e90d4..8a1e9da 100644 --- a/tests/TestStand/Tables/TestCompositeTable.php +++ b/tests/TestStand/Tables/TestCompositeTable.php @@ -15,7 +15,7 @@ protected function getConfig(): TableConfig return TableConfig::fromEntitySchema(TestCompositeEntity::schema()); } - public function save(AbstractEntity|TestCompositeEntity &$entity): void + public function save(AbstractEntity|TestCompositeEntity $entity): void { if ($entity->message === 'Exception') { throw new \Exception('Test Exception'); @@ -23,7 +23,7 @@ public function save(AbstractEntity|TestCompositeEntity &$entity): void parent::save($entity); } - public function delete(AbstractEntity|TestCompositeEntity &$entity): void + public function delete(AbstractEntity|TestCompositeEntity $entity): void { if ($entity->message === 'Exception') { throw new \Exception('Test Exception'); diff --git a/tests/TestStand/Tables/TestMySQLTable.php b/tests/TestStand/Tables/TestMySQLTable.php new file mode 100644 index 0000000..e918355 --- /dev/null +++ b/tests/TestStand/Tables/TestMySQLTable.php @@ -0,0 +1,25 @@ +escapeIdentifier($key); + } +} \ No newline at end of file diff --git a/tests/TestStand/Tables/TestPostgresTable.php b/tests/TestStand/Tables/TestPostgresTable.php new file mode 100644 index 0000000..94ab839 --- /dev/null +++ b/tests/TestStand/Tables/TestPostgresTable.php @@ -0,0 +1,25 @@ +escapeIdentifier($key); + } +} \ No newline at end of file diff --git a/tests/TestStand/Tables/TestUniqueTable.php b/tests/TestStand/Tables/TestUniqueTable.php index 8353194..9387104 100644 --- a/tests/TestStand/Tables/TestUniqueTable.php +++ b/tests/TestStand/Tables/TestUniqueTable.php @@ -19,7 +19,7 @@ public function __construct() $this->init(); } - public function save(AbstractEntity|TestUniqueEntity &$entity): void + public function save(AbstractEntity|TestUniqueEntity $entity): void { if ($entity->name === 'Exception') { throw new \Exception('Test Exception'); From 18fba46f0093415c3d6fa4bcefc0909a0f581750 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 23 Dec 2023 19:45:41 +0000 Subject: [PATCH 63/69] Minor fix in ConnectionManager::loadConfigs --- src/ConnectionManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ConnectionManager.php b/src/ConnectionManager.php index aeb463f..f79bcad 100644 --- a/src/ConnectionManager.php +++ b/src/ConnectionManager.php @@ -67,7 +67,7 @@ private static function loadConfigs(): array $configFile )); } - $configFileContent = require_once $configFile; + $configFileContent = require $configFile; if (empty($configFileContent) || !is_array($configFileContent)) { throw new DbException(sprintf( 'Connections config file `%s` should return array of connection params', From 7efbe27d9bfa4cae31abd8f5c67412cf429745d3 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Mon, 25 Dec 2023 10:22:12 +0000 Subject: [PATCH 64/69] Optimize DatabaseSpecificTrait::escapeIdentifier --- src/Helpers/DatabaseSpecificTrait.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Helpers/DatabaseSpecificTrait.php b/src/Helpers/DatabaseSpecificTrait.php index 14a72ff..7769175 100644 --- a/src/Helpers/DatabaseSpecificTrait.php +++ b/src/Helpers/DatabaseSpecificTrait.php @@ -52,7 +52,11 @@ protected function escapeIdentifier(string $key): string { $this->identifyPlatform(); if ($this->isMySQL) { - return implode('.', array_map(fn ($part) => "`$part`", explode('.', $key))); + if (strpos($key, '.')) { + return implode('.', array_map(fn ($part) => "`$part`", explode('.', $key))); + } else { + return "`$key`"; + } } else { return '"' . $key . '"'; } From 5830b1ef542845e1b49aea09c257efb644090694 Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Wed, 27 Dec 2023 12:01:17 +0000 Subject: [PATCH 65/69] Remove ORM mentions from the README --- README.md | 29 ++++++++--------------------- composer.json | 2 +- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0385896..256a728 100644 --- a/README.md +++ b/README.md @@ -3,49 +3,36 @@ [![Build Status](https://github.com/compositephp/db/actions/workflows/main.yml/badge.svg)](https://github.com/compositephp/db/actions) [![Codecov](https://codecov.io/gh/compositephp/db/branch/master/graph/badge.svg)](https://codecov.io/gh/compositephp/db/) -Composite DB is lightweight and fast PHP ORM, DataMapper and Table Gateway which allows you to represent your SQL tables +Composite DB is lightweight and fast PHP DataMapper and Table Gateway which allows you to represent your SQL tables scheme in OOP style using full power of PHP 8.1+ class syntax. It also gives you CRUD, query builder and automatic caching out of the box, so you can start to work with your database from php code in a minutes! Overview: -* [Mission](#mission) +* [Features](#features) * [Requirements](#requirements) * [Installation](#installation) * [Quick example](#quick-example) * [Documentation](doc/README.md) -## Mission -You probably may ask, why do you need another ORM if there are already popular Doctrine, CycleORM, etc.? - -Composite DB solves multiple problems: +## Features * **Lightweight** - easier entity schema, no getters and setters, you don't need attributes for each column definition, just use native php class syntax. -* **Speed** - it's 1.5x faster in pure SQL queries mode and many times faster in automatic caching mode. -* **Easy caching** - gives you CRUD operations caching out of the box and in general its much easier to work with cached "selects". +* **Speed** - it's 1.5x faster in pure SQL queries mode and many times faster in automatic caching mode (see [benchmark](https://github.com/compositephp/php-orm-benchmark)). +* **Easy caching** - gives you CRUD operations caching out of the box and in general it's much easier to work with cached "selects". * **Strict types** - Composite DB forces you to be more strict typed and makes your IDE happy. * **Hydration** - you can serialize your Entities to plain array or json and deserialize them back. -* **Flexibility** - gives you more freedom to extend Repositories, for example its easier to build sharding tables. +* **Flexibility** - gives you more freedom to extend Repositories, for example it's easier to build sharding tables. * **Code generation** - you can generate Entity and Repository classes from your SQL tables. -* **Division of responsibility** - there is no "god" entity manager, every Entity has its own Repository class and its the only entry point to make queries to your table. +* **Division of responsibility** - every Entity has its own Repository class, and it's the only entry point to make queries to your table. It also has many popular features such as: * **Query Builder** - build your queries with constructor, based on [doctrine/dbal](https://github.com/doctrine/dbal) * **Migrations** - synchronise your php entities with database tables -But there is 1 sacrifice for all these features - there is no support for relations in Composite DB. Its too much -uncontrollable magic and hidden bottlenecks with "JOINs" and its not possible to implement automatic caching with -relations. We recommend to have full control and make several cached select queries instead of "JOINs". - -### When you shouldn't use Composite DB - -1. If you have intricate structure with many foreign keys in your database -2. You 100% sure in your indexes and fully trust "JOINs" performance -3. You dont want to do extra cached select queries and want some magic - ## Requirements * PHP 8.1+ @@ -180,7 +167,7 @@ $user = User::fromArray([ ]); ``` -And thats it, no special getters or setters, no "behaviours" or extra code, smart entity casts everything automatically. +And that's it, no special getters or setters, no "behaviours" or extra code, smart entity casts everything automatically. More about Entity and supported auto casting types you can find [here](doc/entity.md). ## License: diff --git a/composer.json b/composer.json index ef84c75..a6c1c43 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "compositephp/db", - "description": "PHP 8.1+ ORM and Table Gateway", + "description": "PHP 8.1+ DataMapper and Table Gateway", "type": "library", "license": "MIT", "minimum-stability": "dev", From 0945254f53f4450905c925e206ad92746c6e978b Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sat, 13 Jul 2024 14:46:06 +0100 Subject: [PATCH 66/69] Add CombinedTransaction::try() --- src/CombinedTransaction.php | 13 +++++++++++++ tests/Table/CombinedTransactionTest.php | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/CombinedTransaction.php b/src/CombinedTransaction.php index bb8a4a5..bb5a590 100644 --- a/src/CombinedTransaction.php +++ b/src/CombinedTransaction.php @@ -54,6 +54,19 @@ public function delete(AbstractTable $table, AbstractEntity &$entity): void }); } + /** + * @throws Exceptions\DbException + */ + public function try(callable $callback): void + { + try { + $callback(); + } catch (\Throwable $e) { + $this->rollback(); + throw new Exceptions\DbException($e->getMessage(), 500, $e); + } + } + public function rollback(): void { foreach ($this->transactions as $connection) { diff --git a/tests/Table/CombinedTransactionTest.php b/tests/Table/CombinedTransactionTest.php index 1e67a2c..78f3ef2 100644 --- a/tests/Table/CombinedTransactionTest.php +++ b/tests/Table/CombinedTransactionTest.php @@ -130,6 +130,20 @@ public function test_failedDelete(): void $this->assertNotNull($compositeTable->findOne($cEntity->user_id, $cEntity->post_id)); } + public function test_try(): void + { + $compositeTable = new Tables\TestCompositeTable(); + $entity = new Entities\TestCompositeEntity(user_id: mt_rand(1, 1000), post_id: mt_rand(1, 1000), message: 'Bar');; + + try { + $transaction = new CombinedTransaction(); + $transaction->save($compositeTable, $entity); + $transaction->try(fn() => throw new \Exception('test')); + $transaction->commit(); + } catch (DbException) {} + $this->assertNull($compositeTable->findOne($entity->user_id, $entity->post_id)); + } + public function test_lockFailed(): void { $cache = new Helpers\FalseCache(); From a869f6be4537951cd9865a522e6b4519016e367e Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Sun, 3 Nov 2024 22:47:44 +0000 Subject: [PATCH 67/69] Fix Docrtine DBAL deprecation notice, initialize Config and set DefaultSchemaManagerFactory --- src/ConnectionManager.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/ConnectionManager.php b/src/ConnectionManager.php index f79bcad..7854ed6 100644 --- a/src/ConnectionManager.php +++ b/src/ConnectionManager.php @@ -8,6 +8,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; class ConnectionManager { @@ -24,6 +25,12 @@ public static function getConnection(string $name, ?Configuration $config = null { if (!isset(self::$connections[$name])) { try { + if (!$config) { + $config = new Configuration(); + } + if (!$config->getSchemaManagerFactory()) { + $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); + } self::$connections[$name] = DriverManager::getConnection( params: self::getConnectionParams($name), config: $config, From 20a1fc502d676e23ab378ad86b59363115a3c39d Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Mon, 23 Dec 2024 22:43:26 +0000 Subject: [PATCH 68/69] Adapt tests to php 8.4 --- phpunit.xml | 4 ++++ tests/Attributes/PrimaryKeyAttributeTest.php | 5 ++--- tests/Connection/ConnectionManagerTest.php | 5 ++--- tests/MultiQuery/MultiInsertTest.php | 5 ++--- tests/Table/AbstractCachedTableTest.php | 23 +++++--------------- tests/Table/AbstractTableTest.php | 9 +++----- tests/Table/AutoIncrementTableTest.php | 3 ++- tests/Table/CombinedTransactionTest.php | 5 ++--- tests/Table/CompositeTableTest.php | 4 +++- tests/Table/UniqueTableTest.php | 3 ++- 10 files changed, 28 insertions(+), 38 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 9b42ad0..1389feb 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -3,6 +3,10 @@ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" bootstrap="tests/bootstrap.php" executionOrder="depends,defects" beStrictAboutOutputDuringTests="true" failOnRisky="true" failOnWarning="true" colors="true" cacheDirectory=".phpunit.cache" requireCoverageMetadata="false" + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnTestsThatTriggerErrors="true" + displayDetailsOnTestsThatTriggerNotices="true" + displayDetailsOnPhpunitDeprecations="true" displayDetailsOnTestsThatTriggerWarnings="true" beStrictAboutCoverageMetadata="true"> diff --git a/tests/Attributes/PrimaryKeyAttributeTest.php b/tests/Attributes/PrimaryKeyAttributeTest.php index 750cd6a..1fd6c72 100644 --- a/tests/Attributes/PrimaryKeyAttributeTest.php +++ b/tests/Attributes/PrimaryKeyAttributeTest.php @@ -5,6 +5,7 @@ use Composite\DB\TableConfig; use Composite\Entity\AbstractEntity; use Composite\DB\Attributes; +use PHPUnit\Framework\Attributes\DataProvider; final class PrimaryKeyAttributeTest extends \PHPUnit\Framework\TestCase { @@ -34,9 +35,7 @@ public function __construct( ]; } - /** - * @dataProvider primaryKey_dataProvider - */ + #[DataProvider('primaryKey_dataProvider')] public function test_primaryKey(AbstractEntity $entity, array $expected): void { $schema = $entity::schema(); diff --git a/tests/Connection/ConnectionManagerTest.php b/tests/Connection/ConnectionManagerTest.php index 7d634ff..d17d625 100644 --- a/tests/Connection/ConnectionManagerTest.php +++ b/tests/Connection/ConnectionManagerTest.php @@ -5,6 +5,7 @@ use Composite\DB\ConnectionManager; use Composite\DB\Exceptions\DbException; use Doctrine\DBAL\Connection; +use PHPUnit\Framework\Attributes\DataProvider; final class ConnectionManagerTest extends \PHPUnit\Framework\TestCase { @@ -42,9 +43,7 @@ public static function invalidConfig_dataProvider(): array ]; } - /** - * @dataProvider invalidConfig_dataProvider - */ + #[DataProvider('invalidConfig_dataProvider')] public function test_invalidConfig(string $configPath): void { $reflection = new \ReflectionClass(ConnectionManager::class); diff --git a/tests/MultiQuery/MultiInsertTest.php b/tests/MultiQuery/MultiInsertTest.php index 1cf09e4..69631a9 100644 --- a/tests/MultiQuery/MultiInsertTest.php +++ b/tests/MultiQuery/MultiInsertTest.php @@ -4,12 +4,11 @@ use Composite\DB\ConnectionManager; use Composite\DB\MultiQuery\MultiInsert; +use PHPUnit\Framework\Attributes\DataProvider; class MultiInsertTest extends \PHPUnit\Framework\TestCase { - /** - * @dataProvider multiInsertQuery_dataProvider - */ + #[DataProvider('multiInsertQuery_dataProvider')] public function test_multiInsertQuery($tableName, $rows, $expectedSql, $expectedParameters) { $connection = ConnectionManager::getConnection('sqlite'); diff --git a/tests/Table/AbstractCachedTableTest.php b/tests/Table/AbstractCachedTableTest.php index 826ede0..d9e706e 100644 --- a/tests/Table/AbstractCachedTableTest.php +++ b/tests/Table/AbstractCachedTableTest.php @@ -3,13 +3,12 @@ namespace Composite\DB\Tests\Table; use Composite\DB\AbstractCachedTable; -use Composite\DB\AbstractTable; -use Composite\DB\Exceptions\DbException; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Tables; use Composite\DB\Where; use Composite\Entity\AbstractEntity; use Composite\DB\Tests\Helpers; +use PHPUnit\Framework\Attributes\DataProvider; use Ramsey\Uuid\Uuid; final class AbstractCachedTableTest extends \PHPUnit\Framework\TestCase @@ -43,9 +42,7 @@ public static function getOneCacheKey_dataProvider(): array ]; } - /** - * @dataProvider getOneCacheKey_dataProvider - */ + #[DataProvider('getOneCacheKey_dataProvider')] public function test_getOneCacheKey(AbstractCachedTable $table, AbstractEntity $object, string $expected): void { $reflectionMethod = new \ReflectionMethod($table, 'getOneCacheKey'); @@ -88,9 +85,7 @@ public static function getCountCacheKey_dataProvider(): array ]; } - /** - * @dataProvider getCountCacheKey_dataProvider - */ + #[DataProvider('getCountCacheKey_dataProvider')] public function test_getCountCacheKey(array|Where $where, string $expected): void { $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()); @@ -159,9 +154,7 @@ public static function getListCacheKey_dataProvider(): array ]; } - /** - * @dataProvider getListCacheKey_dataProvider - */ + #[DataProvider('getListCacheKey_dataProvider')] public function test_getListCacheKey(array|Where $where, array $orderBy, ?int $limit, string $expected): void { $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()); @@ -197,9 +190,7 @@ public static function getCustomCacheKey_dataProvider(): array ]; } - /** - * @dataProvider getCustomCacheKey_dataProvider - */ + #[DataProvider('getCustomCacheKey_dataProvider')] public function test_getCustomCacheKey(array $parts, string $expected): void { $table = new Tables\TestAutoincrementCachedTable(Helpers\CacheHelper::getCache()); @@ -253,9 +244,7 @@ public static function collectCacheKeysByEntity_dataProvider(): array ]; } - /** - * @dataProvider collectCacheKeysByEntity_dataProvider - */ + #[DataProvider('collectCacheKeysByEntity_dataProvider')] public function test_collectCacheKeysByEntity(AbstractEntity $entity, AbstractCachedTable $table, array $expected): void { $reflectionMethod = new \ReflectionMethod($table, 'collectCacheKeysByEntity'); diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php index f5e77ce..c14e58a 100644 --- a/tests/Table/AbstractTableTest.php +++ b/tests/Table/AbstractTableTest.php @@ -7,6 +7,7 @@ use Composite\DB\Tests\TestStand\Tables; use Composite\Entity\AbstractEntity; use Composite\Entity\Exceptions\EntityException; +use PHPUnit\Framework\Attributes\DataProvider; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; @@ -54,9 +55,7 @@ public static function getPkCondition_dataProvider(): array ]; } - /** - * @dataProvider getPkCondition_dataProvider - */ + #[DataProvider('getPkCondition_dataProvider')] public function test_getPkCondition(AbstractTable $table, int|string|array|AbstractEntity|UuidInterface $object, array $expected): void { $reflectionMethod = new \ReflectionMethod($table, 'getPkCondition'); @@ -95,9 +94,7 @@ public function test_illegalCreateEntity(): void $this->assertEmpty($empty); } - /** - * @dataProvider buildWhere_dataProvider - */ + #[DataProvider('buildWhere_dataProvider')] public function test_buildWhere($where, $expectedSQL, $expectedParams): void { $table = new Tables\TestStrictTable(); diff --git a/tests/Table/AutoIncrementTableTest.php b/tests/Table/AutoIncrementTableTest.php index 98690f2..03a7569 100644 --- a/tests/Table/AutoIncrementTableTest.php +++ b/tests/Table/AutoIncrementTableTest.php @@ -9,6 +9,7 @@ use Composite\DB\Tests\TestStand\Tables; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Interfaces\IAutoincrementTable; +use PHPUnit\Framework\Attributes\DataProvider; final class AutoIncrementTableTest extends \PHPUnit\Framework\TestCase { @@ -36,8 +37,8 @@ public static function crud_dataProvider(): array /** * @param class-string $class - * @dataProvider crud_dataProvider */ + #[DataProvider('crud_dataProvider')] public function test_crud(AbstractTable&IAutoincrementTable $table, string $class): void { $table->truncate(); diff --git a/tests/Table/CombinedTransactionTest.php b/tests/Table/CombinedTransactionTest.php index 78f3ef2..f7ba7d9 100644 --- a/tests/Table/CombinedTransactionTest.php +++ b/tests/Table/CombinedTransactionTest.php @@ -7,6 +7,7 @@ use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Tables; use Composite\DB\Tests\Helpers; +use PHPUnit\Framework\Attributes\DataProvider; final class CombinedTransactionTest extends \PHPUnit\Framework\TestCase { @@ -185,9 +186,7 @@ public function test_lock(): void $this->assertNotEmpty($table->findByPk($e2->id)); } - /** - * @dataProvider buildLockKey_dataProvider - */ + #[DataProvider('buildLockKey_dataProvider')] public function test_buildLockKey($keyParts, $expectedResult) { $reflection = new \ReflectionClass(CombinedTransaction::class); diff --git a/tests/Table/CompositeTableTest.php b/tests/Table/CompositeTableTest.php index 224e604..09f784b 100644 --- a/tests/Table/CompositeTableTest.php +++ b/tests/Table/CompositeTableTest.php @@ -9,6 +9,7 @@ use Composite\DB\Tests\TestStand\Tables; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Interfaces\ICompositeTable; +use PHPUnit\Framework\Attributes\DataProvider; final class CompositeTableTest extends \PHPUnit\Framework\TestCase { @@ -28,8 +29,9 @@ public static function crud_dataProvider(): array /** * @param class-string $class - * @dataProvider crud_dataProvider + * @throws \Throwable */ + #[DataProvider('crud_dataProvider')] public function test_crud(AbstractTable&ICompositeTable $table, string $class): void { $table->truncate(); diff --git a/tests/Table/UniqueTableTest.php b/tests/Table/UniqueTableTest.php index 186ff97..abeb9ff 100644 --- a/tests/Table/UniqueTableTest.php +++ b/tests/Table/UniqueTableTest.php @@ -7,6 +7,7 @@ use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Tables; use Composite\DB\Tests\TestStand\Interfaces\IUniqueTable; +use PHPUnit\Framework\Attributes\DataProvider; use Ramsey\Uuid\Uuid; final class UniqueTableTest extends \PHPUnit\Framework\TestCase @@ -27,8 +28,8 @@ public static function crud_dataProvider(): array /** * @param class-string $class - * @dataProvider crud_dataProvider */ + #[DataProvider('crud_dataProvider')] public function test_crud(AbstractTable&IUniqueTable $table, string $class): void { $table->truncate(); From 59c52ab0d8ddc3e9917be8184bdaedbd50d3275e Mon Sep 17 00:00:00 2001 From: Composite PHP Date: Tue, 3 Jun 2025 22:23:16 +0100 Subject: [PATCH 69/69] - Add DoctrineTypes usage - Bump doctrine/dbal version - Prevent empty transaction in CombinedTransaction - Add ability to define Doctrine Configuration in config file --- composer.json | 2 +- src/AbstractTable.php | 49 ++++++++++++++++++++++--- src/CombinedTransaction.php | 31 ++++++++++------ src/ConnectionManager.php | 30 ++++----------- src/Helpers/DatabaseSpecificTrait.php | 3 +- tests/Table/AbstractTableTest.php | 24 +++++++++++- tests/Table/CombinedTransactionTest.php | 40 +++++++++++++++++++- tests/Traits/OptimisticLockTest.php | 4 +- tests/config.php | 3 ++ 9 files changed, 140 insertions(+), 46 deletions(-) diff --git a/composer.json b/composer.json index a6c1c43..1adebb3 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "ext-pdo": "*", "psr/simple-cache": "1 - 3", "compositephp/entity": "^v0.1.11", - "doctrine/dbal": "^3.5" + "doctrine/dbal": "^4.2" }, "require-dev": { "kodus/file-cache": "^2.0", diff --git a/src/AbstractTable.php b/src/AbstractTable.php index e144cd3..b2536a7 100644 --- a/src/AbstractTable.php +++ b/src/AbstractTable.php @@ -6,8 +6,10 @@ use Composite\DB\MultiQuery\MultiInsert; use Composite\DB\MultiQuery\MultiSelect; use Composite\Entity\AbstractEntity; +use Composite\Entity\Columns; use Composite\Entity\Helpers\DateTimeHelper; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\ParameterType; use Ramsey\Uuid\UuidInterface; abstract class AbstractTable @@ -52,8 +54,13 @@ public function save(AbstractEntity $entity): void $connection = $this->getConnection(); $this->checkUpdatedAt($entity); - $insertData = $this->prepareDataForSql($entity->toArray()); - $this->getConnection()->insert($this->getTableName(), $insertData); + $insertData = $entity->toArray(); + $preparedInsertData = $this->prepareDataForSql($insertData); + $this->getConnection()->insert( + table: $this->getTableName(), + data: $preparedInsertData, + types: $this->getDoctrineTypes($insertData), + ); if ($this->config->autoIncrementKey && ($lastInsertedId = $connection->lastInsertId())) { $insertData[$this->config->autoIncrementKey] = intval($lastInsertedId); @@ -66,7 +73,6 @@ public function save(AbstractEntity $entity): void if (!$changedColumns = $entity->getChangedColumns()) { return; } - $changedColumns = $this->prepareDataForSql($changedColumns); if ($this->config->hasUpdatedAt() && property_exists($entity, 'updated_at')) { $entity->updated_at = new \DateTimeImmutable(); $changedColumns['updated_at'] = DateTimeHelper::dateTimeToString($entity->updated_at); @@ -81,10 +87,19 @@ public function save(AbstractEntity $entity): void } $updateString = implode(', ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($changedColumns))); $whereString = implode(' AND ', array_map(fn ($key) => $this->escapeIdentifier($key) . "=?", array_keys($whereParams))); + $preparedParams = array_merge( + array_values($this->prepareDataForSql($changedColumns)), + array_values($this->prepareDataForSql($whereParams)), + ); + $types = array_merge( + $this->getDoctrineTypes($changedColumns), + $this->getDoctrineTypes($whereParams), + ); $entityUpdated = (bool)$this->getConnection()->executeStatement( sql: "UPDATE " . $this->escapeIdentifier($this->getTableName()) . " SET $updateString WHERE $whereString;", - params: array_merge(array_values($changedColumns), array_values($whereParams)), + params: $preparedParams, + types: $types, ); if ($this->config->hasOptimisticLock() && !$entityUpdated) { throw new Exceptions\LockException('Failed to update entity version, concurrency modification, rolling back.'); @@ -93,6 +108,23 @@ public function save(AbstractEntity $entity): void } } + private function getDoctrineTypes(array $data): array + { + $result = []; + foreach ($data as $value) { + if (is_bool($value)) { + $result[] = ParameterType::BOOLEAN; + } elseif (is_int($value)) { + $result[] = ParameterType::INTEGER; + } elseif (is_null($value)) { + $result[] = ParameterType::NULL; + } else { + $result[] = ParameterType::STRING; + } + } + return $result; + } + /** * @param AbstractEntity[] $entities * @throws \Throwable @@ -124,7 +156,11 @@ public function saveMany(array $entities): void rows: $chunk, ); if ($multiInsert->getSql()) { - $connection->executeStatement($multiInsert->getSql(), $multiInsert->getParameters()); + $connection->executeStatement( + sql: $multiInsert->getSql(), + params: $multiInsert->getParameters(), + types: $this->getDoctrineTypes(array_keys($chunk[0])), + ); } } } @@ -317,6 +353,9 @@ final protected function createEntities(mixed $data, ?string $keyColumnName = nu */ protected function getPkCondition(int|string|array|AbstractEntity|UuidInterface $data): array { + if (empty($this->config->primaryKeys)) { + throw new \Exception("Primary keys are not defined in `" . $this::class . "` table config"); + } $condition = []; if ($data instanceof AbstractEntity) { if ($data->isNew()) { diff --git a/src/CombinedTransaction.php b/src/CombinedTransaction.php index bb5a590..22b39d4 100644 --- a/src/CombinedTransaction.php +++ b/src/CombinedTransaction.php @@ -17,11 +17,12 @@ class CombinedTransaction /** * @throws Exceptions\DbException */ - public function save(AbstractTable $table, AbstractEntity &$entity): void + public function save(AbstractTable $table, AbstractEntity $entity): void { - $this->doInTransaction($table, function () use ($table, &$entity) { - $table->save($entity); - }); + if (!$entity->isNew() && !$entity->getChangedColumns()) { + return; + } + $this->doInTransaction($table, fn() => $table->save($entity)); } /** @@ -31,6 +32,9 @@ public function save(AbstractTable $table, AbstractEntity &$entity): void */ public function saveMany(AbstractTable $table, array $entities): void { + if (!$entities) { + return; + } $this->doInTransaction($table, fn () => $table->saveMany($entities)); } @@ -41,17 +45,18 @@ public function saveMany(AbstractTable $table, array $entities): void */ public function deleteMany(AbstractTable $table, array $entities): void { + if (!$entities) { + return; + } $this->doInTransaction($table, fn () => $table->deleteMany($entities)); } /** * @throws Exceptions\DbException */ - public function delete(AbstractTable $table, AbstractEntity &$entity): void + public function delete(AbstractTable $table, AbstractEntity $entity): void { - $this->doInTransaction($table, function () use ($table, &$entity) { - $table->delete($entity); - }); + $this->doInTransaction($table, fn () => $table->delete($entity)); } /** @@ -82,9 +87,7 @@ public function commit(): void { foreach ($this->transactions as $connectionName => $connection) { try { - if (!$connection->commit()) { - throw new Exceptions\DbException("Could not commit transaction for database `$connectionName`"); - } + $connection->commit(); // I have no idea how to simulate failed commit // @codeCoverageIgnoreStart } catch (\Throwable $e) { @@ -119,7 +122,11 @@ public function releaseLock(): void if (!$this->cache || !$this->lockKey) { return; } - $this->cache->delete($this->lockKey); + if (!$this->cache->delete($this->lockKey)) { + // @codeCoverageIgnoreStart + throw new DbException("Failed to release lock `{$this->lockKey}`"); + // @codeCoverageIgnoreEnd + } } private function doInTransaction(AbstractTable $table, callable $callback): void diff --git a/src/ConnectionManager.php b/src/ConnectionManager.php index 7854ed6..13694ef 100644 --- a/src/ConnectionManager.php +++ b/src/ConnectionManager.php @@ -3,12 +3,10 @@ namespace Composite\DB; use Composite\DB\Exceptions\DbException; -use Doctrine\Common\EventManager; use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Connection; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Exception; -use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; class ConnectionManager { @@ -21,20 +19,20 @@ class ConnectionManager /** * @throws DbException */ - public static function getConnection(string $name, ?Configuration $config = null, ?EventManager $eventManager = null): Connection + public static function getConnection(string $name, ?Configuration $config = null): Connection { + if (self::$configs === null) { + self::$configs = self::loadConfigs(); + } if (!isset(self::$connections[$name])) { try { - if (!$config) { - $config = new Configuration(); - } - if (!$config->getSchemaManagerFactory()) { - $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); + $connectionParams = self::$configs[$name] ?? throw new DbException("Connection config `$name` not found"); + if (!$config && isset($connectionParams['configuration']) && $connectionParams['configuration'] instanceof Configuration) { + $config = $connectionParams['configuration']; } self::$connections[$name] = DriverManager::getConnection( - params: self::getConnectionParams($name), + params: $connectionParams, config: $config, - eventManager: $eventManager, ); } catch (Exception $e) { throw new DbException($e->getMessage(), $e->getCode(), $e); @@ -43,18 +41,6 @@ public static function getConnection(string $name, ?Configuration $config = null return self::$connections[$name]; } - /** - * @return array - * @throws DbException - */ - private static function getConnectionParams(string $name): array - { - if (self::$configs === null) { - self::$configs = self::loadConfigs(); - } - return self::$configs[$name] ?? throw new DbException("Connection config `$name` not found"); - } - /** * @return array> * @throws DbException diff --git a/src/Helpers/DatabaseSpecificTrait.php b/src/Helpers/DatabaseSpecificTrait.php index 7769175..fe0c6eb 100644 --- a/src/Helpers/DatabaseSpecificTrait.php +++ b/src/Helpers/DatabaseSpecificTrait.php @@ -39,9 +39,8 @@ private function identifyPlatform(): void */ private function prepareDataForSql(array $data): array { - $this->identifyPlatform(); foreach ($data as $columnName => $value) { - if (is_bool($value) && !$this->isPostgreSQL) { + if (is_bool($value)) { $data[$columnName] = $value ? 1 : 0; } } diff --git a/tests/Table/AbstractTableTest.php b/tests/Table/AbstractTableTest.php index c14e58a..6aaa0a0 100644 --- a/tests/Table/AbstractTableTest.php +++ b/tests/Table/AbstractTableTest.php @@ -3,6 +3,7 @@ namespace Composite\DB\Tests\Table; use Composite\DB\AbstractTable; +use Composite\DB\TableConfig; use Composite\DB\Tests\TestStand\Entities; use Composite\DB\Tests\TestStand\Tables; use Composite\Entity\AbstractEntity; @@ -197,4 +198,25 @@ public function test_databaseSpecific(): void $postgresTable = new Tables\TestPostgresTable(); $this->assertEquals('"column"', $postgresTable->escapeIdentifierPub('column')); } -} \ No newline at end of file + + public function test_getPkCondition_throwsExceptionWhenNoPrimaryKeys(): void + { + $mockTable = new class extends AbstractTable { + protected function getConfig(): TableConfig + { + return new TableConfig( + connectionName: 'default', + tableName: 'test_table', + entityClass: AbstractEntity::class, + primaryKeys: [], // Empty primary keys + ); + } + }; + + $this->expectException(\Exception::class); + $this->expectExceptionMessage("Primary keys are not defined in `" . get_class($mockTable) . "` table config"); + + $reflectionMethod = new \ReflectionMethod($mockTable, 'getPkCondition'); + $reflectionMethod->invoke($mockTable, 1); + } +} diff --git a/tests/Table/CombinedTransactionTest.php b/tests/Table/CombinedTransactionTest.php index f7ba7d9..49d9447 100644 --- a/tests/Table/CombinedTransactionTest.php +++ b/tests/Table/CombinedTransactionTest.php @@ -204,4 +204,42 @@ public static function buildLockKey_dataProvider() 'more than max length' => [[str_repeat('a', 55)], sha1('composite.lock.' . str_repeat('a', 55))], ]; } -} \ No newline at end of file + + public function test_saveWithNoChanges(): void + { + $autoIncrementTable = new Tables\TestAutoincrementTable(); + + $entity = new Entities\TestAutoincrementEntity(name: 'TestNoChanges'); + $autoIncrementTable->save($entity); + + $transaction = new CombinedTransaction(); + $transaction->save($autoIncrementTable, $entity); + $transaction->commit(); + + $this->assertNotNull($autoIncrementTable->findByPk($entity->id)); + + $autoIncrementTable->delete($entity); + } + + public function test_saveManyWithEmptyEntities(): void + { + $autoIncrementTable = new Tables\TestAutoincrementTable(); + + $transaction = new CombinedTransaction(); + $transaction->saveMany($autoIncrementTable, []); + $transaction->commit(); + + $this->assertTrue(true); + } + + public function test_deleteManyWithEmptyEntities(): void + { + $autoIncrementTable = new Tables\TestAutoincrementTable(); + + $transaction = new CombinedTransaction(); + $transaction->deleteMany($autoIncrementTable, []); + $transaction->commit(); + + $this->assertTrue(true); + } +} diff --git a/tests/Traits/OptimisticLockTest.php b/tests/Traits/OptimisticLockTest.php index 9856e25..16f95d1 100644 --- a/tests/Traits/OptimisticLockTest.php +++ b/tests/Traits/OptimisticLockTest.php @@ -29,7 +29,7 @@ public function test_trait(): void $aiEntity2->name = 'John2'; $aiTable2->save($aiEntity2); - $this->assertTrue($db->commit()); + $db->commit(); $aiEntity3 = $aiTable1->findByPk($aiEntity1->id); $this->assertEquals('John2', $aiEntity3->name); @@ -57,7 +57,7 @@ public function test_trait(): void } $this->assertTrue($exceptionCaught); - $this->assertTrue($db->rollBack()); + $db->rollBack(); $olEntity3 = $olTable1->findByPk($olEntity1->id); $this->assertEquals(1, $olEntity3->getVersion()); diff --git a/tests/config.php b/tests/config.php index 0e156e6..74cd265 100644 --- a/tests/config.php +++ b/tests/config.php @@ -1,5 +1,7 @@ [ 'driver' => 'pdo_sqlite', @@ -11,6 +13,7 @@ 'user' => 'test', 'password' => 'test', 'host' => '127.0.0.1', + 'configuration' => new Configuration(), ], 'postgres' => [ 'driver' => 'pdo_pgsql',