diff --git a/README.md b/README.md index e99875d..cd28a31 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ use NativeSQLMapperTrait; ```php $ArticlesTable = $this->fetchTable(ArticlesTable::class); -$stmt = $ArticlesTable->prepareSQL(" +$stmt = $ArticlesTable->prepareNativeStatement(" SELECT id AS Articles__id, title AS Articles__title @@ -72,7 +72,7 @@ $stmt = $ArticlesTable->prepareSQL(" "); $stmt->bindValue('title', 'My Article Title'); /** @var \App\Model\Entity\Article[] $entities */ -$entities = $ArticlesTable->fromNativeQuery($stmt)->all(); +$entities = $ArticlesTable->mapNativeStatement($stmt)->all(); ``` `$entities` now contains hydrated `Article` entities based on the SQL result. @@ -82,7 +82,7 @@ $entities = $ArticlesTable->fromNativeQuery($stmt)->all(); ## 🔁 hasMany Example Using Minimalistic SQL ```php -$stmt = $ArticlesTable->prepareSQL(" +$stmt = $ArticlesTable->prepareNativeStatement(" SELECT a.id AS Articles__id, title AS Articles__title, @@ -93,7 +93,7 @@ $stmt = $ArticlesTable->prepareSQL(" LEFT JOIN comments AS c ON a.id=c.article_id "); -$entities = $ArticlesTable->fromNativeQuery($stmt)->all(); +$entities = $ArticlesTable->mapNativeStatement($stmt)->all(); ``` `$entities` now contains an array of Article objects with Comment objects as children. @@ -114,7 +114,7 @@ Notice that `FROM` and `JOIN` clauses may use short or long aliases or no aliase ```php $ArticlesTable = $this->fetchTable(ArticlesTable::class); -$stmt = $ArticlesTable->prepareSQL(" +$stmt = $ArticlesTable->prepareNativeStatement(" SELECT Articles.id AS Articles__id, Articles.title AS Articles__title, @@ -126,7 +126,7 @@ $stmt = $ArticlesTable->prepareSQL(" LEFT JOIN tags AS Tags ON Tags.id=ArticlesTags.tag_id "); -$entities = $ArticlesTable->fromNativeQuery($stmt)->all(); +$entities = $ArticlesTable->mapNativeStatement($stmt)->all(); ``` You can find more examples in tests - https://github.com/bancer/native-sql-mapper/tree/develop/tests/TestCase/ORM. diff --git a/composer.json b/composer.json index 83c4cdb..f7943b9 100644 --- a/composer.json +++ b/composer.json @@ -30,8 +30,17 @@ "minimum-stability": "stable", "scripts": { "all-tests": [ + "echo '------------------------------------------'", + "echo '--- PHPSTAN TESTS ---'", + "echo '------------------------------------------'", "@phpstan", + "echo '------------------------------------------'", + "echo '--- PHPCS TESTS ---'", + "echo '------------------------------------------'", "@phpcs", + "echo '------------------------------------------'", + "echo '--- PHPUNIT TESTS ---'", + "echo '------------------------------------------'", "@phpunit" ], "ci-tests": [ @@ -42,7 +51,8 @@ "phpstan": "vendor/bin/phpstan analyse -c phpstan.neon", "phpcs": "vendor/bin/phpcs --standard=PSR12 -p src tests", "phpunit": "vendor/bin/phpunit tests", - "phpunit-coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit tests --coverage-text" + "phpunit-coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit tests --coverage-text", + "phpunit-coverage-html": "XDEBUG_MODE=coverage vendor/bin/phpunit tests --coverage-html coverage/" }, "config": { "allow-plugins": { diff --git a/src/ORM/NativeQueryResultMapper.php b/src/ORM/NativeQueryResultMapper.php new file mode 100644 index 0000000..7b5f89d --- /dev/null +++ b/src/ORM/NativeQueryResultMapper.php @@ -0,0 +1,142 @@ +|null + */ + protected $mapStrategy = null; + + /** + * Constructor. + * + * @param \Cake\ORM\Table $rootTable The root table instance. + * @param \Cake\Database\StatementInterface $stmt The prepared statement. + */ + public function __construct(Table $rootTable, StatementInterface $stmt) + { + $this->rootTable = $rootTable; + $this->stmt = $stmt; + $this->isExecuted = false; + } + + /** + * Provide a custom mapping strategy instead of relying + * on automatic alias inference. + * + * The structure must match the output of MappingStrategy::toArray(). + * + * @param array $strategy Mapping configuration. + * @return $this + */ + public function setMappingStrategy(array $strategy): self + { + $this->mapStrategy = $strategy; + return $this; + } + + /** + * Execute the SQL statement if not executed yet, fetch all rows, + * build (or use) the mapping strategy, and hydrate the result set + * into entities. + * + * @return \Cake\Datasource\EntityInterface[] Hydrated entity list. + */ + public function all(): array + { + if (!$this->isExecuted) { + $this->stmt->execute(); + $this->isExecuted = true; + } + $rows = $this->stmt->fetchAll(\PDO::FETCH_ASSOC); + if (!$rows) { + return []; + } + $aliasMap = []; + if ($this->mapStrategy === null) { + $aliases = $this->extractAliases($rows); + $strategy = new MappingStrategy($this->rootTable, $aliases); + $this->mapStrategy = $strategy->build()->toArray(); + $aliasMap = $strategy->getAliasMap(); + } + $hydrator = new RecursiveEntityHydrator($this->rootTable, $this->mapStrategy, $aliasMap); + return $hydrator->hydrateMany($rows); + } + + /** + * Extract column aliases used in the SQL result set. + * + * Each column must follow `{Alias}__{column}` format. + * Throws UnknownAliasException if the alias format is invalid. + * + * @param array|mixed> $rows Result set rows. + * @return string[] Sorted list of unique aliases. + * + * @throws \InvalidArgumentException If the first row is not an array. + * @throws \Bancer\NativeQueryMapper\ORM\UnknownAliasException + * If a column does not follow expected alias format. + */ + protected function extractAliases(array $rows): array + { + $firstRow = $rows[0] ?? []; + if (!is_array($firstRow)) { + throw new \InvalidArgumentException('First element of the result set is not an array'); + } + $keys = array_keys($firstRow); + $aliases = []; + foreach ($keys as $key) { + if (!is_string($key) || !str_contains($key, '__')) { + throw new UnknownAliasException("Column '$key' must use an alias in the format {Alias}__$key"); + } + [$alias, $field] = explode('__', $key, 2); + if (mb_strlen($alias) <= 0 || mb_strlen($field) <= 0) { + $message = "Alias '$key' is invalid. Column alias must use {Alias}__{column_name} format"; + throw new UnknownAliasException($message); + } + $aliases[] = $alias; + } + sort($aliases); + return $aliases; + } +} diff --git a/src/ORM/NativeSQLMapperTrait.php b/src/ORM/NativeSQLMapperTrait.php index c37663a..567b91b 100644 --- a/src/ORM/NativeSQLMapperTrait.php +++ b/src/ORM/NativeSQLMapperTrait.php @@ -6,24 +6,57 @@ use Cake\Database\StatementInterface; +/** + * NativeSQLMapperTrait + * + * Provides convenience functions for working with native SQL queries in + * CakePHP Table classes. It allows preparing raw SQL statements using + * the table's connection driver and wrapping executed statements in a + * NativeQueryResultMapper object, enabling automatic entity and association + * mapping based on CakePHP-style column aliases. + */ trait NativeSQLMapperTrait { /** - * Create a StatementQuery wrapper for a prepared statement. + * Wrap a prepared statement in a NativeQueryResultMapper, enabling the + * mapping of native SQL result sets into fully hydrated entities. * - * @param \Cake\Database\StatementInterface $stmt - * @return \Bancer\NativeQueryMapper\ORM\StatementQuery + * Typically used after calling prepareNativeStatement() and binding + * the statement parameters. + * + * Example: + * ```php + * $stmt = $ArticlesTable->prepareNativeStatement(" + * SELECT id AS Articles__id FROM articles + * "); + * $entities = $ArticlesTable->mapNativeStatement($stmt)->all(); + * ``` + * + * @param \Cake\Database\StatementInterface $stmt Prepared statement. + * @return \Bancer\NativeQueryMapper\ORM\NativeQueryResultMapper Wrapper for ORM-level mapping of native results. */ - public function fromNativeQuery(StatementInterface $stmt): StatementQuery + public function mapNativeStatement(StatementInterface $stmt): NativeQueryResultMapper { - return new StatementQuery($this, $stmt); + return new NativeQueryResultMapper($this, $stmt); } /** - * @param string $stmt - * @return \Cake\Database\StatementInterface + * Prepare a native SQL statement using the table's database + * connection driver. This provides direct access to low-level PDO-style + * prepared statements while still using the CakePHP connection. + * + * Example: + * ```php + * $stmt = $ArticlesTable->prepareNativeStatement(" + * SELECT id AS Articles__id FROM articles WHERE title = :title + * "); + * $stmt->bindValue('title', 'Example'); + * ``` + * + * @param string $stmt Raw SQL string to prepare. + * @return \Cake\Database\StatementInterface Prepared statement ready for parameter binding and execution. */ - public function prepareSQL(string $stmt): StatementInterface + public function prepareNativeStatement(string $stmt): StatementInterface { return $this->getConnection()->getDriver()->prepare($stmt); } diff --git a/src/ORM/AutoHydratorRecursive.php b/src/ORM/RecursiveEntityHydrator.php similarity index 74% rename from src/ORM/AutoHydratorRecursive.php rename to src/ORM/RecursiveEntityHydrator.php index ad5bec7..a692c22 100644 --- a/src/ORM/AutoHydratorRecursive.php +++ b/src/ORM/RecursiveEntityHydrator.php @@ -9,11 +9,26 @@ use Cake\Utility\Hash; use RuntimeException; -class AutoHydratorRecursive +/** + * Recursively hydrates nested CakePHP entities from a set of rows produced + * by a native SQL query using Cake-style `{Alias}__{field}` column naming. + * + * This class constructs entity graphs according to a precomputed + * `MappingStrategy`, supports deep associations and belongsToMany relations, + * and caches hydrated entities to avoid duplication during recursion. + */ +class RecursiveEntityHydrator { + /** + * The root Table instance initiating hydration. + * + * @var \Cake\ORM\Table + */ protected Table $rootTable; /** + * Supported association types in the mapping strategy. + * * @var string[] */ protected array $associationTypes = [ @@ -24,46 +39,55 @@ class AutoHydratorRecursive ]; /** - * Precomputed mapping strategy. + * Precomputed mapping strategy produced by MappingStrategy::build()->toArray(). * * @var mixed[] */ protected array $mappingStrategy = []; /** + * Maps each alias to a map of hashed field sets to entity index. + * + * Example: * [ - * '{alias}' => [ - * '{hash}' => {index}, - * ], + * 'Articles' => [ + * 'a1b2c3' => 0, + * 'd4e5f6' => 1, + * ], * ] * - * @var int[][] + * @var array> */ protected array $entitiesMap = []; /** - * The map of aliases and corresponding Table objects. + * A map of aliases to their corresponding Table objects. * - * @var array + * @var array */ protected array $aliasMap = []; /** + * List of hydrated root-level entities. + * * @var \Cake\Datasource\EntityInterface[] */ protected array $entities = []; /** - * If mapping strategy contains hasMany or belongsToMany association then all mapped models must have primary keys. + * Whether the presence of primary keys is mandatory for all entities, + * inferred automatically based on the mapping strategy. * - * @var boolean + * @var bool */ - protected bool $isPrimaryKeyRequired; + private bool $isPrimaryKeyRequired; /** - * @param \Cake\ORM\Table $rootTable - * @param mixed[] $mappingStrategy Mapping strategy. - * @param array $aliasMap Aliases and corresponding Table objects. + * Constructor. + * + * @param \Cake\ORM\Table $rootTable The root Table instance. + * @param mixed[] $mappingStrategy Precomputed mapping strategy. + * @param array $aliasMap Map of aliases to Table objects. */ public function __construct(Table $rootTable, array $mappingStrategy, array $aliasMap) { @@ -73,7 +97,9 @@ public function __construct(Table $rootTable, array $mappingStrategy, array $ali } /** - * @param mixed[][] $rows + * Hydrate an array of rows into a list of fully mapped entities. + * + * @param mixed[][] $rows Flat rows from PDO::FETCH_ASSOC. * @return \Cake\Datasource\EntityInterface[] */ public function hydrateMany(array $rows): array @@ -86,11 +112,13 @@ public function hydrateMany(array $rows): array } /** + * Recursively map aliases to entities and attach them to their parent entities. * - * @param mixed[] $mappingStrategy - * @param mixed[][] $row - * @param \Cake\Datasource\EntityInterface $parent - * @param string $parentAssociation + * @param mixed[] $mappingStrategy Strategy node for the current level. + * @param mixed[][] $row Parsed row grouped by alias. + * @param \Cake\Datasource\EntityInterface|null $parent Parent entity, if any. + * @param string|null $parentAssociation Association type joining child to parent. + * @return void */ protected function map( array $mappingStrategy, @@ -185,10 +213,16 @@ protected function map( } /** - * @param class-string<\Cake\Datasource\EntityInterface> $className Entity class name. - * @param mixed[] $fields Entity fields with values. - * @param string $alias Entity alias. - * @param string[]|string|null $primaryKey The name(s) of the primary key column(s). + * Create an entity from raw field data using either: + * - Table marshaller (preferred), or + * - direct entity instantiation (fallback). + * + * Returns null when the row for the alias is "empty" (all NULL fields). + * + * @param class-string<\Cake\Datasource\EntityInterface> $className Entity class. + * @param mixed[] $fields Raw database fields. + * @param string $alias Alias of the entity. + * @param string[]|string|null $primaryKey Primary key name(s). * @return \Cake\Datasource\EntityInterface|null */ protected function constructEntity( @@ -243,8 +277,11 @@ protected function constructEntity( } /** - * @param mixed[] $fields - * @param string|null $parentEntityHash + * Compute a stable hash for an entity's field set, + * optionally including the parent entity's hash for hasMany relations. + * + * @param mixed[] $fields Raw database fields. + * @param string|null $parentEntityHash The hash of the parent entity object. * @return string */ protected function computeFieldsHash(array $fields, ?string $parentEntityHash = null): string @@ -254,6 +291,8 @@ protected function computeFieldsHash(array $fields, ?string $parentEntityHash = } /** + * Determine if the current mapping-strategy node contains associations. + * * @param mixed[] $node * @return bool */ @@ -264,6 +303,13 @@ protected function hasAssociations(array $node): bool } /** + * Parse rows grouped by `{Alias}__{field}` format into: + * + * [ + * ['Articles' => [...], 'Comments' => [...]], + * ['Articles' => [...], 'Comments' => [...]], + * ] + * * @param mixed[][] $rows * @return mixed[][][] */ @@ -282,8 +328,9 @@ protected function parse(array $rows): array } /** - * Checks whether the mapping strategy requires all primary keys to be present. - * If mapping strategy contains hasMany or belongsToMany association then all mapped models must have primary keys. + * Check if the strategy requires primary keys for ALL mapped entities. + * + * Required when using hasMany or belongsToMany associations. * * @return bool */ diff --git a/src/ORM/StatementQuery.php b/src/ORM/StatementQuery.php deleted file mode 100644 index 83d3aa1..0000000 --- a/src/ORM/StatementQuery.php +++ /dev/null @@ -1,94 +0,0 @@ -rootTable = $rootTable; - $this->stmt = $stmt; - $this->isExecuted = false; - } - - /** - * Provide a custom mapping strategy. - * - * @param mixed[] $strategy - * @return $this - */ - public function mapStrategy(array $strategy): self - { - $this->mapStrategy = $strategy; - return $this; - } - - /** - * Execute and hydrate results. - * - * @return \Cake\Datasource\EntityInterface[] - */ - public function all(): array - { - if (!$this->isExecuted) { - $this->stmt->execute(); - $this->isExecuted = true; - } - $rows = $this->stmt->fetchAll(\PDO::FETCH_ASSOC); - if (!$rows) { - return []; - } - $aliasMap = []; - if ($this->mapStrategy === null) { - $aliases = $this->extractAliases($rows); - $strategy = new MappingStrategy($this->rootTable, $aliases); - $this->mapStrategy = $strategy->build()->toArray(); - $aliasMap = $strategy->getAliasMap(); - } - $hydrator = new AutoHydratorRecursive($this->rootTable, $this->mapStrategy, $aliasMap); - return $hydrator->hydrateMany($rows); - } - - /** - * Extracts aliases of the columns from the query's result set. - * - * @param mixed[] $rows Result set rows. - * @return string[] - */ - protected function extractAliases(array $rows): array - { - $firstRow = $rows[0] ?? []; - if (!is_array($firstRow)) { - throw new \InvalidArgumentException('First element of the result set is not an array'); - } - $keys = array_keys($firstRow); - $aliases = []; - foreach ($keys as $key) { - if (!is_string($key) || !str_contains($key, '__')) { - throw new UnknownAliasException("Column '$key' must use an alias in the format {Alias}__$key"); - } - [$alias, $field] = explode('__', $key, 2); - if (mb_strlen($alias) <= 0 || mb_strlen($field) <= 0) { - $message = "Alias '$key' is invalid. Column alias must use {Alias}__{column_name} format"; - throw new UnknownAliasException($message); - } - $aliases[] = $alias; - } - sort($aliases); - return $aliases; - } -} diff --git a/src/polyfill_str_contains.php b/src/polyfill_str_contains.php index f07b778..1a053e5 100644 --- a/src/polyfill_str_contains.php +++ b/src/polyfill_str_contains.php @@ -1,5 +1,7 @@ expectExceptionMessage($expectedMessage); /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT a.id AS a__id, a.title AS a__title FROM articles AS a "); - $ArticlesTable->fromNativeQuery($stmt)->all(); + $ArticlesTable->mapNativeStatement($stmt)->all(); } public function testMissingAlias(): void @@ -63,13 +63,13 @@ public function testMissingAlias(): void $this->expectExceptionMessage("Column 'title' must use an alias in the format {Alias}__title"); /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT id AS Articles__id, title FROM articles "); - $ArticlesTable->fromNativeQuery($stmt)->all(); + $ArticlesTable->mapNativeStatement($stmt)->all(); } public function testIncompleteAlias(): void @@ -80,13 +80,13 @@ public function testIncompleteAlias(): void ); /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT id AS Articles__id, title AS Articles__ FROM articles "); - $ArticlesTable->fromNativeQuery($stmt)->all(); + $ArticlesTable->mapNativeStatement($stmt)->all(); } public function testUnrecognizedRootAlias(): void @@ -95,13 +95,13 @@ public function testUnrecognizedRootAlias(): void $this->expectExceptionMessage("None of the root table associations match alias 'Books'"); /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT id AS Articles__id, title AS Books__title FROM articles "); - $ArticlesTable->fromNativeQuery($stmt)->all(); + $ArticlesTable->mapNativeStatement($stmt)->all(); } public function testUnrecognizedChildAlias(): void @@ -110,7 +110,7 @@ public function testUnrecognizedChildAlias(): void $this->expectExceptionMessage("None of the table associations match alias 'Books'"); /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT Articles.id AS Articles__id, Articles.title AS Articles__title, @@ -121,14 +121,14 @@ public function testUnrecognizedChildAlias(): void LEFT JOIN comments AS Comments ON Articles.id=Comments.article_id "); - $ArticlesTable->fromNativeQuery($stmt)->all(); + $ArticlesTable->mapNativeStatement($stmt)->all(); } public function testEmptyResultSet(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT Articles.id AS Articles__id, Articles.title AS Articles__title @@ -136,7 +136,7 @@ public function testEmptyResultSet(): void WHERE Articles.title = :title "); $stmt->bindValue('title', 'Non-existing-title'); - $actual = $ArticlesTable->fromNativeQuery($stmt)->all(); + $actual = $ArticlesTable->mapNativeStatement($stmt)->all(); static::assertSame([], $actual); } @@ -144,13 +144,13 @@ public function testSimplestSelect(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT Articles.id AS Articles__id, Articles.title AS Articles__title FROM articles AS Articles "); - $actual = $ArticlesTable->fromNativeQuery($stmt)->all(); + $actual = $ArticlesTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Article::class, $actual[0]); $expected = [ @@ -169,13 +169,13 @@ public function testSimplestSelectMinimalSQL(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT id AS Articles__id, title AS Articles__title FROM articles "); - $actual = $ArticlesTable->fromNativeQuery($stmt)->all(); + $actual = $ArticlesTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Article::class, $actual[0]); $expected = [ @@ -196,7 +196,7 @@ public function testHasManyWithoutIdColumn(): void $this->expectExceptionMessage("'Articles__id' column must be present in the query's SELECT clause"); /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT Articles.title AS Articles__title, Comments.id AS Comments__id, @@ -206,14 +206,14 @@ public function testHasManyWithoutIdColumn(): void LEFT JOIN comments AS Comments ON Articles.id=Comments.article_id "); - $ArticlesTable->fromNativeQuery($stmt)->all(); + $ArticlesTable->mapNativeStatement($stmt)->all(); } public function testHasMany(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT Articles.id AS Articles__id, Articles.title AS Articles__title, @@ -224,7 +224,7 @@ public function testHasMany(): void LEFT JOIN comments AS Comments ON Articles.id=Comments.article_id "); - $actual = $ArticlesTable->fromNativeQuery($stmt)->all(); + $actual = $ArticlesTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Article::class, $actual[0]); $actualComments = $actual[0]->get('comments'); @@ -264,7 +264,7 @@ public function testHasManyMinimalSQL(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT a.id AS Articles__id, title AS Articles__title, @@ -275,7 +275,7 @@ public function testHasManyMinimalSQL(): void LEFT JOIN comments AS c ON a.id=c.article_id "); - $actual = $ArticlesTable->fromNativeQuery($stmt)->all(); + $actual = $ArticlesTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Article::class, $actual[0]); $actualComments = $actual[0]->get('comments'); @@ -315,7 +315,7 @@ public function testBelongsTo(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\CommentsTable $CommentsTable */ $CommentsTable = $this->fetchTable(CommentsTable::class); - $stmt = $CommentsTable->prepareSQL(" + $stmt = $CommentsTable->prepareNativeStatement(" SELECT Comments.id AS Comments__id, Comments.article_id AS Comments__article_id, @@ -326,7 +326,7 @@ public function testBelongsTo(): void LEFT JOIN articles AS Articles ON Articles.id=Comments.article_id "); - $actual = $CommentsTable->fromNativeQuery($stmt)->all(); + $actual = $CommentsTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Comment::class, $actual[0]); static::assertInstanceOf(Article::class, $actual[0]->get('article')); @@ -356,7 +356,7 @@ public function testBelongsToWithoutIdColumns(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\CommentsTable $CommentsTable */ $CommentsTable = $this->fetchTable(CommentsTable::class); - $stmt = $CommentsTable->prepareSQL(" + $stmt = $CommentsTable->prepareNativeStatement(" SELECT article_id AS Comments__article_id, content AS Comments__content, @@ -365,7 +365,7 @@ public function testBelongsToWithoutIdColumns(): void LEFT JOIN articles ON articles.id=comments.article_id "); - $actual = $CommentsTable->fromNativeQuery($stmt)->all(); + $actual = $CommentsTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Comment::class, $actual[0]); static::assertInstanceOf(Article::class, $actual[0]->get('article')); @@ -393,7 +393,7 @@ public function testBelongsToMinimalSQL(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\CommentsTable $CommentsTable */ $CommentsTable = $this->fetchTable(CommentsTable::class); - $stmt = $CommentsTable->prepareSQL(" + $stmt = $CommentsTable->prepareNativeStatement(" SELECT c.id AS Comments__id, article_id AS Comments__article_id, @@ -404,7 +404,7 @@ public function testBelongsToMinimalSQL(): void LEFT JOIN articles AS a ON a.id=c.article_id "); - $actual = $CommentsTable->fromNativeQuery($stmt)->all(); + $actual = $CommentsTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Comment::class, $actual[0]); static::assertInstanceOf(Article::class, $actual[0]->get('article')); @@ -434,7 +434,7 @@ public function testHasOne(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\UsersTable $UsersTable */ $UsersTable = $this->fetchTable(UsersTable::class); - $stmt = $UsersTable->prepareSQL(" + $stmt = $UsersTable->prepareNativeStatement(" SELECT Users.id AS Users__id, Users.username AS Users__username, @@ -445,7 +445,7 @@ public function testHasOne(): void LEFT JOIN profiles AS Profiles ON Users.id=Profiles.user_id "); - $actual = $UsersTable->fromNativeQuery($stmt)->all(); + $actual = $UsersTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(User::class, $actual[0]); static::assertInstanceOf(Profile::class, $actual[0]->get('profile')); @@ -476,7 +476,7 @@ public function testHasOneMinimalSQL(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\UsersTable $UsersTable */ $UsersTable = $this->fetchTable(UsersTable::class); - $stmt = $UsersTable->prepareSQL(" + $stmt = $UsersTable->prepareNativeStatement(" SELECT u.id AS Users__id, username AS Users__username, @@ -487,7 +487,7 @@ public function testHasOneMinimalSQL(): void LEFT JOIN profiles AS p ON u.id=p.user_id "); - $actual = $UsersTable->fromNativeQuery($stmt)->all(); + $actual = $UsersTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(User::class, $actual[0]); static::assertInstanceOf(Profile::class, $actual[0]->get('profile')); @@ -518,7 +518,7 @@ public function testBelongsToManySimple(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT Articles.id AS Articles__id, Articles.title AS Articles__title, @@ -530,7 +530,7 @@ public function testBelongsToManySimple(): void LEFT JOIN tags AS Tags ON Tags.id=ArticlesTags.tag_id "); - $actual = $ArticlesTable->fromNativeQuery($stmt)->all(); + $actual = $ArticlesTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Article::class, $actual[0]); $actualTags = $actual[0]->get('tags'); @@ -569,7 +569,7 @@ public function testBelongsToManySimpleMinimalSQL(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT a.id AS Articles__id, title AS Articles__title, @@ -581,7 +581,7 @@ public function testBelongsToManySimpleMinimalSQL(): void LEFT JOIN tags AS t ON t.id=at.tag_id "); - $actual = $ArticlesTable->fromNativeQuery($stmt)->all(); + $actual = $ArticlesTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Article::class, $actual[0]); $actualTags = $actual[0]->get('tags'); @@ -620,7 +620,7 @@ public function testBelongsToManyFetchJoinTable(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT Articles.id AS Articles__id, Articles.title AS Articles__title, @@ -635,7 +635,7 @@ public function testBelongsToManyFetchJoinTable(): void LEFT JOIN tags AS Tags ON Tags.id=ArticlesTags.tag_id "); - $actual = $ArticlesTable->fromNativeQuery($stmt)->all(); + $actual = $ArticlesTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Article::class, $actual[0]); $actualTags = $actual[0]->get('tags'); @@ -687,7 +687,7 @@ public function testBelongsToManyFetchJoinTableMinimalSQL(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\ArticlesTable $ArticlesTable */ $ArticlesTable = $this->fetchTable(ArticlesTable::class); - $stmt = $ArticlesTable->prepareSQL(" + $stmt = $ArticlesTable->prepareNativeStatement(" SELECT a.id AS Articles__id, title AS Articles__title, @@ -702,7 +702,7 @@ public function testBelongsToManyFetchJoinTableMinimalSQL(): void LEFT JOIN tags AS t ON t.id=at.tag_id "); - $actual = $ArticlesTable->fromNativeQuery($stmt)->all(); + $actual = $ArticlesTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Article::class, $actual[0]); $actualTags = $actual[0]->get('tags'); @@ -754,7 +754,7 @@ public function testDeepAssociations(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\CountriesTable $CountriesTable */ $CountriesTable = $this->fetchTable(CountriesTable::class); - $stmt = $CountriesTable->prepareSQL(" + $stmt = $CountriesTable->prepareNativeStatement(" SELECT Countries.id AS Countries__id, Countries.name AS Countries__name, @@ -783,7 +783,7 @@ public function testDeepAssociations(): void LEFT JOIN comments AS Comments ON Comments.article_id=Articles.id "); - $actual = $CountriesTable->fromNativeQuery($stmt)->all(); + $actual = $CountriesTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Country::class, $actual[0]); $actualUsers = $actual[0]->get('users'); @@ -857,7 +857,7 @@ public function testDeepAssociationsMinimalSQL(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\CountriesTable $CountriesTable */ $CountriesTable = $this->fetchTable(CountriesTable::class); - $stmt = $CountriesTable->prepareSQL(" + $stmt = $CountriesTable->prepareNativeStatement(" SELECT c.id AS Countries__id, c.name AS Countries__name, @@ -886,7 +886,7 @@ public function testDeepAssociationsMinimalSQL(): void LEFT JOIN comments AS cm ON cm.article_id=a.id "); - $actual = $CountriesTable->fromNativeQuery($stmt)->all(); + $actual = $CountriesTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Country::class, $actual[0]); $actualUsers = $actual[0]->get('users'); @@ -960,7 +960,7 @@ public function testDeepAssociationsWithBelongsToMany(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\CountriesTable $CountriesTable */ $CountriesTable = $this->fetchTable(CountriesTable::class); - $stmt = $CountriesTable->prepareSQL(" + $stmt = $CountriesTable->prepareNativeStatement(" SELECT Countries.id AS Countries__id, Countries.name AS Countries__name, @@ -991,7 +991,7 @@ public function testDeepAssociationsWithBelongsToMany(): void LEFT JOIN comments AS Comments ON Comments.article_id=Articles.id "); - $actual = $CountriesTable->fromNativeQuery($stmt)->all(); + $actual = $CountriesTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Country::class, $actual[0]); $actualUsers = $actual[0]->get('users'); @@ -1095,7 +1095,7 @@ public function testDeepAssociationsWithBelongsToManyMinimalSQL(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\CountriesTable $CountriesTable */ $CountriesTable = $this->fetchTable(CountriesTable::class); - $stmt = $CountriesTable->prepareSQL(" + $stmt = $CountriesTable->prepareNativeStatement(" SELECT c.id AS Countries__id, c.name AS Countries__name, @@ -1126,7 +1126,7 @@ public function testDeepAssociationsWithBelongsToManyMinimalSQL(): void LEFT JOIN comments AS cm ON cm.article_id=a.id "); - $actual = $CountriesTable->fromNativeQuery($stmt)->all(); + $actual = $CountriesTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Country::class, $actual[0]); $actualUsers = $actual[0]->get('users'); @@ -1230,14 +1230,14 @@ public function testDatetimeFields(): void { /** @var \Bancer\NativeQueryMapperTest\TestApp\Model\Table\CommentsTable $CommentsTable */ $CommentsTable = $this->fetchTable(CommentsTable::class); - $stmt = $CommentsTable->prepareSQL(" + $stmt = $CommentsTable->prepareNativeStatement(" SELECT id AS Comments__id, content AS Comments__content, created AS Comments__created FROM comments "); - $actual = $CommentsTable->fromNativeQuery($stmt)->all(); + $actual = $CommentsTable->mapNativeStatement($stmt)->all(); static::assertCount(5, $actual); static::assertInstanceOf(Comment::class, $actual[0]); $expected = [