From 37268814d8d07342b91a557b278c0db14a4c4624 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Fri, 15 Nov 2019 16:01:35 +0100 Subject: [PATCH 1/2] Whitelist: first implementation (still some issues to fix) --- src/AbstractTDBMObject.php | 36 ++--- src/AlterableResultIterator.php | 7 +- src/DbRow.php | 20 +-- src/InnerResultIterator.php | 11 +- src/OrderByAnalyzer.php | 5 +- src/PageIterator.php | 16 +- src/QueryFactory/AbstractQueryFactory.php | 42 ++++-- .../FindObjectsFromRawSqlQueryFactory.php | 15 +- src/QueryFactory/QueryFactory.php | 3 + src/ResultIterator.php | 32 +++- src/TDBMObjectStateEnum.php | 15 +- src/TDBMService.php | 85 ++++++----- src/Utils/BeanDescriptor.php | 142 ++++++++++++++---- .../DirectForeignKeyMethodDescriptor.php | 33 ++-- .../ManyToManyRelationshipPathDescriptor.php | 15 +- src/Utils/ObjectBeanPropertyDescriptor.php | 36 +++-- src/Utils/PivotTableMethodsDescriptor.php | 29 +++- tests/AbstractTDBMObjectTest.php | 4 +- tests/Dao/TestUserDao.php | 33 ++-- tests/TDBMAbstractServiceTest.php | 2 +- tests/TDBMServiceTest.php | 109 ++++++++------ .../DirectForeignKeyMethodDescriptorTest.php | 2 +- .../Utils/PivotTableMethodsDescriptorTest.php | 2 +- 23 files changed, 455 insertions(+), 239 deletions(-) diff --git a/src/AbstractTDBMObject.php b/src/AbstractTDBMObject.php index 10df8c56..623b6508 100644 --- a/src/AbstractTDBMObject.php +++ b/src/AbstractTDBMObject.php @@ -99,7 +99,7 @@ public function __construct(?string $tableName = null, array $primaryKeys = [], $this->_setStatus(TDBMObjectStateEnum::STATE_DETACHED); } else { $this->_attach($tdbmService); - if (!empty($primaryKeys)) { + if (!empty($primaryKeys)) { // @TODO (gua) might not be fully loaded $this->_setStatus(TDBMObjectStateEnum::STATE_NOT_LOADED); } else { $this->_setStatus(TDBMObjectStateEnum::STATE_NEW); @@ -110,7 +110,7 @@ public function __construct(?string $tableName = null, array $primaryKeys = [], /** * Alternative constructor called when data is fetched from database via a SELECT. * - * @param array[] $beanData array> + * @param array> $beanData array> * @param TDBMService $tdbmService */ public function _constructFromData(array $beanData, TDBMService $tdbmService): void @@ -121,7 +121,7 @@ public function _constructFromData(array $beanData, TDBMService $tdbmService): v $this->dbRows[$table] = new DbRow($this, $table, static::getForeignKeys($table), $tdbmService->_getPrimaryKeysFromObjectData($table, $columns), $tdbmService, $columns); } - $this->status = TDBMObjectStateEnum::STATE_LOADED; + $this->status = TDBMObjectStateEnum::STATE_LOADED; // @TODO might be not fully loaded } /** @@ -251,27 +251,14 @@ protected function set(string $var, $value, ?string $tableName = null): void } } - /** - * @param string $foreignKeyName - * @param AbstractTDBMObject $bean - */ - protected function setRef(string $foreignKeyName, AbstractTDBMObject $bean = null, string $tableName = null): void + protected function setRef(string $foreignKeyName, ?AbstractTDBMObject $bean, string $tableName, string $className, string $resultIteratorClass): void { - if ($tableName === null) { - if (count($this->dbRows) > 1) { - throw new TDBMException('This object is based on several tables. You must specify which table you are retrieving data from.'); - } elseif (count($this->dbRows) === 1) { - $tableName = (string) array_keys($this->dbRows)[0]; - } else { - throw new TDBMException('Please specify a table for this object.'); - } - } - + assert($bean === null || is_a($bean, $className), new TDBMInvalidArgumentException('$bean should be `null` or `' . $className . '`. `' . ($bean === null ? 'null' : get_class($bean)) . '` provided.')); if (!isset($this->dbRows[$tableName])) { $this->registerTable($tableName); } - $oldLinkedBean = $this->dbRows[$tableName]->getRef($foreignKeyName); + $oldLinkedBean = $this->dbRows[$tableName]->getRef($foreignKeyName, $className, $resultIteratorClass); if ($oldLinkedBean !== null) { $oldLinkedBean->removeManyToOneRelationship($tableName, $foreignKeyName, $this); } @@ -291,7 +278,7 @@ protected function setRef(string $foreignKeyName, AbstractTDBMObject $bean = nul * * @return AbstractTDBMObject|null */ - protected function getRef(string $foreignKeyName, ?string $tableName = null) : ?AbstractTDBMObject + protected function getRef(string $foreignKeyName, string $tableName, string $className, string $resultIteratorClass) : ?AbstractTDBMObject { $tableName = $this->checkTableName($tableName); @@ -299,11 +286,11 @@ protected function getRef(string $foreignKeyName, ?string $tableName = null) : ? return null; } - return $this->dbRows[$tableName]->getRef($foreignKeyName); + return $this->dbRows[$tableName]->getRef($foreignKeyName, $className, $resultIteratorClass); } /** - * Adds a many to many relationship to this bean. + * Adds a many to many$table relationship to this bean. * * @param string $pivotTableName * @param AbstractTDBMObject $remoteBean @@ -525,15 +512,16 @@ private function removeManyToOneRelationship(string $tableName, string $foreignK * * @return AlterableResultIterator */ - protected function retrieveManyToOneRelationshipsStorage(string $tableName, string $foreignKeyName, array $searchFilter, string $orderString = null) : AlterableResultIterator + protected function retrieveManyToOneRelationshipsStorage(string $tableName, string $foreignKeyName, array $searchFilter, ?string $orderString, string $resultIteratorClass) : AlterableResultIterator { + assert(is_a($resultIteratorClass, ResultIterator::class, true), new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.')); $key = $tableName.'___'.$foreignKeyName; $alterableResultIterator = $this->getManyToOneAlterableResultIterator($tableName, $foreignKeyName); if ($this->status === TDBMObjectStateEnum::STATE_DETACHED || $this->status === TDBMObjectStateEnum::STATE_NEW || (isset($this->manyToOneRelationships[$key]) && $this->manyToOneRelationships[$key]->getUnderlyingResultIterator() !== null)) { return $alterableResultIterator; } - $unalteredResultIterator = $this->tdbmService->findObjects($tableName, $searchFilter, [], $orderString); + $unalteredResultIterator = $this->tdbmService->findObjects($tableName, $searchFilter, [], $orderString, [], null, null, $resultIteratorClass); $alterableResultIterator->setResultIterator($unalteredResultIterator->getIterator()); diff --git a/src/AlterableResultIterator.php b/src/AlterableResultIterator.php index f02ffb4a..3ed53efc 100644 --- a/src/AlterableResultIterator.php +++ b/src/AlterableResultIterator.php @@ -75,11 +75,8 @@ public function add($object): void { $this->alterations->attach($object, 'add'); - if ($this->resultArray !== null) { - $foundKey = array_search($object, $this->resultArray, true); - if ($foundKey === false) { - $this->resultArray[] = $object; - } + if ($this->resultArray !== null && !in_array($object, $this->resultArray, true)) { + $this->resultArray[] = $object; } } diff --git a/src/DbRow.php b/src/DbRow.php index 84d2780e..e8ebe342 100644 --- a/src/DbRow.php +++ b/src/DbRow.php @@ -134,6 +134,7 @@ public function __construct(AbstractTDBMObject $object, string $tableName, Forei $this->_setPrimaryKeys($primaryKeys); if (!empty($dbRow)) { $this->dbRow = $dbRow; + // @TODO (gua): might not be fully loaded $this->status = TDBMObjectStateEnum::STATE_LOADED; } else { $this->status = TDBMObjectStateEnum::STATE_NOT_LOADED; @@ -219,6 +220,9 @@ public function _dbLoadIfNotLoaded(): void */ public function get(string $var) { + if ($this->_getStatus() === TDBMObjectStateEnum::STATE_PARTIALLY_LOADED && !array_key_exists($var, $this->dbRow)) { + throw new TDBMInvalidArgumentException('Cannot load `'.$var.'` in partially loaded object with data ' . print_r($this->dbRow, true)); + } if (!isset($this->primaryKeys[$var])) { $this->_dbLoadIfNotLoaded(); } @@ -235,16 +239,6 @@ public function set(string $var, $value): void { $this->_dbLoadIfNotLoaded(); - /* - // Ok, let's start by checking the column type - $type = $this->db_connection->getColumnType($this->dbTableName, $var); - - // Throws an exception if the type is not ok. - if (!$this->db_connection->checkType($value, $type)) { - throw new TDBMException("Error! Invalid value passed for attribute '$var' of table '$this->dbTableName'. Passed '$value', but expecting '$type'"); - } - */ - /*if ($var == $this->getPrimaryKey() && isset($this->dbRow[$var])) throw new TDBMException("Error! Changing primary key value is forbidden.");*/ $this->dbRow[$var] = $value; @@ -275,7 +269,7 @@ public function setRef(string $foreignKeyName, AbstractTDBMObject $bean = null): * * @return AbstractTDBMObject|null */ - public function getRef(string $foreignKeyName) : ?AbstractTDBMObject + public function getRef(string $foreignKeyName, string $className, string $resultIteratorClass) : ?AbstractTDBMObject { if (array_key_exists($foreignKeyName, $this->references)) { return $this->references[$foreignKeyName]; @@ -303,9 +297,9 @@ public function getRef(string $foreignKeyName) : ?AbstractTDBMObject // If the foreign key points to the primary key, let's use findObjectByPk if ($this->tdbmService->getPrimaryKeyColumns($foreignTableName) === $foreignColumns) { - return $this->tdbmService->findObjectByPk($foreignTableName, $filter, [], true); + return $this->tdbmService->findObjectByPk($foreignTableName, $filter, [], true, $className, $resultIteratorClass); } else { - return $this->tdbmService->findObject($foreignTableName, $filter); + return $this->tdbmService->findObject($foreignTableName, $filter, [], [], $className, $resultIteratorClass); } } } diff --git a/src/InnerResultIterator.php b/src/InnerResultIterator.php index 86e75006..b6cc626a 100644 --- a/src/InnerResultIterator.php +++ b/src/InnerResultIterator.php @@ -42,6 +42,7 @@ class InnerResultIterator implements \Iterator, InnerResultIteratorInterface private $objectStorage; private $className; + /** @var TDBMService */ private $tdbmService; private $magicSql; private $parameters; @@ -78,7 +79,7 @@ private function __construct() */ public static function createInnerResultIterator(string $magicSql, array $parameters, ?int $limit, ?int $offset, array $columnDescriptors, ObjectStorageInterface $objectStorage, ?string $className, TDBMService $tdbmService, MagicQuery $magicQuery, LoggerInterface $logger): self { - $iterator = new static(); + $iterator = new static(); // @TODO (gua) Should I know here if it's a partial load ? (to allow to give that state to DBRow and TDBMObject) $iterator->magicSql = $magicSql; $iterator->objectStorage = $objectStorage; $iterator->className = $className; @@ -170,10 +171,12 @@ public function key() */ public function next() { + /** @var array $row */ $row = $this->statement->fetch(\PDO::FETCH_ASSOC); if ($row) { // array>> + /** @var array>> $beansData */ $beansData = []; foreach ($row as $key => $value) { if (!isset($this->columnDescriptors[$key])) { @@ -201,13 +204,15 @@ public function next() list($actualClassName, $mainBeanTableName, $tablesUsed) = $this->tdbmService->_getClassNameFromBeanData($beanData); - if ($this->className !== null) { + // @TODO (gua) this is a weird hack to be able to force a TDBMObject... + // ClassName could be used to override $actualClassName + if ($this->className !== null && is_a($this->className, TDBMObject::class, true)) { $actualClassName = $this->className; } // Let's filter out the beanData that is not used (because it belongs to a part of the hierarchy that is not fetched: foreach ($beanData as $tableName => $descriptors) { - if (!in_array($tableName, $tablesUsed)) { + if (!in_array($tableName, $tablesUsed, true)) { unset($beanData[$tableName]); } } diff --git a/src/OrderByAnalyzer.php b/src/OrderByAnalyzer.php index 6055baec..c104a507 100644 --- a/src/OrderByAnalyzer.php +++ b/src/OrderByAnalyzer.php @@ -5,6 +5,7 @@ use Doctrine\Common\Cache\Cache; use PHPSQLParser\PHPSQLParser; +use PHPSQLParser\utils\ExpressionType; /** * Class in charge of analyzing order by clauses. @@ -85,7 +86,7 @@ private function analyzeOrderByNoCache(string $orderBy) : array for ($i = 0, $count = count($parsed['ORDER']); $i < $count; ++$i) { $orderItem = $parsed['ORDER'][$i]; - if ($orderItem['expr_type'] === 'colref') { + if ($orderItem['expr_type'] === ExpressionType::COLREF) { $parts = $orderItem['no_quotes']['parts']; $columnName = array_pop($parts); if (!empty($parts)) { @@ -95,7 +96,7 @@ private function analyzeOrderByNoCache(string $orderBy) : array } $results[] = [ - 'type' => 'colref', + 'type' => ExpressionType::COLREF, 'table' => $tableName, 'column' => $columnName, 'direction' => $orderItem['direction'], diff --git a/src/PageIterator.php b/src/PageIterator.php index f25c516e..a59a3554 100644 --- a/src/PageIterator.php +++ b/src/PageIterator.php @@ -76,8 +76,20 @@ private function __construct() * @param mixed[] $parameters * @param array[] $columnDescriptors */ - public static function createResultIterator(ResultIterator $parentResult, string $magicSql, array $parameters, int $limit, int $offset, array $columnDescriptors, ObjectStorageInterface $objectStorage, ?string $className, TDBMService $tdbmService, MagicQuery $magicQuery, int $mode, LoggerInterface $logger): self - { + public static function createResultIterator( + ResultIterator $parentResult, + string $magicSql, + array $parameters, + int $limit, + int $offset, + array $columnDescriptors, + ObjectStorageInterface $objectStorage, + ?string $className, + TDBMService $tdbmService, + MagicQuery $magicQuery, + int $mode, + LoggerInterface $logger + ): self { $iterator = new self(); $iterator->parentResult = $parentResult; $iterator->magicSql = $magicSql; diff --git a/src/QueryFactory/AbstractQueryFactory.php b/src/QueryFactory/AbstractQueryFactory.php index d78da3fc..d07cbf69 100644 --- a/src/QueryFactory/AbstractQueryFactory.php +++ b/src/QueryFactory/AbstractQueryFactory.php @@ -3,6 +3,8 @@ namespace TheCodingMachine\TDBM\QueryFactory; +use PHPSQLParser\utils\ExpressionType; +use TheCodingMachine\TDBM\ResultIterator; use function array_unique; use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Schema\Schema; @@ -52,6 +54,8 @@ abstract class AbstractQueryFactory implements QueryFactory * @var string */ protected $mainTable; + /** @var null|ResultIterator */ + protected $resultIterator; /** * @param TDBMService $tdbmService @@ -68,6 +72,11 @@ public function __construct(TDBMService $tdbmService, Schema $schema, OrderByAna $this->mainTable = $mainTable; } + public function setResultIterator(ResultIterator $resultIterator): void + { + $this->resultIterator = $resultIterator; + } + /** * Returns the column list that must be fetched for the SQL request. * @@ -120,7 +129,7 @@ protected function getColumnsList(string $mainTable, array $additionalTablesFetc // If we sort by a column, there is a high chance we will fetch the bean containing this column. // Hence, we should add the table to the $additionalTablesFetch foreach ($orderByColumns as $orderByColumn) { - if ($orderByColumn['type'] === 'colref') { + if ($orderByColumn['type'] === ExpressionType::COLREF) { if ($orderByColumn['table'] !== null) { if ($canAddAdditionalTablesFetch) { $additionalTablesFetch[] = $orderByColumn['table']; @@ -140,16 +149,16 @@ protected function getColumnsList(string $mainTable, array $additionalTablesFetc $reconstructedOrderBys[] = ($orderByColumn['table'] !== null ? $mysqlPlatform->quoteIdentifier($orderByColumn['table']).'.' : '').$mysqlPlatform->quoteIdentifier($orderByColumn['column']).' '.$orderByColumn['direction']; } } elseif ($orderByColumn['type'] === 'expr') { + if ($securedOrderBy) { + throw new TDBMInvalidArgumentException('Invalid ORDER BY column: "'.$orderByColumn['expr'].'". If you want to use expression in your ORDER BY clause, you must wrap them in a UncheckedOrderBy object. For instance: new UncheckedOrderBy("col1 + col2 DESC")'); + } + $sortColumnName = 'sort_column_'.$sortColumn; $columnsList[] = $orderByColumn['expr'].' as '.$sortColumnName; $columnDescList[$sortColumnName] = [ 'tableGroup' => null, ]; ++$sortColumn; - - if ($securedOrderBy) { - throw new TDBMInvalidArgumentException('Invalid ORDER BY column: "'.$orderByColumn['expr'].'". If you want to use expression in your ORDER BY clause, you must wrap them in a UncheckedOrderBy object. For instance: new UncheckedOrderBy("col1 + col2 DESC")'); - } } } @@ -181,15 +190,20 @@ protected function getColumnsList(string $mainTable, array $additionalTablesFetc foreach ($allFetchedTables as $table) { foreach ($this->schema->getTable($table)->getColumns() as $column) { $columnName = $column->getName(); - $columnDescList[$table.'____'.$columnName] = [ - 'as' => $table.'____'.$columnName, - 'table' => $table, - 'column' => $columnName, - 'type' => $column->getType(), - 'tableGroup' => $tableGroups[$table], - ]; - $columnsList[] = $mysqlPlatform->quoteIdentifier($table).'.'.$mysqlPlatform->quoteIdentifier($columnName).' as '. - $connection->quoteIdentifier($table.'____'.$columnName); + if ($this->resultIterator === null // @TODO (gua) don't take care of whitelist in case of LIMIT below 2 + || $table !== $mainTable + || $this->resultIterator->isInWhitelist($columnName, $table) + ) { + $columnDescList[$table . '____' . $columnName] = [ + 'as' => $table . '____' . $columnName, + 'table' => $table, + 'column' => $columnName, + 'type' => $column->getType(), + 'tableGroup' => $tableGroups[$table], + ]; + $columnsList[] = $mysqlPlatform->quoteIdentifier($table) . '.' . $mysqlPlatform->quoteIdentifier($columnName) . ' as ' . + $connection->quoteIdentifier($table . '____' . $columnName); + } } } diff --git a/src/QueryFactory/FindObjectsFromRawSqlQueryFactory.php b/src/QueryFactory/FindObjectsFromRawSqlQueryFactory.php index 71c0c508..f74ced2b 100644 --- a/src/QueryFactory/FindObjectsFromRawSqlQueryFactory.php +++ b/src/QueryFactory/FindObjectsFromRawSqlQueryFactory.php @@ -5,6 +5,8 @@ use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Schema\Schema; +use PHPSQLParser\utils\ExpressionType; +use TheCodingMachine\TDBM\ResultIterator; use TheCodingMachine\TDBM\TDBMException; use TheCodingMachine\TDBM\TDBMService; use PHPSQLParser\PHPSQLCreator; @@ -77,6 +79,11 @@ public function getColumnDescriptors(): array return $this->columnDescriptors; } + public function setResultIterator(ResultIterator $resultIterator): void + { + // We do not need to know the result iterator here + } + /** * @param string $sql * @param null|string $sqlCount @@ -177,7 +184,7 @@ private function formatSelect(array $baseSelect): array $fetchedTables = []; foreach ($baseSelect as $entry) { - if ($entry['expr_type'] !== 'colref') { + if ($entry['expr_type'] !== ExpressionType::COLREF) { $formattedSelect[] = $entry; continue; } @@ -205,7 +212,7 @@ private function formatSelect(array $baseSelect): array $columnName = $column->getName(); $alias = "{$tableName}____{$columnName}"; $formattedSelect[] = [ - 'expr_type' => 'colref', + 'expr_type' => ExpressionType::COLREF, 'base_expr' => $connection->quoteIdentifier($tableName).'.'.$connection->quoteIdentifier($columnName), 'no_quotes' => [ 'delim' => '.', @@ -292,7 +299,7 @@ private function generateSimpleSqlCount(array $parsedSql): array } else { $countSubExpr = [ [ - 'expr_type' => 'colref', + 'expr_type' => ExpressionType::COLREF, 'base_expr' => '*', 'sub_tree' => false ] @@ -368,7 +375,7 @@ private function generateWrappedSqlCount(array $parsedSql): array 'base_expr' => 'COUNT', 'sub_tree' => [ [ - 'expr_type' => 'colref', + 'expr_type' => ExpressionType::COLREF, 'base_expr' => '*', 'sub_tree' => false ] diff --git a/src/QueryFactory/QueryFactory.php b/src/QueryFactory/QueryFactory.php index dcd2f5bf..6bf76211 100644 --- a/src/QueryFactory/QueryFactory.php +++ b/src/QueryFactory/QueryFactory.php @@ -3,6 +3,7 @@ namespace TheCodingMachine\TDBM\QueryFactory; +use TheCodingMachine\TDBM\ResultIterator; use TheCodingMachine\TDBM\UncheckedOrderBy; /** @@ -47,4 +48,6 @@ public function getColumnDescriptors() : array; * @return string[][] An array of column descriptors. Value is an array with those keys: table, column */ public function getSubQueryColumnDescriptors() : array; + + public function setResultIterator(ResultIterator $resultIterator) : void; } diff --git a/src/ResultIterator.php b/src/ResultIterator.php index df7368c4..7419cda4 100644 --- a/src/ResultIterator.php +++ b/src/ResultIterator.php @@ -82,13 +82,24 @@ private function __construct() /** * @param mixed[] $parameters */ - public static function createResultIterator(QueryFactory $queryFactory, array $parameters, ObjectStorageInterface $objectStorage, ?string $className, TDBMService $tdbmService, MagicQuery $magicQuery, int $mode, LoggerInterface $logger): self - { + public static function createResultIterator( + QueryFactory $queryFactory, + array $parameters, + ObjectStorageInterface $objectStorage, + ?string $className, + TDBMService $tdbmService, + MagicQuery $magicQuery, + int $mode, + LoggerInterface $logger + ): self { $iterator = new static(); if ($mode !== TDBMService::MODE_CURSOR && $mode !== TDBMService::MODE_ARRAY) { throw new TDBMException("Unknown fetch mode: '".$mode."'"); } + if (is_subclass_of($iterator, self::class, false)) { // We only add iterator if it's specified + $queryFactory->setResultIterator($iterator); + } $iterator->queryFactory = $queryFactory; $iterator->objectStorage = $objectStorage; $iterator->className = $className; @@ -100,7 +111,7 @@ public static function createResultIterator(QueryFactory $queryFactory, array $p return $iterator; } - public static function createEmpyIterator(): self + public static function createEmptyIterator(): self { $iterator = new self(); $iterator->totalCount = 0; @@ -383,4 +394,19 @@ public function _getSubQuery(): string return $sql; } + + protected function addToWhitelist(string $column, string $table) : void + { + throw new TDBMException('Table `' . $table . '` not found in inheritance Schema.'); + } + + public function isInWhitelist(string $column, string $table) : bool + { + throw new TDBMException('Table `' . $table . '` not found in inheritance Schema'); + } + + protected function removeFromWhitelist(string $column, string $table) : void + { + throw new TDBMException('Table `' . $table . '` not found in inheritance Schema'); + } } diff --git a/src/TDBMObjectStateEnum.php b/src/TDBMObjectStateEnum.php index c13fd664..6008db07 100644 --- a/src/TDBMObjectStateEnum.php +++ b/src/TDBMObjectStateEnum.php @@ -28,11 +28,12 @@ */ final class TDBMObjectStateEnum { - const STATE_DETACHED = 'detached'; - const STATE_NEW = 'new'; - const STATE_SAVING = 'saving'; - const STATE_NOT_LOADED = 'not loaded'; - const STATE_LOADED = 'loaded'; - const STATE_DIRTY = 'dirty'; - const STATE_DELETED = 'deleted'; + public const STATE_DETACHED = 'detached'; + public const STATE_NEW = 'new'; + public const STATE_SAVING = 'saving'; + public const STATE_NOT_LOADED = 'not loaded'; + public const STATE_LOADED = 'loaded'; + public const STATE_DIRTY = 'dirty'; + public const STATE_DELETED = 'deleted'; + public const STATE_PARTIALLY_LOADED = 'partially_loaded'; } diff --git a/src/TDBMService.php b/src/TDBMService.php index dbf387ab..df1c894e 100644 --- a/src/TDBMService.php +++ b/src/TDBMService.php @@ -160,18 +160,13 @@ class TDBMService */ private $orderByAnalyzer; - /** - * @var string - */ + /** @var string */ private $beanNamespace; - - /** - * @var NamingStrategyInterface - */ + /** @var string */ + private $resultIteratorNamespace; + /** @var NamingStrategyInterface */ private $namingStrategy; - /** - * @var ConfigurationInterface - */ + /** @var ConfigurationInterface */ private $configuration; /** @@ -205,6 +200,7 @@ public function __construct(ConfigurationInterface $configuration) } $this->orderByAnalyzer = new OrderByAnalyzer($this->cache, $this->cachePrefix); $this->beanNamespace = $configuration->getBeanNamespace(); + $this->resultIteratorNamespace = $configuration->getResultIteratorNamespace(); $this->namingStrategy = $configuration->getNamingStrategy(); $this->configuration = $configuration; } @@ -356,7 +352,17 @@ private function deleteAllConstraintWithThisObject(AbstractTDBMObject $obj): voi foreach ($incomingFks as $incomingFk) { $filter = SafeFunctions::arrayCombine($incomingFk->getUnquotedLocalColumns(), $pks); - $results = $this->findObjects($incomingFk->getLocalTableName(), $filter); + $localTableName = $incomingFk->getLocalTableName(); + $results = $this->findObjects( + $localTableName, + $filter, + [], + null, + [], + null, + $this->beanNamespace . '\\' . $this->namingStrategy->getBeanClassName($localTableName), + $this->resultIteratorNamespace . '\\' . $this->namingStrategy->getResultIteratorClassName($localTableName) + ); foreach ($results as $bean) { $this->deleteCascade($bean); @@ -1126,7 +1132,7 @@ private function exploreChildrenTablesRelationships(SchemaAnalyzer $schemaAnalyz * * @throws TDBMException */ - public function findObjects(string $mainTable, $filter = null, array $parameters = array(), $orderString = null, array $additionalTablesFetch = array(), ?int $mode = null, string $className = null, string $resultIteratorClass = ResultIterator::class): ResultIterator + public function findObjects(string $mainTable, $filter, array $parameters, $orderString, array $additionalTablesFetch, ?int $mode, ?string $className, string $resultIteratorClass): ResultIterator { if (!is_a($resultIteratorClass, ResultIterator::class, true)) { throw new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.'); @@ -1163,7 +1169,7 @@ public function findObjects(string $mainTable, $filter = null, array $parameters * * @throws TDBMException */ - public function findObjectsFromSql(string $mainTable, string $from, $filter = null, array $parameters = array(), $orderString = null, ?int $mode = null, string $className = null, string $resultIteratorClass = ResultIterator::class): ResultIterator + public function findObjectsFromSql(string $mainTable, string $from, $filter, array $parameters, $orderString, ?int $mode, ?string $className, string $resultIteratorClass): ResultIterator { if (!is_a($resultIteratorClass, ResultIterator::class, true)) { throw new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.'); @@ -1192,20 +1198,23 @@ public function findObjectsFromSql(string $mainTable, string $from, $filter = nu * @param string[] $additionalTablesFetch * @param bool $lazy Whether to perform lazy loading on this object or not * @param string $className + * @param string $resultIteratorClass * * @return AbstractTDBMObject * * @throws TDBMException */ - public function findObjectByPk(string $table, array $primaryKeys, array $additionalTablesFetch = array(), bool $lazy = false, string $className = null): AbstractTDBMObject + public function findObjectByPk(string $table, array $primaryKeys, array $additionalTablesFetch, bool $lazy, string $className, string $resultIteratorClass): AbstractTDBMObject { + assert(is_a($resultIteratorClass, ResultIterator::class, true), new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.')); + assert(is_a($className, AbstractTDBMObject::class, true), new TDBMInvalidArgumentException('$className should be a `'. AbstractTDBMObject::class. '`. `' . $className . '` provided.')); $primaryKeys = $this->_getPrimaryKeysFromObjectData($table, $primaryKeys); $hash = $this->getObjectHash($primaryKeys); $dbRow = $this->objectStorage->get($table, $hash); if ($dbRow !== null) { $bean = $dbRow->getTDBMObject(); - if ($className !== null && !is_a($bean, $className)) { + if (!is_a($bean, $className)) { throw new TDBMException("TDBM cannot create a bean of class '".$className."'. The requested object was already loaded and its class is '".get_class($bean)."'"); } @@ -1218,20 +1227,12 @@ public function findObjectByPk(string $table, array $primaryKeys, array $additio $tables = $this->_getRelatedTablesByInheritance($table); // Only allowed if no inheritance. if (count($tables) === 1) { - if ($className === null) { - try { - $className = $this->getBeanClassName($table); - } catch (TDBMInvalidArgumentException $e) { - $className = TDBMObject::class; - } - } - // Let's construct the bean if (!isset($this->reflectionClassCache[$className])) { $this->reflectionClassCache[$className] = new \ReflectionClass($className); } // Let's bypass the constructor when creating the bean! - /** @var AbstractTDBMObject */ + /** @var AbstractTDBMObject $bean */ $bean = $this->reflectionClassCache[$className]->newInstanceWithoutConstructor(); $bean->_constructLazy($table, $primaryKeys, $this); @@ -1241,7 +1242,7 @@ public function findObjectByPk(string $table, array $primaryKeys, array $additio // Did not find the object in cache? Let's query it! try { - return $this->findObjectOrFail($table, $primaryKeys, [], $additionalTablesFetch, $className); + return $this->findObjectOrFail($table, $primaryKeys, [], $additionalTablesFetch, $className, $resultIteratorClass); } catch (NoBeanFoundException $exception) { $primaryKeysStringified = implode(' and ', array_map(function ($key, $value) { return "'".$key."' = ".$value; @@ -1257,15 +1258,16 @@ public function findObjectByPk(string $table, array $primaryKeys, array $additio * @param string|array|null $filter The SQL filters to apply to the query (the WHERE part). All columns must be prefixed by the table name (in the form: table.column) * @param mixed[] $parameters * @param string[] $additionalTablesFetch - * @param string $className Optional: The name of the class to instantiate. This class must extend the TDBMObject class. If none is specified, a TDBMObject instance will be returned + * @param string $className The name of the class to instantiate. This class must extend the TDBMObject class. If none is specified, a TDBMObject instance will be returned * * @return AbstractTDBMObject|null The object we want, or null if no object matches the filters * * @throws TDBMException */ - public function findObject(string $mainTable, $filter = null, array $parameters = array(), array $additionalTablesFetch = array(), string $className = null) : ?AbstractTDBMObject + public function findObject(string $mainTable, $filter, array $parameters, array $additionalTablesFetch, string $className, string $resultIteratorClass) : ?AbstractTDBMObject { - $objects = $this->findObjects($mainTable, $filter, $parameters, null, $additionalTablesFetch, self::MODE_ARRAY, $className); + assert(is_a($resultIteratorClass, ResultIterator::class, true), new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.')); + $objects = $this->findObjects($mainTable, $filter, $parameters, null, $additionalTablesFetch, self::MODE_ARRAY, $className, $resultIteratorClass); return $this->getAtMostOneObjectOrFail($objects, $mainTable, $filter, $parameters); } @@ -1318,9 +1320,10 @@ private function getAtMostOneObjectOrFail(ResultIterator $objects, string $mainT * * @throws TDBMException */ - public function findObjectFromSql(string $mainTable, string $from, $filter = null, array $parameters = array(), ?string $className = null) : ?AbstractTDBMObject + public function findObjectFromSql(string $mainTable, string $from, $filter, array $parameters, ?string $className, string $resultIteratorClass) : ?AbstractTDBMObject { - $objects = $this->findObjectsFromSql($mainTable, $from, $filter, $parameters, null, self::MODE_ARRAY, $className); + assert(is_a($resultIteratorClass, ResultIterator::class, true), new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.')); + $objects = $this->findObjectsFromSql($mainTable, $from, $filter, $parameters, null, self::MODE_ARRAY, $className, $resultIteratorClass); return $this->getAtMostOneObjectOrFail($objects, $mainTable, $filter, $parameters); } @@ -1337,7 +1340,7 @@ public function findObjectFromSql(string $mainTable, string $from, $filter = nul * * @throws TDBMException */ - public function findObjectsFromRawSql(string $mainTable, string $sql, array $parameters = array(), ?int $mode = null, string $className = null, string $sqlCount = null, string $resultIteratorClass = ResultIterator::class): ResultIterator + public function findObjectsFromRawSql(string $mainTable, string $sql, array $parameters, ?int $mode, ?string $className, ?string $sqlCount, string $resultIteratorClass): ResultIterator { if (!is_a($resultIteratorClass, ResultIterator::class, true)) { throw new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.'); @@ -1368,9 +1371,10 @@ public function findObjectsFromRawSql(string $mainTable, string $sql, array $par * * @throws TDBMException */ - public function findObjectOrFail(string $mainTable, $filter = null, array $parameters = array(), array $additionalTablesFetch = array(), string $className = null): AbstractTDBMObject + public function findObjectOrFail(string $mainTable, $filter = null, array $parameters = array(), array $additionalTablesFetch = array(), string $className = null, string $resultIteratorClass = ResultIterator::class): AbstractTDBMObject { - $bean = $this->findObject($mainTable, $filter, $parameters, $additionalTablesFetch, $className); + assert(is_a($resultIteratorClass, ResultIterator::class, true), new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.')); + $bean = $this->findObject($mainTable, $filter, $parameters, $additionalTablesFetch, $className, $resultIteratorClass); if ($bean === null) { throw new NoBeanFoundException("No result found for query on table '".$mainTable."'"); } @@ -1453,7 +1457,16 @@ private function fromCache(string $key, callable $closure) */ public function _getRelatedBeans(ManyToManyRelationshipPathDescriptor $pathDescriptor, AbstractTDBMObject $bean): ResultIterator { - return $this->findObjectsFromSql($pathDescriptor->getTargetName(), $pathDescriptor->getPivotFrom(), $pathDescriptor->getPivotWhere(), $pathDescriptor->getPivotParams($this->getPrimaryKeyValues($bean))); + return $this->findObjectsFromSql( + $pathDescriptor->getTargetName(), + $pathDescriptor->getPivotFrom(), + $pathDescriptor->getPivotWhere(), + $pathDescriptor->getPivotParams($this->getPrimaryKeyValues($bean)), + null, + null, + null, + $pathDescriptor->getResultIteratorClass() + ); } /** @@ -1488,7 +1501,7 @@ private function getPivotTableForeignKeys(string $pivotTableName, AbstractTDBMOb * Key: table name * Value: array of types indexed by column. * - * @var array[] + * @var array> */ private $typesForTable = []; @@ -1497,7 +1510,7 @@ private function getPivotTableForeignKeys(string $pivotTableName, AbstractTDBMOb * * @param string $tableName * - * @return Type[] + * @return array */ public function _getColumnTypesForTable(string $tableName): array { diff --git a/src/Utils/BeanDescriptor.php b/src/Utils/BeanDescriptor.php index abbd1f79..0a1c859b 100644 --- a/src/Utils/BeanDescriptor.php +++ b/src/Utils/BeanDescriptor.php @@ -3,6 +3,7 @@ namespace TheCodingMachine\TDBM\Utils; +use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Schema\Index; use Doctrine\DBAL\Schema\Schema; @@ -42,6 +43,7 @@ use Zend\Code\Generator\MethodGenerator; use Zend\Code\Generator\ParameterGenerator; use Zend\Code\Generator\PropertyGenerator; +use Zend\Code\Generator\PropertyValueGenerator; use function implode; use function var_export; @@ -299,7 +301,7 @@ private function getPropertiesForTable(Table $table): array $beanPropertyDescriptors = []; foreach ($table->getColumns() as $column) { - if (array_search($column->getName(), $ignoreColumns) !== false) { + if (in_array($column->getName(), $ignoreColumns, true)) { continue; } @@ -317,7 +319,7 @@ private function getPropertiesForTable(Table $table): array continue; } - $beanPropertyDescriptors[] = new ObjectBeanPropertyDescriptor($table, $fk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser, $this->registry->getBeanForTableName($fk->getForeignTableName())); + $beanPropertyDescriptors[] = new ObjectBeanPropertyDescriptor($table, $fk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser, $this->registry->getBeanForTableName($fk->getForeignTableName()), $this->resultIteratorNamespace); } else { $beanPropertyDescriptors[] = new ScalarBeanPropertyDescriptor($table, $column, $this->namingStrategy, $this->annotationParser); } @@ -411,7 +413,7 @@ private function getDirectForeignKeysDescriptors(): array $descriptors = []; foreach ($fks as $fk) { - $desc = new DirectForeignKeyMethodDescriptor($fk, $this->table, $this->namingStrategy, $this->annotationParser, $this->beanNamespace); + $desc = new DirectForeignKeyMethodDescriptor($fk, $this->table, $this->namingStrategy, $this->annotationParser, $this->beanNamespace, $this->resultIteratorNamespace); $this->checkForDuplicate($desc); $descriptors[] = $desc; } @@ -435,13 +437,13 @@ private function getPivotTableDescriptors(): array if ($fks[0]->getForeignTableName() === $this->table->getName()) { list($localFk, $remoteFk) = $fks; - $desc = new PivotTableMethodsDescriptor($table, $localFk, $remoteFk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser); + $desc = new PivotTableMethodsDescriptor($table, $localFk, $remoteFk, $this->namingStrategy, $this->annotationParser, $this->beanNamespace, $this->resultIteratorNamespace); $this->checkForDuplicate($desc); $descs[] = $desc; } if ($fks[1]->getForeignTableName() === $this->table->getName()) { list($remoteFk, $localFk) = $fks; - $desc = new PivotTableMethodsDescriptor($table, $localFk, $remoteFk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser); + $desc = new PivotTableMethodsDescriptor($table, $localFk, $remoteFk, $this->namingStrategy, $this->annotationParser, $this->beanNamespace, $this->resultIteratorNamespace); $this->checkForDuplicate($desc); $descs[] = $desc; } @@ -742,6 +744,8 @@ public function generateDaoPhpCode(): ?FileGenerator $baseClassName = $this->namingStrategy->getBaseDaoClassName($tableName); $beanClassWithoutNameSpace = $this->namingStrategy->getBeanClassName($tableName); $beanClassName = $this->beanNamespace.'\\'.$beanClassWithoutNameSpace; + $resultIteratorClassWithoutNameSpace = $this->getResultIteratorClassName(); + $resultIteratorClass = $this->resultIteratorNamespace.'\\'.$resultIteratorClassWithoutNameSpace; $findByDaoCodeMethods = $this->generateFindByDaoCode($this->beanNamespace, $beanClassWithoutNameSpace, $class); @@ -832,7 +836,7 @@ public function generateDaoPhpCode(): ?FileGenerator } else { \$orderBy = null; } -return \$this->tdbmService->findObjects('$tableName', null, [], \$orderBy, [], null, null, \\$this->resultIteratorNamespace\\{$this->getResultIteratorClassName()}::class); +return \$this->tdbmService->findObjects('$tableName', null, [], \$orderBy, [], null, \\$beanClassName::class, \\$resultIteratorClass::class); EOF; $findAllMethod = new MethodGenerator( @@ -842,7 +846,7 @@ public function generateDaoPhpCode(): ?FileGenerator $findAllBody, (new DocBlockGenerator("Get all $beanClassWithoutNameSpace records."))->setWordWrap(false) ); - $findAllMethod->setReturnType($this->resultIteratorNamespace . '\\' . $this->getResultIteratorClassName()); + $findAllMethod->setReturnType($resultIteratorClass); $findAllMethod = $this->codeGeneratorListener->onBaseDaoFindAllGenerated($findAllMethod, $this, $this->configuration, $class); if ($findAllMethod !== null) { $class->addMethodFromGenerator($findAllMethod); @@ -872,7 +876,7 @@ public function generateDaoPhpCode(): ?FileGenerator 'getById', $parameters, MethodGenerator::FLAG_PUBLIC, - "return \$this->tdbmService->findObjectByPk('$tableName', [" . implode(', ', $primaryKeyFilter) . "], [], \$$lazyLoadingParameterName);", + "return \$this->tdbmService->findObjectByPk('$tableName', [" . implode(', ', $primaryKeyFilter) . "], [], \$$lazyLoadingParameterName, \\$beanClassName::class, \\$resultIteratorClass::class);", (new DocBlockGenerator( "Get $beanClassWithoutNameSpace specified by its ID (its primary key).", 'If the primary key does not exist, an exception is thrown.', @@ -922,7 +926,7 @@ public function generateDaoPhpCode(): ?FileGenerator if (\$this->defaultSort && \$orderBy == null) { \$orderBy = '$tableName.'.\$this->defaultSort.' '.\$this->defaultDirection; } -return \$this->tdbmService->findObjects('$tableName', \$filter, \$parameters, \$orderBy, \$additionalTablesFetch, \$mode, null, \\$this->resultIteratorNamespace\\{$this->getResultIteratorClassName()}::class); +return \$this->tdbmService->findObjects('$tableName', \$filter, \$parameters, \$orderBy, \$additionalTablesFetch, \$mode, \\$beanClassName::class, \\$resultIteratorClass::class); EOF; @@ -949,7 +953,7 @@ public function generateDaoPhpCode(): ?FileGenerator ] ))->setWordWrap(false) ); - $findMethod->setReturnType($this->resultIteratorNamespace . '\\' . $this->getResultIteratorClassName()); + $findMethod->setReturnType($resultIteratorClass); $findMethod = $this->codeGeneratorListener->onBaseDaoFindGenerated($findMethod, $this, $this->configuration, $class); if ($findMethod !== null) { $class->addMethodFromGenerator($findMethod); @@ -959,7 +963,7 @@ public function generateDaoPhpCode(): ?FileGenerator if (\$this->defaultSort && \$orderBy == null) { \$orderBy = '$tableName.'.\$this->defaultSort.' '.\$this->defaultDirection; } -return \$this->tdbmService->findObjectsFromSql('$tableName', \$from, \$filter, \$parameters, \$orderBy, \$mode, null, \\$this->resultIteratorNamespace\\{$this->getResultIteratorClassName()}::class); +return \$this->tdbmService->findObjectsFromSql('$tableName', \$from, \$filter, \$parameters, \$orderBy, \$mode, \\$beanClassName::class, \\$resultIteratorClass::class); EOF; $findFromSqlMethod = new MethodGenerator( @@ -991,14 +995,14 @@ public function generateDaoPhpCode(): ?FileGenerator ] ))->setWordWrap(false) ); - $findFromSqlMethod->setReturnType($this->resultIteratorNamespace . '\\' . $this->getResultIteratorClassName()); + $findFromSqlMethod->setReturnType($resultIteratorClass); $findFromSqlMethod = $this->codeGeneratorListener->onBaseDaoFindFromSqlGenerated($findFromSqlMethod, $this, $this->configuration, $class); if ($findFromSqlMethod !== null) { $class->addMethodFromGenerator($findFromSqlMethod); } $findFromRawSqlMethodBody = <<tdbmService->findObjectsFromRawSql('$tableName', \$sql, \$parameters, \$mode, null, \$countSql, \\$this->resultIteratorNamespace\\{$this->getResultIteratorClassName()}::class); +return \$this->tdbmService->findObjectsFromRawSql('$tableName', \$sql, \$parameters, \$mode, \\$beanClassName::class, \$countSql, \\$resultIteratorClass::class); EOF; $findFromRawSqlMethod = new MethodGenerator( @@ -1026,14 +1030,14 @@ public function generateDaoPhpCode(): ?FileGenerator ] ))->setWordWrap(false) ); - $findFromRawSqlMethod->setReturnType($this->resultIteratorNamespace . '\\' . $this->getResultIteratorClassName()); + $findFromRawSqlMethod->setReturnType($resultIteratorClass); $findFromRawSqlMethod = $this->codeGeneratorListener->onBaseDaoFindFromRawSqlGenerated($findFromRawSqlMethod, $this, $this->configuration, $class); if ($findFromRawSqlMethod !== null) { $class->addMethodFromGenerator($findFromRawSqlMethod); } $findOneMethodBody = <<tdbmService->findObject('$tableName', \$filter, \$parameters, \$additionalTablesFetch); +return \$this->tdbmService->findObject('$tableName', \$filter, \$parameters, \$additionalTablesFetch, \\$beanClassName::class, \\$resultIteratorClass::class); EOF; @@ -1064,7 +1068,7 @@ public function generateDaoPhpCode(): ?FileGenerator } $findOneFromSqlMethodBody = <<tdbmService->findObjectFromSql('$tableName', \$from, \$filter, \$parameters); +return \$this->tdbmService->findObjectFromSql('$tableName', \$from, \$filter, \$parameters, \\$beanClassName::class, \\$resultIteratorClass::class); EOF; $findOneFromSqlMethod = new MethodGenerator( @@ -1141,7 +1145,7 @@ public function generateResultIteratorPhpCode(): ?FileGenerator $tableName = $this->table->getName(); - $className = $this->namingStrategy->getResultIteratorClassName($tableName); + $classNameWithoutNamespace = $this->namingStrategy->getResultIteratorClassName($tableName); $baseClassName = $this->namingStrategy->getBaseResultIteratorClassName($tableName); $beanClassWithoutNameSpace = $this->namingStrategy->getBeanClassName($tableName); $beanClassName = $this->beanNamespace.'\\'.$beanClassWithoutNameSpace; @@ -1150,12 +1154,18 @@ public function generateResultIteratorPhpCode(): ?FileGenerator <<addUse(ResultIterator::class); $class->setName($baseClassName); - $class->setExtendedClass(ResultIterator::class); + $extends = $this->getExtendedResultIteratorClassName(); + if ($extends === null) { + $class->addUse(ResultIterator::class); + $class->setExtendedClass(ResultIterator::class); + } else { + $class->addUse($this->resultIteratorNamespace . '\\' . $extends); + $class->setExtendedClass($extends); + } $class->setDocBlock((new DocBlockGenerator( "The $baseClassName class will iterate over results of $beanClassWithoutNameSpace class.", @@ -1163,6 +1173,62 @@ public function generateResultIteratorPhpCode(): ?FileGenerator [new Tag\MethodTag('getIterator', ['\\' . $beanClassName . '[]'])] ))->setWordWrap(false)); + $columnsWithWhitelist = []; + foreach ($this->table->getColumns() as $column) { + $columnsWithWhitelist[$column->getName()] = $column->getType()->getBindingType() !== ParameterType::LARGE_OBJECT; // Should "not null" values be included too ? + } + $whitelistProperty = new PropertyGenerator('whitelist'); + $whitelistProperty->setVisibility(AbstractMemberGenerator::VISIBILITY_PRIVATE); + $whitelistProperty->setDocBlock(new DocBlockGenerator( + 'Columns to fetch from db', + null, + [new VarTag(null,'array', 'Associative array indexed by columns')] + )); + $whitelistProperty->setDefaultValue(new PropertyValueGenerator($columnsWithWhitelist)); + $class->addPropertyFromGenerator($whitelistProperty); + + $whitelistAddMethod = new MethodGenerator( + 'addToWhitelist', + [new ParameterGenerator('column', 'string'), new ParameterGenerator('table', 'string', $tableName)], + MethodGenerator::FLAG_PROTECTED, + <<whitelist[\$column] = true; +} else { + parent::addToWhitelist(\$column, \$table); +} +PHP + ); + $whitelistAddMethod->setReturnType('void'); + $whitelistRemoveMethod = new MethodGenerator( + 'removeFromWhitelist', + [new ParameterGenerator('column', 'string'), new ParameterGenerator('table', 'string', $tableName)], + MethodGenerator::FLAG_PROTECTED, + <<whitelist[\$column] = false; +} else { + parent::removeFromWhitelist(\$column, \$table); +} +PHP + ); + $whitelistRemoveMethod->setReturnType('void'); + $whitelistHasMethod = new MethodGenerator( + 'isInWhitelist', + [new ParameterGenerator('column', 'string'), new ParameterGenerator('table', 'string', $tableName)], + MethodGenerator::FLAG_PUBLIC, + <<whitelist[\$column]; +} +return parent::isInWhitelist(\$column, \$table); +PHP + ); + $whitelistHasMethod->setReturnType('bool'); + $class->addMethodFromGenerator($whitelistAddMethod); + $class->addMethodFromGenerator($whitelistRemoveMethod); + $class->addMethodFromGenerator($whitelistHasMethod); + $file = $this->codeGeneratorListener->onBaseResultIteratorGenerated($file, $this, $this->configuration); return $file; @@ -1265,7 +1331,7 @@ private function generateFindByDaoCodeForIndex(Index $index, string $beanNamespa $fk = $this->isPartOfForeignKey($this->table, $this->table->getColumn($column)); if ($fk !== null) { if (!isset($elements[$fk->getName()])) { - $elements[$fk->getName()] = new ObjectBeanPropertyDescriptor($this->table, $fk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser, $this->registry->getBeanForTableName($fk->getForeignTableName())); + $elements[$fk->getName()] = new ObjectBeanPropertyDescriptor($this->table, $fk, $this->namingStrategy, $this->beanNamespace, $this->annotationParser, $this->registry->getBeanForTableName($fk->getForeignTableName()), $this->resultIteratorNamespace); } } else { $elements[] = new ScalarBeanPropertyDescriptor($this->table, $this->table->getColumn($column), $this->namingStrategy, $this->annotationParser); @@ -1419,24 +1485,31 @@ private function generateGetUsedTablesCode(): MethodGenerator private function generateOnDeleteCode(): ?MethodGenerator { - $code = ''; + $setRefsToNullCode = ['parent::onDelete();']; $relationships = $this->getPropertiesForTable($this->table); foreach ($relationships as $relationship) { if ($relationship instanceof ObjectBeanPropertyDescriptor) { $tdbmFk = ForeignKey::createFromFk($relationship->getForeignKey()); - $code .= '$this->setRef('.var_export($tdbmFk->getCacheKey(), true).', null, '.var_export($this->table->getName(), true).");\n"; + $foreignTableName = $tdbmFk->getForeignTableName(); + $setRefsToNullCode[] = sprintf( + '$this->setRef(%s, %s, %s, %s, %s);', + var_export($tdbmFk->getCacheKey(), true), + 'null', + var_export($this->table->getName(), true), + '\\' . $this->beanNamespace . '\\' . $this->namingStrategy->getBeanClassName($foreignTableName) . '::class', + '\\' . $this->resultIteratorNamespace . '\\' . $this->namingStrategy->getResultIteratorClassName($foreignTableName) . '::class' + ); } } - if (!$code) { + if (count($setRefsToNullCode) === 1) { return null; } $method = new MethodGenerator('onDelete'); $method->setDocBlock('Method called when the bean is removed from database.'); $method->setReturnType('void'); - $method->setBody('parent::onDelete(); -'.$code); + $method->setBody(implode("\n", $setRefsToNullCode)); return $method; } @@ -1600,17 +1673,26 @@ public function getTable(): Table /** * Returns the extended bean class name (without the namespace), or null if the bean is not extended. - * - * @return string */ public function getExtendedBeanClassName(): ?string { $parentFk = $this->schemaAnalyzer->getParentRelationship($this->table->getName()); if ($parentFk !== null) { return $this->namingStrategy->getBeanClassName($parentFk->getForeignTableName()); - } else { - return null; } + return null; + } + + /** + * Returns the extended result iterator class name (without the namespace), or null if the result iterator is not extended. + */ + public function getExtendedResultIteratorClassName(): ?string + { + $parentFk = $this->schemaAnalyzer->getParentRelationship($this->table->getName()); + if ($parentFk !== null) { + return $this->namingStrategy->getResultIteratorClassName($parentFk->getForeignTableName()); + } + return null; } /** diff --git a/src/Utils/DirectForeignKeyMethodDescriptor.php b/src/Utils/DirectForeignKeyMethodDescriptor.php index 97c88778..961981ae 100644 --- a/src/Utils/DirectForeignKeyMethodDescriptor.php +++ b/src/Utils/DirectForeignKeyMethodDescriptor.php @@ -40,26 +40,25 @@ class DirectForeignKeyMethodDescriptor implements RelationshipMethodDescriptorIn * @var string */ private $beanNamespace; - /** - * @param ForeignKeyConstraint $fk The foreign key pointing to our bean - * @param Table $mainTable The main table that is pointed to - * @param NamingStrategyInterface $namingStrategy - * @param AnnotationParser $annotationParser - * @param string $beanNamespace + * @var string */ + private $resultIteratorNamespace; + public function __construct( ForeignKeyConstraint $fk, Table $mainTable, NamingStrategyInterface $namingStrategy, AnnotationParser $annotationParser, - string $beanNamespace + string $beanNamespace, + string $resultIteratorNamespace ) { $this->foreignKey = $fk; $this->mainTable = $mainTable; $this->namingStrategy = $namingStrategy; $this->annotationParser = $annotationParser; $this->beanNamespace = $beanNamespace; + $this->resultIteratorNamespace = $resultIteratorNamespace; } /** @@ -106,6 +105,16 @@ public function getBeanClassName(): string return $this->namingStrategy->getBeanClassName($this->foreignKey->getLocalTableName()); } + /** + * Returns the name of the class that will be returned by the getter (short name). + * + * @return string + */ + public function getResultIteratorClassName(): string + { + return $this->namingStrategy->getResultIteratorClassName($this->foreignKey->getLocalTableName()); + } + /** * Requests the use of an alternative name for this method. */ @@ -133,10 +142,11 @@ public function getCode() : array $getter->setReturnType('?' . $classType); $code = sprintf( - 'return $this->retrieveManyToOneRelationshipsStorage(%s, %s, %s)->first();', + 'return $this->retrieveManyToOneRelationshipsStorage(%s, %s, %s, null, %s)->first();', var_export($this->foreignKey->getLocalTableName(), true), var_export($tdbmFk->getCacheKey(), true), - $this->getFilters($this->foreignKey) + $this->getFilters($this->foreignKey), + '\\' . $this->resultIteratorNamespace . '\\' . $this->getResultIteratorClassName() .'::class' ); } else { $getter->setDocBlock(sprintf('Returns the list of %s pointing to this bean via the %s column.', $beanClass, implode(', ', $this->foreignKey->getUnquotedLocalColumns()))); @@ -147,10 +157,11 @@ public function getCode() : array $getter->setReturnType(AlterableResultIterator::class); $code = sprintf( - 'return $this->retrieveManyToOneRelationshipsStorage(%s, %s, %s);', + 'return $this->retrieveManyToOneRelationshipsStorage(%s, %s, %s, null, %s);', var_export($this->foreignKey->getLocalTableName(), true), var_export($tdbmFk->getCacheKey(), true), - $this->getFilters($this->foreignKey) + $this->getFilters($this->foreignKey), + '\\' . $this->resultIteratorNamespace . '\\' . $this->getResultIteratorClassName() .'::class' ); } diff --git a/src/Utils/ManyToManyRelationshipPathDescriptor.php b/src/Utils/ManyToManyRelationshipPathDescriptor.php index 224cf9b6..86f9dc89 100644 --- a/src/Utils/ManyToManyRelationshipPathDescriptor.php +++ b/src/Utils/ManyToManyRelationshipPathDescriptor.php @@ -3,6 +3,8 @@ namespace TheCodingMachine\TDBM\Utils; use Doctrine\DBAL\Schema\ForeignKeyConstraint; +use TheCodingMachine\TDBM\ResultIterator; +use TheCodingMachine\TDBM\TDBMInvalidArgumentException; use function var_export; class ManyToManyRelationshipPathDescriptor @@ -28,6 +30,10 @@ class ManyToManyRelationshipPathDescriptor * @var array */ private $whereKeys; + /** + * @var string + */ + private $resultIteratorClass; /** * ManyToManyRelationshipPathDescriptor constructor. @@ -37,13 +43,15 @@ class ManyToManyRelationshipPathDescriptor * @param string[] $joinLocalKeys * @param string[] $whereKeys */ - public function __construct(string $targetTable, string $pivotTable, array $joinForeignKeys, array $joinLocalKeys, array $whereKeys) + public function __construct(string $targetTable, string $pivotTable, array $joinForeignKeys, array $joinLocalKeys, array $whereKeys, string $resultIteratorClass) { + assert(is_a($resultIteratorClass, ResultIterator::class, true), new TDBMInvalidArgumentException('$resultIteratorClass should be a `'. ResultIterator::class. '`. `' . $resultIteratorClass . '` provided.')); $this->targetTable = $targetTable; $this->pivotTable = $pivotTable; $this->joinForeignKeys = $joinForeignKeys; $this->joinLocalKeys = $joinLocalKeys; $this->whereKeys = $whereKeys; + $this->resultIteratorClass = $resultIteratorClass; } public static function generateModelKey(ForeignKeyConstraint $remoteFk, ForeignKeyConstraint $localFk): string @@ -95,4 +103,9 @@ public function getPivotParams(array $primaryKeys): array } return $params; } + + public function getResultIteratorClass(): string + { + return $this->resultIteratorClass; + } } diff --git a/src/Utils/ObjectBeanPropertyDescriptor.php b/src/Utils/ObjectBeanPropertyDescriptor.php index d9605670..7d437333 100644 --- a/src/Utils/ObjectBeanPropertyDescriptor.php +++ b/src/Utils/ObjectBeanPropertyDescriptor.php @@ -32,6 +32,10 @@ class ObjectBeanPropertyDescriptor extends AbstractBeanPropertyDescriptor * @var BeanDescriptor */ private $foreignBeanDescriptor; + /** + * @var string + */ + private $resultIteratorNamespace; /** * ObjectBeanPropertyDescriptor constructor. @@ -41,6 +45,7 @@ class ObjectBeanPropertyDescriptor extends AbstractBeanPropertyDescriptor * @param string $beanNamespace * @param AnnotationParser $annotationParser * @param BeanDescriptor $foreignBeanDescriptor The BeanDescriptor of FK foreign table + * @param string $resultIteratorNamespace */ public function __construct( Table $table, @@ -48,7 +53,8 @@ public function __construct( NamingStrategyInterface $namingStrategy, string $beanNamespace, AnnotationParser $annotationParser, - BeanDescriptor $foreignBeanDescriptor + BeanDescriptor $foreignBeanDescriptor, + string $resultIteratorNamespace ) { parent::__construct($table, $namingStrategy); $this->foreignKey = $foreignKey; @@ -57,6 +63,7 @@ public function __construct( $this->table = $table; $this->namingStrategy = $namingStrategy; $this->foreignBeanDescriptor = $foreignBeanDescriptor; + $this->resultIteratorNamespace = $resultIteratorNamespace; } /** @@ -152,25 +159,27 @@ public function isPrimaryKey(): bool public function getGetterSetterCode(): array { $tableName = $this->table->getName(); + $foreignTableName = $this->foreignKey->getForeignTableName(); $getterName = $this->getGetterName(); $setterName = $this->getSetterName(); $isNullable = !$this->isCompulsory(); - $referencedBeanName = $this->namingStrategy->getBeanClassName($this->foreignKey->getForeignTableName()); + $referencedBeanName = $this->namingStrategy->getBeanClassName($foreignTableName); + $referencedResultIteratorName = $this->namingStrategy->getResultIteratorClassName($foreignTableName); $getter = new MethodGenerator($getterName); $getter->setDocBlock('Returns the ' . $referencedBeanName . ' object bound to this object via the ' . implode(' and ', $this->foreignKey->getUnquotedLocalColumns()) . ' column.'); - /*$types = [ $referencedBeanName ]; - if ($isNullable) { - $types[] = 'null'; - } - $getter->getDocBlock()->setTag(new ReturnTag($types));*/ - $getter->setReturnType(($isNullable ? '?' : '') . $this->beanNamespace . '\\' . $referencedBeanName); $tdbmFk = ForeignKey::createFromFk($this->foreignKey); - $getter->setBody('return $this->getRef(' . var_export($tdbmFk->getCacheKey(), true) . ', ' . var_export($tableName, true) . ');'); + $getter->setBody(sprintf( + 'return $this->getRef(%s, %s, %s, %s);', + var_export($tdbmFk->getCacheKey(), true), + var_export($tableName, true), + '\\' . $this->beanNamespace . '\\' . $referencedBeanName . '::class', + '\\' . $this->resultIteratorNamespace . '\\' . $referencedResultIteratorName . '::class' + )); if ($this->isGetterProtected()) { $getter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED); @@ -183,7 +192,14 @@ public function getGetterSetterCode(): array $setter->setReturnType('void'); - $setter->setBody('$this->setRef(' . var_export($tdbmFk->getCacheKey(), true) . ', $object, ' . var_export($tableName, true) . ');'); + $setter->setBody(sprintf( + '$this->setRef(%s, %s, %s, %s, %s);', + var_export($tdbmFk->getCacheKey(), true), + '$object', + var_export($tableName, true), + '\\' . $this->beanNamespace . '\\' . $referencedBeanName . '::class', + '\\' . $this->resultIteratorNamespace . '\\' . $referencedResultIteratorName . '::class' + )); if ($this->isSetterProtected()) { $setter->setVisibility(AbstractMemberGenerator::VISIBILITY_PROTECTED); diff --git a/src/Utils/PivotTableMethodsDescriptor.php b/src/Utils/PivotTableMethodsDescriptor.php index 36dee569..68f97e8a 100644 --- a/src/Utils/PivotTableMethodsDescriptor.php +++ b/src/Utils/PivotTableMethodsDescriptor.php @@ -60,6 +60,10 @@ class PivotTableMethodsDescriptor implements RelationshipMethodDescriptorInterfa * @var string */ private $pathKey; + /** + * @var string + */ + private $resultIteratorNamespace; /** * @param Table $pivotTable The pivot table @@ -67,14 +71,22 @@ class PivotTableMethodsDescriptor implements RelationshipMethodDescriptorInterfa * @param ForeignKeyConstraint $remoteFk * @param NamingStrategyInterface $namingStrategy */ - public function __construct(Table $pivotTable, ForeignKeyConstraint $localFk, ForeignKeyConstraint $remoteFk, NamingStrategyInterface $namingStrategy, string $beanNamespace, AnnotationParser $annotationParser) - { + public function __construct( + Table $pivotTable, + ForeignKeyConstraint $localFk, + ForeignKeyConstraint $remoteFk, + NamingStrategyInterface $namingStrategy, + AnnotationParser $annotationParser, + string $beanNamespace, + string $resultIteratorNamespace + ) { $this->pivotTable = $pivotTable; $this->localFk = $localFk; $this->remoteFk = $remoteFk; $this->namingStrategy = $namingStrategy; - $this->beanNamespace = $beanNamespace; $this->annotationParser = $annotationParser; + $this->beanNamespace = $beanNamespace; + $this->resultIteratorNamespace = $resultIteratorNamespace; $this->pathKey = ManyToManyRelationshipPathDescriptor::generateModelKey($this->remoteFk, $this->localFk); } @@ -107,6 +119,16 @@ public function getBeanClassName(): string return $this->namingStrategy->getBeanClassName($this->remoteFk->getForeignTableName()); } + /** + * Returns the name of the class that will be returned by the getter (short name). + * + * @return string + */ + public function getResultIteratorClassName(): string + { + return $this->namingStrategy->getResultIteratorClassName($this->remoteFk->getForeignTableName()); + } + /** * Returns the plural name. * @@ -159,6 +181,7 @@ public function getManyToManyRelationshipInstantiationCode(): string ', '.$this->getArrayInlineCode($this->remoteFk->getUnquotedForeignColumns()). ', '.$this->getArrayInlineCode($this->remoteFk->getUnquotedLocalColumns()). ', '.$this->getArrayInlineCode($this->localFk->getUnquotedLocalColumns()). + ', \\' . $this->resultIteratorNamespace . '\\' . $this->getResultIteratorClassName() . '::class'. ')'; } diff --git a/tests/AbstractTDBMObjectTest.php b/tests/AbstractTDBMObjectTest.php index ff81c211..ac0c4bd4 100644 --- a/tests/AbstractTDBMObjectTest.php +++ b/tests/AbstractTDBMObjectTest.php @@ -17,7 +17,7 @@ public function testGetManyToManyRelationshipDescriptor() public function testEmptyResultIterator() { - $a = ResultIterator::createEmpyIterator(); + $a = ResultIterator::createEmptyIterator(); foreach ($a as $empty) { throw new \LogicException("Not supposed to iterate on an empty iterator."); } @@ -43,7 +43,7 @@ public function testEmptyResultIterator() public function testEmptyPageIterator() { - $a = ResultIterator::createEmpyIterator(); + $a = ResultIterator::createEmptyIterator(); $b = $a->take(0, 10); foreach ($b as $empty) { throw new \LogicException("Not supposed to iterate on an empty page iterator."); diff --git a/tests/Dao/TestUserDao.php b/tests/Dao/TestUserDao.php index 754f650d..f30a8438 100644 --- a/tests/Dao/TestUserDao.php +++ b/tests/Dao/TestUserDao.php @@ -6,6 +6,7 @@ use TheCodingMachine\TDBM\Test\Dao\Bean\CountryBean; use TheCodingMachine\TDBM\Test\Dao\Bean\UserBean; use TheCodingMachine\TDBM\Test\Dao\Generated\UserBaseDao; +use TheCodingMachine\TDBM\Test\ResultIterator\UserResultIterator; use TheCodingMachine\TDBM\UncheckedOrderBy; /** @@ -15,20 +16,16 @@ class TestUserDao extends UserBaseDao { /** * Returns the list of users by alphabetical order. - * - * @return UserBean[] */ - public function getUsersByAlphabeticalOrder() + public function getUsersByAlphabeticalOrder(): UserResultIterator { // The third parameter will be used in the "ORDER BY" clause of the SQL query. return $this->find(null, [], 'login ASC'); } /** * Returns the list of users by alphabetical order. - * - * @return UserBean[] */ - public function getUsersFromSqlByAlphabeticalOrder() + public function getUsersFromSqlByAlphabeticalOrder(): UserResultIterator { // The third parameter will be used in the "ORDER BY" clause of the SQL query. return $this->findFromSql('users', null, [], 'users.login ASC'); @@ -36,20 +33,16 @@ public function getUsersFromSqlByAlphabeticalOrder() /** * Returns the list of users by alphabetical order. - * - * @return UserBean[] */ - public function getUsersByCountryOrder() + public function getUsersByCountryOrder(): UserResultIterator { // The third parameter will be used in the "ORDER BY" clause of the SQL query. return $this->find(null, [], 'country.label ASC', ['country']); } /** * Returns the list of users by alphabetical order. - * - * @return UserBean[] */ - public function getUsersFromSqlByCountryOrder() + public function getUsersFromSqlByCountryOrder(): UserResultIterator { // The third parameter will be used in the "ORDER BY" clause of the SQL query. return $this->findFromSql('users JOIN country ON country.id = users.country_id', null, [], 'country.label ASC'); @@ -60,10 +53,8 @@ public function getUsersFromSqlByCountryOrder() * * @param string $login * @param string $mode - * - * @return \TheCodingMachine\TDBM\ResultIterator|UserBean[] */ - public function getUsersByLoginStartingWith($login = null, $mode = null) + public function getUsersByLoginStartingWith($login = null, $mode = null): UserResultIterator { return $this->find('login LIKE :login', ['login' => $login.'%'], null, [], $mode); } @@ -87,30 +78,24 @@ public function getUsersByManagerId($managerId) /** * Triggers an error because table "contacts" does not exist. - * - * @return \TheCodingMachine\TDBM\ResultIterator|UserBean[] */ - public function getUsersWrongTableName() + public function getUsersWrongTableName(): UserResultIterator { return $this->find('contacts.manager_id = 1'); } /** * Returns a list of users, sorted by a table on an external column. - * - * @return \TheCodingMachine\TDBM\ResultIterator|UserBean[] */ - public function getUsersByCountryName() + public function getUsersByCountryName(): UserResultIterator { return $this->find(null, [], 'country.label DESC'); } /** * A test to sort by function. - * - * @return \TheCodingMachine\TDBM\ResultIterator|UserBean[] */ - public function getUsersByReversedCountryName() + public function getUsersByReversedCountryName(): UserResultIterator { return $this->find(null, [], new UncheckedOrderBy('REVERSE(country.label) ASC')); } diff --git a/tests/TDBMAbstractServiceTest.php b/tests/TDBMAbstractServiceTest.php index 79a2c202..268fc527 100644 --- a/tests/TDBMAbstractServiceTest.php +++ b/tests/TDBMAbstractServiceTest.php @@ -320,7 +320,7 @@ private static function initSchema(Connection $connection): void // Tables using @Json annotations $db->table('accounts') - ->column('id')->integer()->primaryKey()->autoIncrement() + ->column('id')->integer()->primaryKey()->autoIncrement()->notNull() ->column('name')->string(); $db->table('nodes') diff --git a/tests/TDBMServiceTest.php b/tests/TDBMServiceTest.php index 6d83eda1..4658ae84 100644 --- a/tests/TDBMServiceTest.php +++ b/tests/TDBMServiceTest.php @@ -23,6 +23,12 @@ use Psr\Log\LogLevel; use Psr\Log\NullLogger; +use TheCodingMachine\TDBM\Test\Dao\Bean\ContactBean; +use TheCodingMachine\TDBM\Test\ResultIterator\ContactResultIterator; +use TheCodingMachine\TDBM\Test\ResultIterator\CountryResultIterator; +use TheCodingMachine\TDBM\Test\ResultIterator\PersonResultIterator; +use TheCodingMachine\TDBM\Test\ResultIterator\RoleResultIterator; +use TheCodingMachine\TDBM\Test\ResultIterator\UserResultIterator; use Wa72\SimpleLogger\ArrayLogger; class TDBMServiceTest extends TDBMAbstractServiceTest @@ -126,7 +132,7 @@ public function testInsertMultipleDataAtOnceInInheritance(): void public function testCompleteSave(): void { - $beans = $this->tdbmService->findObjects('users', 'users.login = :login', ['login' => 'jane.doe'], null, [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('users', 'users.login = :login', ['login' => 'jane.doe'], null, [], null, TDBMObject::class, ResultIterator::class); $jane = $beans[0]; $jane->setProperty('country_id', 2, 'users'); @@ -136,7 +142,7 @@ public function testCompleteSave(): void public function testCompleteSave2(): void { - $beans = $this->tdbmService->findObjects('users', 'users.login = :login', ['login' => 'jane.doe'], null, [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('users', 'users.login = :login', ['login' => 'jane.doe'], null, [], null, TDBMObject::class, ResultIterator::class); $jane = $beans[0]; $this->assertEquals(2, $jane->getProperty('country_id', 'users')); @@ -209,8 +215,8 @@ public function testFindObjects(): void $result = $magicQuery->parse("SELECT DISTINCT users.id, users.login FROM users"); var_dump($result);*/ - $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class); - $beans2 = $this->tdbmService->findObjects('contact', 'contact.id = :id', ['id' => 1], null, [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class, ContactResultIterator::class); + $beans2 = $this->tdbmService->findObjects('contact', 'contact.id = :id', ['id' => 1], null, [], null, TDBMObject::class, ContactResultIterator::class); foreach ($beans as $bean) { $bean1 = $bean; @@ -242,7 +248,7 @@ public function testRawSqlFilterCountriesByUserCount(): void HAVING COUNT(users.id) > 1; SQL; /** @var Test\Dao\Bean\CountryBean[]|\Porpaginas\Result $beans */ - $beans = $this->tdbmService->findObjectsFromRawSql('country', $sql, [], null, Test\Dao\Bean\CountryBean::class); + $beans = $this->tdbmService->findObjectsFromRawSql('country', $sql, [], null, Test\Dao\Bean\CountryBean::class, null, CountryResultIterator::class); $count = 0; foreach ($beans as $country) { @@ -265,7 +271,7 @@ public function testRawSqlOrderCountriesByUserCount(): void SQL; /** @var Test\Dao\Bean\CountryBean[]|\Porpaginas\Result $beans */ - $beans = $this->tdbmService->findObjectsFromRawSql('country', $sql, [], null, Test\Dao\Bean\CountryBean::class); + $beans = $this->tdbmService->findObjectsFromRawSql('country', $sql, [], null, Test\Dao\Bean\CountryBean::class, null,CountryResultIterator::class); $count = 0; foreach ($beans as $country) { @@ -294,7 +300,7 @@ public function testRawSqlOrderUsersByCustomRoleOrder(): void SQL; /** @var Test\Dao\Bean\UserBean[]|\Porpaginas\Result $beans */ - $beans = $this->tdbmService->findObjectsFromRawSql('contact', $sql, [], null, Test\Dao\Bean\UserBean::class); + $beans = $this->tdbmService->findObjectsFromRawSql('contact', $sql, [], null, Test\Dao\Bean\UserBean::class, null, UserResultIterator::class); function getCustomOrder(Test\Dao\Bean\UserBean $contact) { @@ -318,7 +324,7 @@ function getCustomOrder(Test\Dao\Bean\UserBean $contact) public function testArrayAccess(): void { - $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class, ResultIterator::class); $this->assertTrue(isset($beans[0])); $this->assertFalse(isset($beans[42])); @@ -345,7 +351,7 @@ public function testArrayAccess(): void public function testArrayAccessException(): void { $this->expectException('TheCodingMachine\TDBM\TDBMInvalidOffsetException'); - $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class, ContactResultIterator::class); $beans[-1]; } @@ -357,7 +363,7 @@ public function testArrayAccessException(): void public function testArrayAccessException2(): void { $this->expectException('TheCodingMachine\TDBM\TDBMInvalidOffsetException'); - $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class, ContactResultIterator::class); $beans['foo']; } @@ -369,7 +375,7 @@ public function testArrayAccessException2(): void public function testBeanGetException(): void { $this->expectException('TheCodingMachine\TDBM\TDBMException'); - $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class, ResultIterator::class); $bean = $beans[0]; // we don't specify the table on inheritance table => exception. @@ -383,7 +389,7 @@ public function testBeanGetException(): void public function testBeanSetException(): void { $this->expectException('TheCodingMachine\TDBM\TDBMException'); - $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class, ResultIterator::class); $bean = $beans[0]; // we don't specify the table on inheritance table => exception. @@ -392,7 +398,7 @@ public function testBeanSetException(): void public function testTake(): void { - $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class, ResultIterator::class); $page = $beans->take(0, 2); @@ -419,7 +425,7 @@ public function testTake(): void public function testTakeInCursorMode(): void { - $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], TDBMService::MODE_CURSOR, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], TDBMService::MODE_CURSOR, TDBMObject::class, ContactResultIterator::class); $page = $beans->take(0, 2); @@ -446,7 +452,7 @@ public function testTakeInCursorMode(): void public function testMap(): void { - $beans = $this->tdbmService->findObjects('person', null, [], 'person.id ASC', [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('person', null, [], 'person.id ASC', [], null, TDBMObject::class, ResultIterator::class); $results = $beans->map(function ($item) { return $item->getProperty('id', 'person'); @@ -471,7 +477,7 @@ public function testMap(): void public function testUnsetException(): void { $this->expectException('TheCodingMachine\TDBM\TDBMException'); - $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class, ContactResultIterator::class); unset($beans[0]); } @@ -483,7 +489,7 @@ public function testUnsetException(): void public function testSetException(): void { $this->expectException('TheCodingMachine\TDBM\TDBMException'); - $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class, ContactResultIterator::class); $beans[0] = 'foo'; } @@ -495,7 +501,7 @@ public function testSetException(): void public function testPageUnsetException(): void { $this->expectException('TheCodingMachine\TDBM\TDBMException'); - $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class, ContactResultIterator::class); $page = $beans->take(0, 1); unset($page[0]); } @@ -507,14 +513,14 @@ public function testPageUnsetException(): void public function testPageSetException(): void { $this->expectException('TheCodingMachine\TDBM\TDBMException'); - $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class, ContactResultIterator::class); $page = $beans->take(0, 1); $page[0] = 'foo'; } public function testToArray(): void { - $beans = $this->tdbmService->findObjects('contact', 'contact.id = :id', ['id' => 1], null, [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', 'contact.id = :id', ['id' => 1], null, [], null, TDBMObject::class, ResultIterator::class); $beanArray = $beans->toArray(); @@ -524,7 +530,7 @@ public function testToArray(): void public function testCursorMode(): void { - $beans = $this->tdbmService->findObjects('contact', 'contact.id = :id', ['id' => 1], null, [], TDBMService::MODE_CURSOR, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', 'contact.id = :id', ['id' => 1], null, [], TDBMService::MODE_CURSOR, TDBMObject::class, ContactResultIterator::class); $this->assertInstanceOf('\\TheCodingMachine\\TDBM\\ResultIterator', $beans); @@ -556,7 +562,7 @@ public function testCursorMode(): void public function testSetFetchMode(): void { $this->tdbmService->setFetchMode(TDBMService::MODE_CURSOR); - $beans = $this->tdbmService->findObjects('contact', 'contact.id = :id', ['id' => 1], null, [], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', 'contact.id = :id', ['id' => 1], null, [], null, TDBMObject::class, ContactResultIterator::class); $this->assertInstanceOf('\\TheCodingMachine\\TDBM\\ResultIterator', $beans); @@ -587,7 +593,7 @@ public function testInvalidSetFetchMode(): void public function testCursorModeException(): void { $this->expectException('TheCodingMachine\TDBM\TDBMException'); - $beans = $this->tdbmService->findObjects('contact', 'contact.id = :id', ['id' => 1], null, [], 99); + $beans = $this->tdbmService->findObjects('contact', 'contact.id = :id', ['id' => 1], null, [], 99, ContactBean::class, ContactResultIterator::class); } /** @@ -597,18 +603,18 @@ public function testCursorModeException(): void public function testTableNameException(): void { $this->expectException('TheCodingMachine\TDBM\TDBMException'); - $beans = $this->tdbmService->findObjects('foo bar'); + $beans = $this->tdbmService->findObjects('foo bar', null, [], null, [], null, AbstractTDBMObject::class, ResultIterator::class); } public function testLinkedTableFetch(): void { - $beans = $this->tdbmService->findObjects('contact', 'contact.id = :id', ['id' => 1], null, ['country'], null, TDBMObject::class); + $beans = $this->tdbmService->findObjects('contact', 'contact.id = :id', ['id' => 1], null, ['country'], null, TDBMObject::class, ContactResultIterator::class); $this->assertInstanceOf(ResultIterator::class, $beans); } public function testFindObject(): void { - $bean = $this->tdbmService->findObject('contact', 'contact.id = :id', ['id' => -42], [], TDBMObject::class); + $bean = $this->tdbmService->findObject('contact', 'contact.id = :id', ['id' => -42], [], TDBMObject::class, ContactResultIterator::class); $this->assertNull($bean); } @@ -619,7 +625,7 @@ public function testFindObject(): void public function testFindObjectOrFail(): void { $this->expectException('TheCodingMachine\TDBM\NoBeanFoundException'); - $bean = $this->tdbmService->findObjectOrFail('contact', 'contact.id = :id', ['id' => -42], [], TDBMObject::class); + $bean = $this->tdbmService->findObjectOrFail('contact', 'contact.id = :id', ['id' => -42], [], TDBMObject::class, ContactResultIterator::class); } /** @@ -629,7 +635,7 @@ public function testFindObjectByPkException(): void { $this->expectException(NoBeanFoundException::class); $this->expectExceptionMessage("No result found for query on table 'contact' for 'id' = -42"); - $bean = $this->tdbmService->findObjectByPk('contact', ['id' => -42], [], false, TDBMObject::class); + $bean = $this->tdbmService->findObjectByPk('contact', ['id' => -42], [], false, TDBMObject::class, ContactResultIterator::class); } /** @@ -639,14 +645,14 @@ public function testFindObjectDuplicateRow(): void { $this->expectException(DuplicateRowException::class); - $bean = $this->tdbmService->findObject('contact'); + $bean = $this->tdbmService->findObject('contact', null, [], [], TDBMObject::class, ContactResultIterator::class); } public function testFindObjectsByBean(): void { - $countryBean = $this->tdbmService->findObject('country', 'id = :id', ['id' => 1], [], TDBMObject::class); + $countryBean = $this->tdbmService->findObject('country', 'id = :id', ['id' => 1], [], TDBMObject::class, ResultIterator::class); - $users = $this->tdbmService->findObjects('users', $countryBean, [], null, [], null, TDBMObject::class); + $users = $this->tdbmService->findObjects('users', $countryBean, [], null, [], null, TDBMObject::class, ResultIterator::class); $this->assertCount(1, $users); $this->assertEquals('jean.dupont', $users[0]->getProperty('login', 'users')); } @@ -671,7 +677,10 @@ public function testFindObjectsFromSql(): void 'roles JOIN roles_rights ON roles.id = roles_rights.role_id JOIN rights ON rights.label = roles_rights.right_label', 'rights.label = :right', array('right' => 'CAN_SING'), - 'roles.name DESC' + 'roles.name DESC', + null, + null, + RoleResultIterator::class ); $this->assertCount(2, $roles); $this->assertInstanceOf(AbstractTDBMObject::class, $roles[0]); @@ -689,7 +698,10 @@ public function testFindObjectsFromSqlBadTableName(): void 'roles JOIN roles_rights ON roles.id = roles_rights.role_id JOIN rights ON rights.label = roles_rights.right_label', 'rights.label = :right', array('right' => 'CAN_SING'), - 'name DESC' + 'name DESC', + null, + null, + ResultIterator::class ); } @@ -705,7 +717,10 @@ public function testFindObjectsFromSqlGroupBy(): void 'roles JOIN roles_rights ON roles.id = roles_rights.role_id JOIN rights ON rights.label = roles_rights.right_label', 'rights.label = :right GROUP BY roles.name', array('right' => 'CAN_SING'), - 'name DESC' + 'name DESC', + null, + null, + RoleResultIterator::class ); $role = $roles[0]; } @@ -720,7 +735,11 @@ public function testFindObjectsFromRawSqlBadTableName(): void $this->tdbmService->findObjectsFromRawSql( '#{azerty', 'roles JOIN roles_rights ON roles.id = roles_rights.role_id JOIN rights ON rights.label = roles_rights.right_label WHERE rights.label = :right', - array('right' => 'CAN_SING') + array('right' => 'CAN_SING'), + null, + TDBMObject::class, + null, + ResultIterator::class ); } @@ -730,7 +749,9 @@ public function testFindObjectFromSql(): void 'roles', 'roles JOIN roles_rights ON roles.id = roles_rights.role_id JOIN rights ON rights.label = roles_rights.right_label', 'rights.label = :right AND name = :name', - array('right' => 'CAN_SING', 'name' => 'Singers') + array('right' => 'CAN_SING', 'name' => 'Singers'), + null, + RoleResultIterator::class ); $this->assertInstanceOf(AbstractTDBMObject::class, $role); } @@ -746,7 +767,9 @@ public function testFindObjectFromSqlException(): void 'roles', 'roles JOIN roles_rights ON roles.id = roles_rights.role_id JOIN rights ON rights.label = roles_rights.right_label', 'rights.label = :right', - array('right' => 'CAN_SING') + array('right' => 'CAN_SING'), + null, + RoleResultIterator::class ); } @@ -759,7 +782,8 @@ public function testFindObjectsFromSqlHierarchyDown(): void array('name' => 'Robert Marley', 'name2' => 'Bill Shakespeare'), null, null, - TDBMObject::class + TDBMObject::class, + PersonResultIterator::class ); $this->assertCount(2, $users); $this->assertSame('robert.marley', $users[0]->getProperty('login', 'users')); @@ -774,7 +798,8 @@ public function testFindObjectsFromSqlHierarchyUp(): void array('login' => 'robert.marley', 'login2' => 'bill.shakespeare'), 'users.login DESC', null, - TDBMObject::class + TDBMObject::class, + UserResultIterator::class ); $this->assertCount(2, $users); $this->assertSame('Robert Marley', $users[0]->getProperty('name', 'person')); @@ -786,7 +811,7 @@ public function testLogger(): void $tdbmService = new TDBMService(new Configuration('TheCodingMachine\\TDBM\\Test\\Dao\\Bean', 'TheCodingMachine\\TDBM\\Test\\Dao', self::getConnection(), $this->getNamingStrategy(), null, null, $arrayLogger)); $tdbmService->setLogLevel(LogLevel::DEBUG); - $beans = $tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class); + $beans = $tdbmService->findObjects('contact', null, [], 'contact.id ASC', [], null, TDBMObject::class, ContactResultIterator::class); $beans->first(); $this->assertNotEmpty($arrayLogger->get()); @@ -794,14 +819,14 @@ public function testLogger(): void public function testFindObjectsCountWithOneToManyLink(): void { - $countries = $this->tdbmService->findObjects('country', "users.status = 'on' OR users.status = 'off'"); + $countries = $this->tdbmService->findObjects('country', "users.status = 'on' OR users.status = 'off'", [], null, [], null, null, CountryResultIterator::class); $this->assertEquals(3, $countries->count()); } public function testFindObjectsFromSqlCountWithOneToManyLink(): void { - $countries = $this->tdbmService->findObjectsFromSql('country', 'country LEFT JOIN users ON country.id = users.country_id', "users.status = 'on' OR users.status = 'off'"); + $countries = $this->tdbmService->findObjectsFromSql('country', 'country LEFT JOIN users ON country.id = users.country_id', "users.status = 'on' OR users.status = 'off'", [], null, null, null, CountryResultIterator::class); $this->assertEquals(3, $countries->count()); } diff --git a/tests/Utils/DirectForeignKeyMethodDescriptorTest.php b/tests/Utils/DirectForeignKeyMethodDescriptorTest.php index 7b5cb335..51b8e48c 100644 --- a/tests/Utils/DirectForeignKeyMethodDescriptorTest.php +++ b/tests/Utils/DirectForeignKeyMethodDescriptorTest.php @@ -15,7 +15,7 @@ public function testGetForeignKey(): void $table = $this->createMock(Table::class); $ns = $this->createMock(DefaultNamingStrategy::class); $ap = $this->createMock(AnnotationParser::class); - $descriptor = new DirectForeignKeyMethodDescriptor($fk, $table, $ns, $ap, ''); + $descriptor = new DirectForeignKeyMethodDescriptor($fk, $table, $ns, $ap, '', ''); $this->assertSame($fk, $descriptor->getForeignKey()); $this->assertSame($table, $descriptor->getMainTable()); diff --git a/tests/Utils/PivotTableMethodsDescriptorTest.php b/tests/Utils/PivotTableMethodsDescriptorTest.php index 434c5124..ad742005 100644 --- a/tests/Utils/PivotTableMethodsDescriptorTest.php +++ b/tests/Utils/PivotTableMethodsDescriptorTest.php @@ -17,7 +17,7 @@ public function testGetters(): void $remoteFk = new ForeignKeyConstraint(['foo2'], new Table('table2'), ['lol2']); $remoteFk->setLocalTable(new Table('table3')); $ns = $this->createMock(DefaultNamingStrategy::class); - $descriptor = new PivotTableMethodsDescriptor($table, $localFk, $remoteFk, $ns, 'Bean\Namespace', AnnotationParser::buildWithDefaultAnnotations([])); + $descriptor = new PivotTableMethodsDescriptor($table, $localFk, $remoteFk, $ns, AnnotationParser::buildWithDefaultAnnotations([]), 'Bean\Namespace', 'ResultIterator\Namespace'); $this->assertSame($table, $descriptor->getPivotTable()); $this->assertSame($localFk, $descriptor->getLocalFk()); From 171aa0c8277b1bf573e547147857c328098b4759 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Fri, 11 Sep 2020 14:19:55 +0200 Subject: [PATCH 2/2] Whitelist: `->setWhitelist()`, handle partial queries --- src/AbstractTDBMObject.php | 20 ++++--- src/DbRow.php | 36 ++++++++----- src/InnerResultIterator.php | 25 +++++++-- src/PageIterator.php | 10 ++-- src/QueryFactory/AbstractQueryFactory.php | 9 ++-- .../FindObjectsFromRawSqlQueryFactory.php | 8 +++ .../FindObjectsFromSqlQueryFactory.php | 10 +++- src/QueryFactory/FindObjectsQueryFactory.php | 14 ++++- src/QueryFactory/QueryFactory.php | 5 ++ src/ResultIterator.php | 26 ++++++---- src/Utils/BeanDescriptor.php | 42 +++++++-------- tests/Dao/TestUserDao.php | 5 ++ tests/TDBMDaoGeneratorTest.php | 52 +++++++++++++++++++ tests/TDBMServiceTest.php | 2 +- 14 files changed, 199 insertions(+), 65 deletions(-) diff --git a/src/AbstractTDBMObject.php b/src/AbstractTDBMObject.php index 623b6508..b1082d79 100644 --- a/src/AbstractTDBMObject.php +++ b/src/AbstractTDBMObject.php @@ -99,7 +99,7 @@ public function __construct(?string $tableName = null, array $primaryKeys = [], $this->_setStatus(TDBMObjectStateEnum::STATE_DETACHED); } else { $this->_attach($tdbmService); - if (!empty($primaryKeys)) { // @TODO (gua) might not be fully loaded + if (!empty($primaryKeys)) { $this->_setStatus(TDBMObjectStateEnum::STATE_NOT_LOADED); } else { $this->_setStatus(TDBMObjectStateEnum::STATE_NEW); @@ -113,15 +113,23 @@ public function __construct(?string $tableName = null, array $primaryKeys = [], * @param array> $beanData array> * @param TDBMService $tdbmService */ - public function _constructFromData(array $beanData, TDBMService $tdbmService): void + public function _constructFromData(array $beanData, TDBMService $tdbmService, bool $isFullyLoaced): void { $this->tdbmService = $tdbmService; foreach ($beanData as $table => $columns) { - $this->dbRows[$table] = new DbRow($this, $table, static::getForeignKeys($table), $tdbmService->_getPrimaryKeysFromObjectData($table, $columns), $tdbmService, $columns); - } - - $this->status = TDBMObjectStateEnum::STATE_LOADED; // @TODO might be not fully loaded + $this->dbRows[$table] = new DbRow( + $this, + $table, + static::getForeignKeys($table), + $tdbmService->_getPrimaryKeysFromObjectData($table, $columns), + $tdbmService, + $columns, + $isFullyLoaced + ); + } + + $this->status = $isFullyLoaced ? TDBMObjectStateEnum::STATE_LOADED : TDBMObjectStateEnum::STATE_PARTIALLY_LOADED; } /** diff --git a/src/DbRow.php b/src/DbRow.php index e8ebe342..d0cc3290 100644 --- a/src/DbRow.php +++ b/src/DbRow.php @@ -21,6 +21,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ +use TheCodingMachine\TDBM\Exception\TDBMPartialQueryException; use TheCodingMachine\TDBM\Schema\ForeignKeys; /** @@ -86,14 +87,14 @@ class DbRow /** * A list of modified columns, indexed by column name. Value is always true. * - * @var array + * @var array */ private $modifiedColumns = []; /** * A list of modified references, indexed by foreign key name. Value is always true. * - * @var array + * @var array */ private $modifiedReferences = []; /** @@ -105,18 +106,23 @@ class DbRow * You should never call the constructor directly. Instead, you should use the * TDBMService class that will create TDBMObjects for you. * - * Used with id!=false when we want to retrieve an existing object - * and id==false if we want a new object - * * @param AbstractTDBMObject $object The object containing this db row * @param string $tableName + * @param ForeignKeys $foreignKeys * @param mixed[] $primaryKeys * @param TDBMService $tdbmService * @param mixed[] $dbRow * @throws TDBMException */ - public function __construct(AbstractTDBMObject $object, string $tableName, ForeignKeys $foreignKeys, array $primaryKeys = array(), TDBMService $tdbmService = null, array $dbRow = []) - { + public function __construct( + AbstractTDBMObject $object, + string $tableName, + ForeignKeys $foreignKeys, + array $primaryKeys = array(), + TDBMService $tdbmService = null, + array $dbRow = [], + bool $isFullyLoaced = null + ) { $this->object = $object; $this->dbTableName = $tableName; $this->foreignKeys = $foreignKeys; @@ -133,9 +139,11 @@ public function __construct(AbstractTDBMObject $object, string $tableName, Forei if (!empty($primaryKeys)) { $this->_setPrimaryKeys($primaryKeys); if (!empty($dbRow)) { + if ($isFullyLoaced === null) { + throw new TDBMInvalidArgumentException('$isFullyLoaced need to be provided if the DbRow is not empty.'); + } $this->dbRow = $dbRow; - // @TODO (gua): might not be fully loaded - $this->status = TDBMObjectStateEnum::STATE_LOADED; + $this->status = $isFullyLoaced ? TDBMObjectStateEnum::STATE_LOADED : TDBMObjectStateEnum::STATE_PARTIALLY_LOADED; } else { $this->status = TDBMObjectStateEnum::STATE_NOT_LOADED; } @@ -173,6 +181,8 @@ public function _setStatus(string $state) : void // after saving we are back to a loaded state, hence unmodified. $this->modifiedColumns = []; $this->modifiedReferences = []; + } elseif ($state === TDBMObjectStateEnum::STATE_NOT_LOADED) { + $this->dbRow = []; } } @@ -220,10 +230,10 @@ public function _dbLoadIfNotLoaded(): void */ public function get(string $var) { - if ($this->_getStatus() === TDBMObjectStateEnum::STATE_PARTIALLY_LOADED && !array_key_exists($var, $this->dbRow)) { - throw new TDBMInvalidArgumentException('Cannot load `'.$var.'` in partially loaded object with data ' . print_r($this->dbRow, true)); - } - if (!isset($this->primaryKeys[$var])) { + if (!array_key_exists($var, $this->dbRow)) { + if ($this->_getStatus() === TDBMObjectStateEnum::STATE_PARTIALLY_LOADED) { + throw new TDBMPartialQueryException($var, $this->dbRow); + } $this->_dbLoadIfNotLoaded(); } diff --git a/src/InnerResultIterator.php b/src/InnerResultIterator.php index b6cc626a..c7c66988 100644 --- a/src/InnerResultIterator.php +++ b/src/InnerResultIterator.php @@ -49,6 +49,7 @@ class InnerResultIterator implements \Iterator, InnerResultIteratorInterface private $limit; private $offset; private $columnDescriptors; + /** @var MagicQuery */ private $magicQuery; /** @@ -66,6 +67,8 @@ class InnerResultIterator implements \Iterator, InnerResultIteratorInterface * @var LoggerInterface */ private $logger; + /** @var bool */ + private $hasExcludedColumns; protected $count = null; @@ -77,9 +80,20 @@ private function __construct() * @param mixed[] $parameters * @param array[] $columnDescriptors */ - public static function createInnerResultIterator(string $magicSql, array $parameters, ?int $limit, ?int $offset, array $columnDescriptors, ObjectStorageInterface $objectStorage, ?string $className, TDBMService $tdbmService, MagicQuery $magicQuery, LoggerInterface $logger): self - { - $iterator = new static(); // @TODO (gua) Should I know here if it's a partial load ? (to allow to give that state to DBRow and TDBMObject) + public static function createInnerResultIterator( + string $magicSql, + array $parameters, + ?int $limit, + ?int $offset, + array $columnDescriptors, + ObjectStorageInterface $objectStorage, + ?string $className, + TDBMService $tdbmService, + MagicQuery $magicQuery, + LoggerInterface $logger, + bool $hasExcludedColumns + ): self { + $iterator = new static(); $iterator->magicSql = $magicSql; $iterator->objectStorage = $objectStorage; $iterator->className = $className; @@ -91,6 +105,7 @@ public static function createInnerResultIterator(string $magicSql, array $parame $iterator->magicQuery = $magicQuery; $iterator->databasePlatform = $iterator->tdbmService->getConnection()->getDatabasePlatform(); $iterator->logger = $logger; + $iterator->hasExcludedColumns = $hasExcludedColumns; return $iterator; } @@ -224,6 +239,7 @@ public function next() $primaryKeys = $this->tdbmService->_getPrimaryKeysFromObjectData($mainBeanTableName, $beanData[$mainBeanTableName]); $hash = $this->tdbmService->getObjectHash($primaryKeys); + /** @var DbRow|null $dbRow */ $dbRow = $this->objectStorage->get($mainBeanTableName, $hash); if ($dbRow !== null) { $bean = $dbRow->getTDBMObject(); @@ -233,8 +249,9 @@ public function next() $reflectionClassCache[$actualClassName] = new \ReflectionClass($actualClassName); } // Let's bypass the constructor when creating the bean! + /** @var AbstractTDBMObject $bean */ $bean = $reflectionClassCache[$actualClassName]->newInstanceWithoutConstructor(); - $bean->_constructFromData($beanData, $this->tdbmService); + $bean->_constructFromData($beanData, $this->tdbmService, !$this->hasExcludedColumns); } // The first bean is the one containing the main table. diff --git a/src/PageIterator.php b/src/PageIterator.php index a59a3554..08d8a4b0 100644 --- a/src/PageIterator.php +++ b/src/PageIterator.php @@ -67,6 +67,8 @@ class PageIterator implements Page, \ArrayAccess, \JsonSerializable * @var LoggerInterface */ private $logger; + /** @var bool */ + private $hasExcludedColumns; private function __construct() { @@ -88,7 +90,8 @@ public static function createResultIterator( TDBMService $tdbmService, MagicQuery $magicQuery, int $mode, - LoggerInterface $logger + LoggerInterface $logger, + bool $hasExcludedColumns ): self { $iterator = new self(); $iterator->parentResult = $parentResult; @@ -103,6 +106,7 @@ public static function createResultIterator( $iterator->magicQuery = $magicQuery; $iterator->mode = $mode; $iterator->logger = $logger; + $iterator->hasExcludedColumns = $hasExcludedColumns; return $iterator; } @@ -125,9 +129,9 @@ public function getIterator() if ($this->parentResult->count() === 0) { $this->innerResultIterator = new EmptyInnerResultIterator(); } elseif ($this->mode === TDBMService::MODE_CURSOR) { - $this->innerResultIterator = InnerResultIterator::createInnerResultIterator($this->magicSql, $this->parameters, $this->limit, $this->offset, $this->columnDescriptors, $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger); + $this->innerResultIterator = InnerResultIterator::createInnerResultIterator($this->magicSql, $this->parameters, $this->limit, $this->offset, $this->columnDescriptors, $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger, $this->hasExcludedColumns); } else { - $this->innerResultIterator = InnerResultArray::createInnerResultIterator($this->magicSql, $this->parameters, $this->limit, $this->offset, $this->columnDescriptors, $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger); + $this->innerResultIterator = InnerResultArray::createInnerResultIterator($this->magicSql, $this->parameters, $this->limit, $this->offset, $this->columnDescriptors, $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger, $this->hasExcludedColumns); } } diff --git a/src/QueryFactory/AbstractQueryFactory.php b/src/QueryFactory/AbstractQueryFactory.php index d07cbf69..95e0f87a 100644 --- a/src/QueryFactory/AbstractQueryFactory.php +++ b/src/QueryFactory/AbstractQueryFactory.php @@ -87,7 +87,7 @@ public function setResultIterator(ResultIterator $resultIterator): void * @param string|UncheckedOrderBy|null $orderBy * * @param bool $canAddAdditionalTablesFetch Set to true if the function can add additional tables to fetch (so if the factory generates its own FROM clause) - * @return mixed[] A 3 elements array: [$columnDescList, $columnsList, $reconstructedOrderBy] + * @return mixed[] A 4 elements array: [$columnDescList, $columnsList, $reconstructedOrderBy, $hasExcludedColumns] */ protected function getColumnsList(string $mainTable, array $additionalTablesFetch = array(), $orderBy = null, bool $canAddAdditionalTablesFetch = false): array { @@ -187,11 +187,12 @@ protected function getColumnsList(string $mainTable, array $additionalTablesFetc $mysqlPlatform = new MySqlPlatform(); // Now, let's build the column list + $hasExcludedColumns = false; foreach ($allFetchedTables as $table) { foreach ($this->schema->getTable($table)->getColumns() as $column) { $columnName = $column->getName(); if ($this->resultIterator === null // @TODO (gua) don't take care of whitelist in case of LIMIT below 2 - || $table !== $mainTable + || $table !== $mainTable // @TODO (gua) Inheritance || $this->resultIterator->isInWhitelist($columnName, $table) ) { $columnDescList[$table . '____' . $columnName] = [ @@ -203,11 +204,13 @@ protected function getColumnsList(string $mainTable, array $additionalTablesFetc ]; $columnsList[] = $mysqlPlatform->quoteIdentifier($table) . '.' . $mysqlPlatform->quoteIdentifier($columnName) . ' as ' . $connection->quoteIdentifier($table . '____' . $columnName); + } else { + $hasExcludedColumns = true; } } } - return [$columnDescList, $columnsList, $reconstructedOrderBy]; + return [$columnDescList, $columnsList, $reconstructedOrderBy, $hasExcludedColumns]; } abstract protected function compute(): void; diff --git a/src/QueryFactory/FindObjectsFromRawSqlQueryFactory.php b/src/QueryFactory/FindObjectsFromRawSqlQueryFactory.php index f74ced2b..f1f8aded 100644 --- a/src/QueryFactory/FindObjectsFromRawSqlQueryFactory.php +++ b/src/QueryFactory/FindObjectsFromRawSqlQueryFactory.php @@ -421,4 +421,12 @@ public function getSubQueryColumnDescriptors(): array { throw new TDBMException('Using resultset generated from findFromRawSql as subqueries is unsupported for now.'); } + + /** + * @return bool Whether it has or no excluded columns. + */ + public function hasExcludedColumns(): bool + { + return false; + } } diff --git a/src/QueryFactory/FindObjectsFromSqlQueryFactory.php b/src/QueryFactory/FindObjectsFromSqlQueryFactory.php index f178da1e..e3c8d598 100644 --- a/src/QueryFactory/FindObjectsFromSqlQueryFactory.php +++ b/src/QueryFactory/FindObjectsFromSqlQueryFactory.php @@ -23,6 +23,8 @@ class FindObjectsFromSqlQueryFactory extends AbstractQueryFactory private $cache; private $cachePrefix; private $schemaAnalyzer; + /** @var bool */ + private $hasExcludedColumns; public function __construct(string $mainTable, string $from, $filterString, $orderBy, TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, SchemaAnalyzer $schemaAnalyzer, Cache $cache, string $cachePrefix) { @@ -43,7 +45,7 @@ protected function compute(): void $allFetchedTables = $this->tdbmService->_getRelatedTablesByInheritance($this->mainTable); - list($columnDescList, $columnsList, $orderString) = $this->getColumnsList($this->mainTable, [], $this->orderBy, false); + list($columnDescList, $columnsList, $orderString, $hasExcludedColumns) = $this->getColumnsList($this->mainTable, [], $this->orderBy, false); $sql = 'SELECT DISTINCT '.implode(', ', $columnsList).' FROM '.$this->from; @@ -104,6 +106,7 @@ protected function compute(): void $this->magicSqlCount = $countSql; $this->magicSqlSubQuery = $subQuery; $this->columnDescList = $columnDescList; + $this->hasExcludedColumns = $hasExcludedColumns; } /** @@ -169,6 +172,11 @@ private function getChildrenRelationshipForeignKeysWithoutCache(string $tableNam } } + public function hasExcludedColumns(): bool + { + return $this->hasExcludedColumns; + } + /** * Returns an item from cache or computes it using $closure and puts it in cache. * diff --git a/src/QueryFactory/FindObjectsQueryFactory.php b/src/QueryFactory/FindObjectsQueryFactory.php index fbaf4c44..3e187f66 100644 --- a/src/QueryFactory/FindObjectsQueryFactory.php +++ b/src/QueryFactory/FindObjectsQueryFactory.php @@ -21,6 +21,8 @@ class FindObjectsQueryFactory extends AbstractQueryFactory * @var Cache */ private $cache; + /** @var bool */ + private $hasExcludedColumns; public function __construct(string $mainTable, array $additionalTablesFetch, $filterString, $orderBy, TDBMService $tdbmService, Schema $schema, OrderByAnalyzer $orderByAnalyzer, Cache $cache) { @@ -37,12 +39,13 @@ protected function compute(): void [ $this->magicSql, $this->magicSqlCount, - $this->columnDescList + $this->columnDescList, + $this->hasExcludedColumns ] = $this->cache->fetch($key); return; } - list($columnDescList, $columnsList, $orderString) = $this->getColumnsList($this->mainTable, $this->additionalTablesFetch, $this->orderBy, true); + list($columnDescList, $columnsList, $orderString, $hasExcludedColumns) = $this->getColumnsList($this->mainTable, $this->additionalTablesFetch, $this->orderBy, true); $sql = 'SELECT DISTINCT '.implode(', ', $columnsList).' FROM MAGICJOIN('.$this->mainTable.')'; @@ -74,11 +77,18 @@ protected function compute(): void $this->magicSqlCount = $countSql; $this->magicSqlSubQuery = $subQuery; $this->columnDescList = $columnDescList; + $this->hasExcludedColumns = $hasExcludedColumns; $this->cache->save($key, [ $this->magicSql, $this->magicSqlCount, $this->columnDescList, + $this->hasExcludedColumns ]); } + + public function hasExcludedColumns(): bool + { + return $this->hasExcludedColumns; + } } diff --git a/src/QueryFactory/QueryFactory.php b/src/QueryFactory/QueryFactory.php index 6bf76211..a0232477 100644 --- a/src/QueryFactory/QueryFactory.php +++ b/src/QueryFactory/QueryFactory.php @@ -50,4 +50,9 @@ public function getColumnDescriptors() : array; public function getSubQueryColumnDescriptors() : array; public function setResultIterator(ResultIterator $resultIterator) : void; + + /** + * @return bool Whether it has or no excluded columns. + */ + public function hasExcludedColumns() : bool; } diff --git a/src/ResultIterator.php b/src/ResultIterator.php index 7419cda4..f50d3200 100644 --- a/src/ResultIterator.php +++ b/src/ResultIterator.php @@ -179,9 +179,9 @@ public function getIterator() if ($this->totalCount === 0) { $this->innerResultIterator = new EmptyInnerResultIterator(); } elseif ($this->mode === TDBMService::MODE_CURSOR) { - $this->innerResultIterator = InnerResultIterator::createInnerResultIterator($this->queryFactory->getMagicSql(), $this->parameters, null, null, $this->queryFactory->getColumnDescriptors(), $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger); + $this->innerResultIterator = InnerResultIterator::createInnerResultIterator($this->queryFactory->getMagicSql(), $this->parameters, null, null, $this->queryFactory->getColumnDescriptors(), $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger, $this->queryFactory->hasExcludedColumns()); } else { - $this->innerResultIterator = InnerResultArray::createInnerResultIterator($this->queryFactory->getMagicSql(), $this->parameters, null, null, $this->queryFactory->getColumnDescriptors(), $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger); + $this->innerResultIterator = InnerResultArray::createInnerResultIterator($this->queryFactory->getMagicSql(), $this->parameters, null, null, $this->queryFactory->getColumnDescriptors(), $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->logger, $this->queryFactory->hasExcludedColumns()); } } @@ -199,7 +199,8 @@ public function take($offset, $limit) if ($this->totalCount === 0) { return PageIterator::createEmpyIterator($this); } - return PageIterator::createResultIterator($this, $this->queryFactory->getMagicSql(), $this->parameters, $limit, $offset, $this->queryFactory->getColumnDescriptors(), $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->mode, $this->logger); + // @TODO (gua) check offset < 2 to get full RI + return PageIterator::createResultIterator($this, $this->queryFactory->getMagicSql(), $this->parameters, $limit, $offset, $this->queryFactory->getColumnDescriptors(), $this->objectStorage, $this->className, $this->tdbmService, $this->magicQuery, $this->mode, $this->logger, $this->queryFactory->hasExcludedColumns()); } /** @@ -395,18 +396,25 @@ public function _getSubQuery(): string return $sql; } - protected function addToWhitelist(string $column, string $table) : void + /** + * @param array $columns The columns names to includes + * @param string $table The concerned table, for use in the case of inheritance + * + * @throws TDBMException If the table is not found in the inheritance schema or If the column do not exist in the table + */ + protected function setWhitelist(string $columns, string $table) : void { throw new TDBMException('Table `' . $table . '` not found in inheritance Schema.'); } + /** + * @param string $column The column names to check + * @param string $table The concerned table, for use in the case of inheritance + * + * @throws TDBMException If the table is not found in the inheritance schema or If the column do not exist in the table + */ public function isInWhitelist(string $column, string $table) : bool { throw new TDBMException('Table `' . $table . '` not found in inheritance Schema'); } - - protected function removeFromWhitelist(string $column, string $table) : void - { - throw new TDBMException('Table `' . $table . '` not found in inheritance Schema'); - } } diff --git a/src/Utils/BeanDescriptor.php b/src/Utils/BeanDescriptor.php index 0a1c859b..ac253d93 100644 --- a/src/Utils/BeanDescriptor.php +++ b/src/Utils/BeanDescriptor.php @@ -1182,37 +1182,34 @@ public function generateResultIteratorPhpCode(): ?FileGenerator $whitelistProperty->setDocBlock(new DocBlockGenerator( 'Columns to fetch from db', null, - [new VarTag(null,'array', 'Associative array indexed by columns')] + [new VarTag(null, 'array', 'Associative array indexed by columns')] )); $whitelistProperty->setDefaultValue(new PropertyValueGenerator($columnsWithWhitelist)); $class->addPropertyFromGenerator($whitelistProperty); - $whitelistAddMethod = new MethodGenerator( - 'addToWhitelist', - [new ParameterGenerator('column', 'string'), new ParameterGenerator('table', 'string', $tableName)], + $whitelistSetMethod = new MethodGenerator( + 'setWhitelist', + [new ParameterGenerator('column', 'string') ,new ParameterGenerator('table', 'string', $tableName)], MethodGenerator::FLAG_PROTECTED, <<whitelist[\$column] = true; -} else { - parent::addToWhitelist(\$column, \$table); -} -PHP - ); - $whitelistAddMethod->setReturnType('void'); - $whitelistRemoveMethod = new MethodGenerator( - 'removeFromWhitelist', - [new ParameterGenerator('column', 'string'), new ParameterGenerator('table', 'string', $tableName)], - MethodGenerator::FLAG_PROTECTED, - <<whitelist[\$column] = false; -} else { - parent::removeFromWhitelist(\$column, \$table); + // Set all variable to false + array_walk(\$this->whitelist, function (bool &\$value) { + \$value = false; + }); + // Set wanted columns to true + foreach (\$columns as \$column) { + if (!array_key_exists(\$column, \$this->whitelist)) { + throw new \TheCodingMachine\TDBM\TDBMException('Column `' . \$column . '` does not exist on table `$tableName`'); + } + \$this->whitelist[\$column] = true; + } + return; } +parent::emptyWhitelist(\$table); PHP ); - $whitelistRemoveMethod->setReturnType('void'); + $whitelistSetMethod->setReturnType('void'); $whitelistHasMethod = new MethodGenerator( 'isInWhitelist', [new ParameterGenerator('column', 'string'), new ParameterGenerator('table', 'string', $tableName)], @@ -1225,8 +1222,7 @@ public function generateResultIteratorPhpCode(): ?FileGenerator PHP ); $whitelistHasMethod->setReturnType('bool'); - $class->addMethodFromGenerator($whitelistAddMethod); - $class->addMethodFromGenerator($whitelistRemoveMethod); + $class->addMethodFromGenerator($whitelistSetMethod); $class->addMethodFromGenerator($whitelistHasMethod); $file = $this->codeGeneratorListener->onBaseResultIteratorGenerated($file, $this, $this->configuration); diff --git a/tests/Dao/TestUserDao.php b/tests/Dao/TestUserDao.php index f30a8438..a526dddd 100644 --- a/tests/Dao/TestUserDao.php +++ b/tests/Dao/TestUserDao.php @@ -123,4 +123,9 @@ public function getUsersByComplexFilterBag(CountryBean $country, array $names) $filterBag[] = $country; return $this->find($filterBag); } + + public function findForWhitelist() + { + return $this->tdbmService->findObjects('users', $filter, $parameters, $orderBy, $additionalTablesFetch, $mode, \TheCodingMachine\TDBM\Test\Dao\Bean\UserBean::class, \TheCodingMachine\TDBM\Test\ResultIterator\UserResultIterator::class); + } } diff --git a/tests/TDBMDaoGeneratorTest.php b/tests/TDBMDaoGeneratorTest.php index 1d923365..49cf5b2b 100644 --- a/tests/TDBMDaoGeneratorTest.php +++ b/tests/TDBMDaoGeneratorTest.php @@ -35,8 +35,10 @@ use TheCodingMachine\TDBM\Dao\TestCountryDao; use TheCodingMachine\TDBM\Dao\TestRoleDao; use TheCodingMachine\TDBM\Dao\TestUserDao; +use TheCodingMachine\TDBM\Exception\TDBMPartialQueryException; use TheCodingMachine\TDBM\Fixtures\Interfaces\TestUserDaoInterface; use TheCodingMachine\TDBM\Fixtures\Interfaces\TestUserInterface; +use TheCodingMachine\TDBM\Test\Dao\AccountDao; use TheCodingMachine\TDBM\Test\Dao\AlbumDao; use TheCodingMachine\TDBM\Test\Dao\AllNullableDao; use TheCodingMachine\TDBM\Test\Dao\AnimalDao; @@ -2236,4 +2238,54 @@ public function testSubQueryExceptionOnPrimaryKeysWithMultipleColumns(): void $this->expectExceptionMessage('You cannot use in a sub-query a table that has a primary key on more that 1 column.'); $states->_getSubQuery(); } + + // @TODO: + // - test can't access partially loaded object + // - test can access partially loaded object + // - test can refresh partially loaded object + // - test can refresh partially loaded RI + // What about jsonSerialize ? + // test inheritance + + public function testWhitelist(): void + { + $userDao = new TestUserDao($this->tdbmService); + $users = $userDao->findAll(); + + $this->assertCount(6, $users); + $this->assertEquals('john.smith', $users[0]->getLogin()); + +// $this + + } + + /** + * @depends testDaoGeneration + */ + public function testExceptionOnNotLoadedAccess(): void + { + $fileDao = new FileDao($this->tdbmService); + $this->expectException(TDBMPartialQueryException::class); + $files = $fileDao->findAll(); + // @TODO empty whitelist and add only id + foreach ($files as $file) { + $this->assertIsInt($file->getId()); + $file->getFile(); + } + } + + /** + * @depends testDaoGeneration + */ + public function testCanAccessWhitelistedProperties(): void + { + $accountDao = new AccountDao($this->tdbmService); + $accounts = $accountDao->findAll(); + $accounts->liteWhitelist(); // Empty + id / label + foreach ($accounts as $account) { + $account->getId(); + $account->getName(); + } + $this->assertTrue(true); + } } diff --git a/tests/TDBMServiceTest.php b/tests/TDBMServiceTest.php index 4658ae84..9f53e26a 100644 --- a/tests/TDBMServiceTest.php +++ b/tests/TDBMServiceTest.php @@ -271,7 +271,7 @@ public function testRawSqlOrderCountriesByUserCount(): void SQL; /** @var Test\Dao\Bean\CountryBean[]|\Porpaginas\Result $beans */ - $beans = $this->tdbmService->findObjectsFromRawSql('country', $sql, [], null, Test\Dao\Bean\CountryBean::class, null,CountryResultIterator::class); + $beans = $this->tdbmService->findObjectsFromRawSql('country', $sql, [], null, Test\Dao\Bean\CountryBean::class, null, CountryResultIterator::class); $count = 0; foreach ($beans as $country) {