diff --git a/composer.lock b/composer.lock index 998712bb4..ba10c5f07 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ce8a55f102310b9f9fcd4e3e9007b668", + "content-hash": "682e90f2c192330895122fe20f5988d4", "packages": [ { "name": "brick/varexporter", @@ -201,16 +201,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -253,9 +253,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "psr/container", @@ -1139,11 +1139,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.32", + "version": "2.1.33", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227", - "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9e800e6bee7d5bd02784d4c6069b48032d16224f", + "reference": "9e800e6bee7d5bd02784d4c6069b48032d16224f", "shasum": "" }, "require": { @@ -1188,20 +1188,20 @@ "type": "github" } ], - "time": "2025-11-11T15:18:17+00:00" + "time": "2025-12-05T10:24:31+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "2.0.8", + "version": "2.0.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "2fe9fbeceaf76dd1ebaa7bbbb25e2fb5e59db2fe" + "reference": "5e30669bef866eff70db8b58d72a5c185aa82414" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/2fe9fbeceaf76dd1ebaa7bbbb25e2fb5e59db2fe", - "reference": "2fe9fbeceaf76dd1ebaa7bbbb25e2fb5e59db2fe", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/5e30669bef866eff70db8b58d72a5c185aa82414", + "reference": "5e30669bef866eff70db8b58d72a5c185aa82414", "shasum": "" }, "require": { @@ -1239,41 +1239,41 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.8" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.11" }, - "time": "2025-11-11T07:55:22+00:00" + "time": "2025-12-19T09:05:35+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.11", + "version": "11.0.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", - "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.4.0", + "nikic/php-parser": "^5.7.0", "php": ">=8.2", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-text-template": "^4.0.1", "sebastian/code-unit-reverse-lookup": "^4.0.1", "sebastian/complexity": "^4.0.1", - "sebastian/environment": "^7.2.0", + "sebastian/environment": "^7.2.1", "sebastian/lines-of-code": "^3.0.1", "sebastian/version": "^5.0.2", - "theseer/tokenizer": "^1.2.3" + "theseer/tokenizer": "^1.3.1" }, "require-dev": { - "phpunit/phpunit": "^11.5.2" + "phpunit/phpunit": "^11.5.46" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -1311,7 +1311,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" }, "funding": [ { @@ -1331,7 +1331,7 @@ "type": "tidelift" } ], - "time": "2025-08-27T14:37:49+00:00" + "time": "2025-12-24T07:01:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1580,16 +1580,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.44", + "version": "11.5.46", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c346885c95423eda3f65d85a194aaa24873cda82" + "reference": "75dfe79a2aa30085b7132bb84377c24062193f33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c346885c95423eda3f65d85a194aaa24873cda82", - "reference": "c346885c95423eda3f65d85a194aaa24873cda82", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/75dfe79a2aa30085b7132bb84377c24062193f33", + "reference": "75dfe79a2aa30085b7132bb84377c24062193f33", "shasum": "" }, "require": { @@ -1661,7 +1661,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.44" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.46" }, "funding": [ { @@ -1685,7 +1685,7 @@ "type": "tidelift" } ], - "time": "2025-11-13T07:17:35+00:00" + "time": "2025-12-06T08:01:15+00:00" }, { "name": "psr/cache", @@ -1788,21 +1788,21 @@ }, { "name": "rector/rector", - "version": "2.2.8", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "303aa811649ccd1d32e51e62d5c85949d01b5f1b" + "reference": "f7166355dcf47482f27be59169b0825995f51c7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/303aa811649ccd1d32e51e62d5c85949d01b5f1b", - "reference": "303aa811649ccd1d32e51e62d5c85949d01b5f1b", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/f7166355dcf47482f27be59169b0825995f51c7d", + "reference": "f7166355dcf47482f27be59169b0825995f51c7d", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.32" + "phpstan/phpstan": "^2.1.33" }, "conflict": { "rector/rector-doctrine": "*", @@ -1836,7 +1836,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.2.8" + "source": "https://github.com/rectorphp/rector/tree/2.3.0" }, "funding": [ { @@ -1844,7 +1844,7 @@ "type": "github" } ], - "time": "2025-11-12T18:38:00+00:00" + "time": "2025-12-25T22:00:18+00:00" }, { "name": "sebastian/cli-parser", @@ -3094,16 +3094,16 @@ }, { "name": "symfony/console", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8" + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", - "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", + "url": "https://api.github.com/repos/symfony/console/zipball/732a9ca6cd9dfd940c639062d5edbde2f6727fb6", + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6", "shasum": "" }, "require": { @@ -3168,7 +3168,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.0" + "source": "https://github.com/symfony/console/tree/v7.4.3" }, "funding": [ { @@ -3188,7 +3188,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-23T14:50:43+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3329,16 +3329,16 @@ }, { "name": "symfony/finder", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" + "reference": "fffe05569336549b20a1be64250b40516d6e8d06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", - "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", + "url": "https://api.github.com/repos/symfony/finder/zipball/fffe05569336549b20a1be64250b40516d6e8d06", + "reference": "fffe05569336549b20a1be64250b40516d6e8d06", "shasum": "" }, "require": { @@ -3373,7 +3373,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.0" + "source": "https://github.com/symfony/finder/tree/v7.4.3" }, "funding": [ { @@ -3393,7 +3393,7 @@ "type": "tidelift" } ], - "time": "2025-11-05T05:42:40+00:00" + "time": "2025-12-23T14:50:43+00:00" }, { "name": "symfony/options-resolver", @@ -3803,16 +3803,16 @@ }, { "name": "symfony/process", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" + "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", - "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "url": "https://api.github.com/repos/symfony/process/zipball/2f8e1a6cdf590ca63715da4d3a7a3327404a523f", + "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f", "shasum": "" }, "require": { @@ -3844,7 +3844,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.0" + "source": "https://github.com/symfony/process/tree/v7.4.3" }, "funding": [ { @@ -3864,7 +3864,7 @@ "type": "tidelift" } ], - "time": "2025-10-16T11:21:06+00:00" + "time": "2025-12-19T10:00:43+00:00" }, { "name": "symfony/service-contracts", @@ -4269,5 +4269,5 @@ "platform-overrides": { "php": "8.2.99" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 93677f8fe..7a16d0e2d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,7 +16,6 @@ ./test/unit/Adapter/Driver/Pdo/ConnectionIntegrationTest.php ./test/unit/Adapter/Driver/Pdo/StatementIntegrationTest.php ./test/unit/Adapter/AdapterAwareTraitTest.php - ./test/unit/TableGateway ./test/integration diff --git a/src/Feature/FeatureInterface.php b/src/Feature/FeatureInterface.php new file mode 100644 index 000000000..5d8200dce --- /dev/null +++ b/src/Feature/FeatureInterface.php @@ -0,0 +1,13 @@ + */ + public function getMagicMethodSpecifications(): array; +} diff --git a/src/ResultSet/AbstractResultSet.php b/src/ResultSet/AbstractResultSet.php index 6a6ed52fd..cf2791699 100644 --- a/src/ResultSet/AbstractResultSet.php +++ b/src/ResultSet/AbstractResultSet.php @@ -5,7 +5,6 @@ namespace PhpDb\ResultSet; use ArrayIterator; -use ArrayObject; use Countable; use Exception; use Iterator; @@ -126,7 +125,7 @@ public function getDataSource(): ResultInterface|IteratorAggregate|Iterator|null * Retrieve count of fields in individual rows of the result set */ #[Override] - public function getFieldCount(): mixed + public function getFieldCount(): int { if (null !== $this->fieldCount) { return $this->fieldCount; @@ -289,14 +288,4 @@ public function toArray(): array return $return; } - - /** - * Set the row object prototype - */ - abstract public function setRowPrototype(ArrayObject $rowPrototype): ResultSetInterface; - - /** - * Get the row object prototype - */ - abstract public function getRowPrototype(): ?object; } diff --git a/src/ResultSet/Exception/ExceptionInterface.php b/src/ResultSet/Exception/ExceptionInterface.php index dc635ed13..99af05fb7 100644 --- a/src/ResultSet/Exception/ExceptionInterface.php +++ b/src/ResultSet/Exception/ExceptionInterface.php @@ -1,5 +1,7 @@ returnType)) { $this->returnType = ResultSetReturnType::from($this->returnType); @@ -27,17 +30,18 @@ public function __construct( /** {@inheritDoc} */ #[Override] - public function setRowPrototype(ArrayObject $rowPrototype): ResultSetInterface + public function setRowPrototype(ArrayObject|RowPrototypeInterface $rowPrototype): ResultSetInterface { $this->rowPrototype = $rowPrototype; + return $this; } /** {@inheritDoc} */ #[Override] - public function getRowPrototype(): ArrayObject + public function getRowPrototype(): ArrayObject|RowPrototypeInterface { - return $this->rowPrototype ??= new ArrayObject([], ArrayObject::ARRAY_AS_PROPS); + return $this->rowPrototype; } /** @@ -52,13 +56,14 @@ public function getReturnType(): ResultSetReturnType * Iterator: get current item */ #[Override] - public function current(): array|ArrayObject|null + public function current(): array|ArrayObject|RowPrototypeInterface|null { $data = parent::current(); if ($this->returnType === ResultSetReturnType::ArrayObject && is_array($data)) { $ao = clone $this->getRowPrototype(); $ao->exchangeArray($data); + return $ao; } @@ -68,17 +73,17 @@ public function current(): array|ArrayObject|null /** * Set the row object prototype * - * @deprecated use setObjectPrototype() + * @deprecated use setRowPrototype() */ - public function setArrayObjectPrototype(ArrayObject $arrayObjectPrototype): ResultSetInterface + public function setArrayObjectPrototype(ArrayObject|RowPrototypeInterface $arrayObjectPrototype): ResultSetInterface { return $this->setRowPrototype($arrayObjectPrototype); } /** - * @deprecated use getObjectPrototype() + * @deprecated use getRowPrototype() */ - public function getArrayObjectPrototype(): ArrayObject + public function getArrayObjectPrototype(): ArrayObject|RowPrototypeInterface { return $this->getRowPrototype(); } diff --git a/src/ResultSet/ResultSetInterface.php b/src/ResultSet/ResultSetInterface.php index a4152014c..6e143053f 100644 --- a/src/ResultSet/ResultSetInterface.php +++ b/src/ResultSet/ResultSetInterface.php @@ -20,14 +20,14 @@ public function initialize(iterable $dataSource): ResultSetInterface; * from the database might be a column, and/or the result of an * operation or intersection of some data */ - public function getFieldCount(): mixed; + public function getFieldCount(): int; /** * Set the row object prototype * * @throws Exception\InvalidArgumentException */ - public function setRowPrototype(ArrayObject $rowPrototype): ResultSetInterface; + public function setRowPrototype(ArrayObject|RowPrototypeInterface $rowPrototype): ResultSetInterface; /** * Get the row object prototype diff --git a/src/ResultSet/RowPrototypeInterface.php b/src/ResultSet/RowPrototypeInterface.php new file mode 100644 index 000000000..517628d1e --- /dev/null +++ b/src/ResultSet/RowPrototypeInterface.php @@ -0,0 +1,20 @@ +featureSet->setRowGateway($this); $this->featureSet->apply('preInitialize', []); - if (! is_string($this->table) && ! $this->table instanceof TableIdentifier) { + if ($this->table === null) { throw new Exception\RuntimeException('This row object does not have a valid table set.'); } if ($this->primaryKeyColumn === null) { throw new Exception\RuntimeException('This row object does not have a primary key column set.'); - } elseif (is_string($this->primaryKeyColumn)) { - $this->primaryKeyColumn = (array) $this->primaryKeyColumn; } - if (! $this->sql instanceof Sql) { + if ($this->sql === null) { throw new Exception\RuntimeException('This row object does not have a Sql object set.'); } @@ -76,7 +65,6 @@ public function initialize(): void /** * Populate Data - * todo: Refactor to a standard ArrayObject implementation - remove fluent interface */ public function populate(array $rowData, bool $rowExistsInDatabase = false): RowGatewayInterface { @@ -85,20 +73,26 @@ public function populate(array $rowData, bool $rowExistsInDatabase = false): Row $this->data = $rowData; if ($rowExistsInDatabase === true) { $this->processPrimaryKeyData(); - return $this; + } else { + $this->primaryKeyData = null; } - $this->primaryKeyData = null; - return $this; } /** - * todo: Refactor to a standard ArrayObject implementation - remove proxy to populate + * docs: Behaviour has changed - this no longer returns RowGatewayInterface but + * instead an array of the old data as per original PHP spec. + * + * @return array */ - public function exchangeArray(array $array): RowGatewayInterface + public function exchangeArray(array $array): array { - return $this->populate($array, true); + $oldData = $this->data; + + $this->populate($array, true); + + return $oldData; } #[Override] @@ -106,18 +100,16 @@ public function save(): int { $this->initialize(); - if ($this->rowExistsInDatabase()) { - // UPDATE + $rowsAffected = 0; + if ($this->rowExistsInDatabase()) { $data = $this->data; $where = []; $isPkModified = false; - // primary key is always an array even if its a single column foreach ($this->primaryKeyColumn as $pkColumn) { $where[$pkColumn] = $this->primaryKeyData[$pkColumn]; - // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator - if ($data[$pkColumn] == $this->primaryKeyData[$pkColumn]) { + if ($data[$pkColumn] === $this->primaryKeyData[$pkColumn]) { unset($data[$pkColumn]); } else { $isPkModified = true; @@ -127,51 +119,42 @@ public function save(): int $statement = $this->sql->prepareStatementForSqlObject($this->sql->update()->set($data)->where($where)); $result = $statement->execute(); $rowsAffected = $result->getAffectedRows(); - unset($statement, $result); // cleanup + unset($statement, $result); - // If one or more primary keys are modified, we update the where clause if ($isPkModified) { foreach ($this->primaryKeyColumn as $pkColumn) { - // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator if ($data[$pkColumn] !== $this->primaryKeyData[$pkColumn]) { $where[$pkColumn] = $data[$pkColumn]; } } } } else { - // INSERT $insert = $this->sql->insert(); $insert->values($this->data); $statement = $this->sql->prepareStatementForSqlObject($insert); - - $result = $statement->execute(); + $result = $statement->execute(); if (($primaryKeyValue = $result->getGeneratedValue()) && count($this->primaryKeyColumn) === 1) { $this->primaryKeyData = [$this->primaryKeyColumn[0] => $primaryKeyValue]; } else { - // make primary key data available so that $where can be complete $this->processPrimaryKeyData(); } $rowsAffected = $result->getAffectedRows(); - unset($statement, $result); // cleanup + unset($statement, $result); $where = []; - // primary key is always an array even if its a single column foreach ($this->primaryKeyColumn as $pkColumn) { $where[$pkColumn] = $this->primaryKeyData[$pkColumn]; } } - // refresh data $statement = $this->sql->prepareStatementForSqlObject($this->sql->select()->where($where)); $result = $statement->execute(); $rowData = $result->current(); - unset($statement, $result); // cleanup + unset($statement, $result); - // make sure data and original data are in sync after save $this->populate($rowData, true); - // return rows affected return $rowsAffected; } @@ -181,34 +164,32 @@ public function delete(): int $this->initialize(); $where = []; - // primary key is always an array even if its a single column foreach ($this->primaryKeyColumn as $pkColumn) { $where[$pkColumn] = $this->primaryKeyData[$pkColumn] ?? null; } // @todo determine if we need to do a select to ensure 1 row will be affected - $statement = $this->sql->prepareStatementForSqlObject($this->sql->delete()->where($where)); - $result = $statement->execute(); + $rowsAffected = 0; + $statement = $this->sql->prepareStatementForSqlObject($this->sql->delete()->where($where)); + $result = $statement->execute(); - $affectedRows = $result->getAffectedRows(); - if ($affectedRows === 1) { - // detach from database + $rowsAffected = $result->getAffectedRows(); + if ($rowsAffected === 1) { $this->primaryKeyData = null; } - return $affectedRows; + return $rowsAffected; } /** * Offset Exists * - * @param string $offset - * @return bool + * @param string $offset */ #[Override] #[ReturnTypeWillChange] - public function offsetExists($offset) + public function offsetExists($offset): bool { return array_key_exists($offset, $this->data); } @@ -216,12 +197,11 @@ public function offsetExists($offset) /** * Offset get * - * @param string $offset - * @return mixed + * @param string $offset */ #[Override] #[ReturnTypeWillChange] - public function offsetGet($offset) + public function offsetGet($offset): mixed { return $this->data[$offset]; } @@ -229,38 +209,34 @@ public function offsetGet($offset) /** * Offset set * - * @param string $offset - * @param mixed $value - * @return $this Provides a fluent interface + * @param string $offset */ #[Override] #[ReturnTypeWillChange] - public function offsetSet($offset, $value) + public function offsetSet($offset, mixed $value): static { $this->data[$offset] = $value; + return $this; } /** * Offset unset * - * @param string $offset - * @return $this Provides a fluent interface + * @param string $offset */ #[Override] #[ReturnTypeWillChange] - public function offsetUnset($offset) + public function offsetUnset($offset): static { $this->data[$offset] = null; + return $this; } - /** - * @return int - */ #[Override] #[ReturnTypeWillChange] - public function count() + public function count(): int { return count($this->data); } diff --git a/src/RowGateway/Exception/ExceptionInterface.php b/src/RowGateway/Exception/ExceptionInterface.php index b7b81b4e6..eb68e4fe4 100644 --- a/src/RowGateway/Exception/ExceptionInterface.php +++ b/src/RowGateway/Exception/ExceptionInterface.php @@ -1,5 +1,7 @@ rowGateway = $rowGateway; } @@ -35,10 +32,8 @@ public function initialize(): void throw new Exception\RuntimeException('This method is not intended to be called on this object.'); } - /** - * @return array - */ - public function getMagicMethodSpecifications() + /** @return array */ + public function getMagicMethodSpecifications(): array { return []; } diff --git a/src/RowGateway/Feature/FeatureInterface.php b/src/RowGateway/Feature/FeatureInterface.php new file mode 100644 index 000000000..d69313342 --- /dev/null +++ b/src/RowGateway/Feature/FeatureInterface.php @@ -0,0 +1,13 @@ +rowGateway = $rowGateway; foreach ($this->features as $feature) { @@ -39,13 +38,9 @@ public function setRowGateway(AbstractRowGateway $rowGateway) return $this; } - /** - * @param string $featureClassName - * @return AbstractFeature - */ - public function getFeatureByClassName($featureClassName) + public function getFeatureByClassName(string $featureClassName): ?FeatureInterface { - $feature = false; + $feature = null; foreach ($this->features as $potentialFeature) { if ($potentialFeature instanceof $featureClassName) { $feature = $potentialFeature; @@ -55,10 +50,7 @@ public function getFeatureByClassName($featureClassName) return $feature; } - /** - * @return $this Provides a fluent interface - */ - public function addFeatures(array $features) + public function addFeatures(array $features): static { foreach ($features as $feature) { $this->addFeature($feature); @@ -66,26 +58,20 @@ public function addFeatures(array $features) return $this; } - /** - * @return $this Provides a fluent interface - */ - public function addFeature(AbstractFeature $feature) + public function addFeature(FeatureInterface $feature): static { $this->features[] = $feature; - $feature->setRowGateway($feature); + if ($this->rowGateway !== null) { + $feature->setRowGateway($this->rowGateway); + } return $this; } - /** - * @param string $method - * @param array $args - * @return void - */ - public function apply($method, $args) + public function apply(string $method, array $args): void { foreach ($this->features as $feature) { if (method_exists($feature, $method)) { - $return = call_user_func_array([$feature, $method], $args); + $return = $feature->$method(...$args); if ($return === self::APPLY_HALT) { break; } @@ -93,58 +79,32 @@ public function apply($method, $args) } } - /** - * @param string $property - * @return bool - */ - public function canCallMagicGet($property) + public function canCallMagicGet(string $property): false { return false; } - /** - * @param string $property - * @return mixed - */ - public function callMagicGet($property) + public function callMagicGet(string $property): mixed { return null; } - /** - * @param string $property - * @return bool - */ - public function canCallMagicSet($property) + public function canCallMagicSet(string $property): false { return false; } - /** - * @param string $property - * @param mixed $value - * @return mixed - */ - public function callMagicSet($property, $value) + public function callMagicSet(string $property, mixed $value): mixed { return null; } - /** - * @param string $method - * @return bool - */ - public function canCallMagicCall($method) + public function canCallMagicCall(string $method): bool { return false; } - /** - * @param string $method - * @param array $arguments - * @return mixed - */ - public function callMagicCall($method, $arguments) + public function callMagicCall(string $method, array $arguments): mixed { return null; } diff --git a/src/RowGateway/RowGateway.php b/src/RowGateway/RowGateway.php index 1393cf5d4..75ba32a6f 100644 --- a/src/RowGateway/RowGateway.php +++ b/src/RowGateway/RowGateway.php @@ -1,25 +1,32 @@ primaryKeyColumn = empty($primaryKeyColumn) ? null : (array) $primaryKeyColumn; + if (is_string($primaryKeyColumn)) { + $primaryKeyColumn = $primaryKeyColumn !== '' ? (array) $primaryKeyColumn : null; + } + $this->primaryKeyColumn = $primaryKeyColumn; // set table $this->table = $table; @@ -27,10 +34,8 @@ public function __construct($primaryKeyColumn, $table, $adapterOrSql = null) // set Sql object if ($adapterOrSql instanceof Sql) { $this->sql = $adapterOrSql; - } elseif ($adapterOrSql instanceof AdapterInterface) { - $this->sql = new Sql($adapterOrSql, $this->table); } else { - throw new Exception\InvalidArgumentException('A valid Sql object was not provided.'); + $this->sql = new Sql($adapterOrSql, $this->table); } if ($this->sql->getTable() !== $this->table) { diff --git a/src/RowGateway/RowGatewayInterface.php b/src/RowGateway/RowGatewayInterface.php index c35782467..70788aa48 100644 --- a/src/RowGateway/RowGatewayInterface.php +++ b/src/RowGateway/RowGatewayInterface.php @@ -4,7 +4,9 @@ namespace PhpDb\RowGateway; -interface RowGatewayInterface +use PhpDb\ResultSet\RowPrototypeInterface; + +interface RowGatewayInterface extends RowPrototypeInterface { public function save(): int; diff --git a/src/TableGateway/AbstractTableGateway.php b/src/TableGateway/AbstractTableGateway.php index 39668ae49..63bac1ecb 100644 --- a/src/TableGateway/AbstractTableGateway.php +++ b/src/TableGateway/AbstractTableGateway.php @@ -25,10 +25,8 @@ use function end; use function is_array; use function is_object; -use function is_string; use function reset; use function sprintf; -use function strtolower; /** * @property AdapterInterface $adapter @@ -37,27 +35,21 @@ */ abstract class AbstractTableGateway implements TableGatewayInterface { - /** @var bool */ - protected $isInitialized = false; + protected bool $isInitialized = false; - /** @var AdapterInterface */ - protected $adapter; + protected ?AdapterInterface $adapter = null; - /** @var string|array|TableIdentifier */ - protected $table; + protected TableIdentifier|string|array|null $table = null; - /** @var array */ - protected $columns = []; + protected array $columns = []; - protected Feature\FeatureSet $featureSet; + protected ?Feature\FeatureSet $featureSet = null; - protected ?ResultSetInterface $resultSetPrototype; + protected ?ResultSetInterface $resultSetPrototype = null; - /** @var Sql */ - protected $sql; + protected ?Sql $sql = null; - /** @var int */ - protected $lastInsertValue; + protected ?int $lastInsertValue = null; public function isInitialized(): bool { @@ -75,27 +67,26 @@ public function initialize(): void return; } - /** @phpstan-ignore instanceof.alwaysTrue */ - if (! $this->featureSet instanceof Feature\FeatureSet) { + if ($this->featureSet === null) { $this->featureSet = new Feature\FeatureSet(); } $this->featureSet->setTableGateway($this); $this->featureSet->apply(EventFeatureEventsInterface::EVENT_PRE_INITIALIZE, []); - if (! $this->adapter instanceof AdapterInterface) { + if ($this->adapter === null) { throw new Exception\RuntimeException('This table does not have an Adapter setup'); } - if (! is_string($this->table) && ! $this->table instanceof TableIdentifier && ! is_array($this->table)) { + if ($this->table === null) { throw new Exception\RuntimeException('This table object does not have a valid table set.'); } - if (! $this->resultSetPrototype instanceof ResultSetInterface) { + if ($this->resultSetPrototype === null) { $this->resultSetPrototype = new ResultSet(); } - if (! $this->sql instanceof Sql) { + if ($this->sql === null) { $this->sql = new Sql($this->adapter, $this->table); } @@ -122,16 +113,28 @@ public function getColumns(): array public function getFeatureSet(): Feature\FeatureSet { + if (! $this->isInitialized) { + $this->initialize(); + } + return $this->featureSet; } public function getResultSetPrototype(): ResultSetInterface { + if (! $this->isInitialized) { + $this->initialize(); + } + return $this->resultSetPrototype; } public function getSql(): Sql { + if (! $this->isInitialized) { + $this->initialize(); + } + return $this->sql; } @@ -158,6 +161,7 @@ public function selectWith(Select $select): ResultSetInterface if (! $this->isInitialized) { $this->initialize(); } + return $this->executeSelect($select); } @@ -211,6 +215,7 @@ public function insert(array $set): int } $insert = $this->sql->insert(); $insert->values($set); + return $this->executeInsert($insert); } @@ -219,12 +224,13 @@ public function insertWith(Insert $insert): int if (! $this->isInitialized) { $this->initialize(); } + return $this->executeInsert($insert); } /** - * @todo add $columns support * @throws Exception\RuntimeException + * @todo add $columns support */ protected function executeInsert(Insert $insert): int { @@ -293,12 +299,13 @@ public function updateWith(Update $update): int if (! $this->isInitialized) { $this->initialize(); } + return $this->executeUpdate($update); } /** - * @todo add $columns support * @throws Exception\RuntimeException + * @todo add $columns support */ protected function executeUpdate(Update $update): int { @@ -345,18 +352,20 @@ public function delete(Where|Closure|array|string $where): int } else { $delete->where($where); } + return $this->executeDelete($delete); } public function deleteWith(Delete $delete): int { $this->initialize(); + return $this->executeDelete($delete); } /** - * @todo add $columns support * @throws Exception\RuntimeException + * @todo add $columns support */ protected function executeDelete(Delete $delete): int { @@ -401,18 +410,20 @@ public function getLastInsertValue(): int */ public function __get(string $property): mixed { - switch (strtolower($property)) { - case 'lastinsertvalue': - return $this->lastInsertValue; - case 'adapter': - return $this->adapter; - case 'table': - return $this->table; - } - if ($this->featureSet->canCallMagicGet($property)) { - return $this->featureSet->callMagicGet($property); - } - throw new Exception\InvalidArgumentException('Invalid magic property access in ' . self::class . '::__get()'); + try { + return match ($property) { + 'lastInsertValue', 'adapter', 'table' => $this->$property, + default => throw new Exception\InvalidArgumentException(), + }; + } catch (Exception\InvalidArgumentException) { + if ($this->featureSet->canCallMagicGet($property)) { + return $this->featureSet->callMagicGet($property); + } + } + + throw new Exception\InvalidArgumentException( + 'Invalid magic property access in ' . self::class . '::__get()' + ); } /** @@ -422,6 +433,7 @@ public function __set(string $property, mixed $value): void { if ($this->featureSet->canCallMagicSet($property)) { $this->featureSet->callMagicSet($property, $value); + return; } throw new Exception\InvalidArgumentException('Invalid magic property access in ' . self::class . '::__set()'); @@ -453,7 +465,7 @@ public function __clone(): void && count($this->table) === 1 && is_object(reset($this->table)) ) { - foreach ($this->table as $alias => &$tableObject) { + foreach ($this->table as &$tableObject) { $tableObject = clone $tableObject; } } diff --git a/src/TableGateway/Exception/ExceptionInterface.php b/src/TableGateway/Exception/ExceptionInterface.php index cd278a3ac..3807d1a7d 100644 --- a/src/TableGateway/Exception/ExceptionInterface.php +++ b/src/TableGateway/Exception/ExceptionInterface.php @@ -1,5 +1,7 @@ tableGateway = $tableGateway; } @@ -30,8 +27,8 @@ public function initialize(): void // No-op } - /** @return string[] */ - public function getMagicMethodSpecifications() + /** @return array */ + public function getMagicMethodSpecifications(): array { return []; } diff --git a/src/TableGateway/Feature/EventFeature.php b/src/TableGateway/Feature/EventFeature.php index aea505dcc..d78fd6dfa 100644 --- a/src/TableGateway/Feature/EventFeature.php +++ b/src/TableGateway/Feature/EventFeature.php @@ -1,5 +1,7 @@ eventManager; } /** * Retrieve composed event instance - * - * @return EventFeature\TableGatewayEvent */ - public function getEvent() + public function getEvent(): EventFeature\TableGatewayEvent { return $this->event; } @@ -67,10 +63,8 @@ public function getEvent() * Ensures that the composed TableGateway has identifiers based on the * class name, and that the event target is set to the TableGateway * instance. It then triggers the "preInitialize" event. - * - * @return void */ - public function preInitialize() + public function preInitialize(): void { if (get_class($this->tableGateway) !== TableGateway::class) { $this->eventManager->addIdentifiers([get_class($this->tableGateway)]); @@ -83,10 +77,8 @@ public function preInitialize() /** * Trigger the "postInitialize" event - * - * @return void */ - public function postInitialize() + public function postInitialize(): void { $this->event->setName(static::EVENT_POST_INITIALIZE); $this->eventManager->triggerEvent($this->event); @@ -97,10 +89,8 @@ public function postInitialize() * * Triggers the "preSelect" event mapping the following parameters: * - $select as "select" - * - * @return void */ - public function preSelect(Select $select) + public function preSelect(Select $select): void { $this->event->setName(static::EVENT_PRE_SELECT); $this->event->setParams(['select' => $select]); @@ -114,11 +104,12 @@ public function preSelect(Select $select) * - $statement as "statement" * - $result as "result" * - $resultSet as "result_set" - * - * @return void */ - public function postSelect(StatementInterface $statement, ResultInterface $result, ResultSetInterface $resultSet) - { + public function postSelect( + StatementInterface $statement, + ResultInterface $result, + ResultSetInterface $resultSet + ): void { $this->event->setName(static::EVENT_POST_SELECT); $this->event->setParams([ 'statement' => $statement, @@ -133,10 +124,8 @@ public function postSelect(StatementInterface $statement, ResultInterface $resul * * Triggers the "preInsert" event mapping the following parameters: * - $insert as "insert" - * - * @return void */ - public function preInsert(Insert $insert) + public function preInsert(Insert $insert): void { $this->event->setName(static::EVENT_PRE_INSERT); $this->event->setParams(['insert' => $insert]); @@ -149,10 +138,8 @@ public function preInsert(Insert $insert) * Triggers the "postInsert" event mapping the following parameters: * - $statement as "statement" * - $result as "result" - * - * @return void */ - public function postInsert(StatementInterface $statement, ResultInterface $result) + public function postInsert(StatementInterface $statement, ResultInterface $result): void { $this->event->setName(static::EVENT_POST_INSERT); $this->event->setParams([ @@ -167,10 +154,8 @@ public function postInsert(StatementInterface $statement, ResultInterface $resul * * Triggers the "preUpdate" event mapping the following parameters: * - $update as "update" - * - * @return void */ - public function preUpdate(Update $update) + public function preUpdate(Update $update): void { $this->event->setName(static::EVENT_PRE_UPDATE); $this->event->setParams(['update' => $update]); @@ -183,10 +168,8 @@ public function preUpdate(Update $update) * Triggers the "postUpdate" event mapping the following parameters: * - $statement as "statement" * - $result as "result" - * - * @return void */ - public function postUpdate(StatementInterface $statement, ResultInterface $result) + public function postUpdate(StatementInterface $statement, ResultInterface $result): void { $this->event->setName(static::EVENT_POST_UPDATE); $this->event->setParams([ @@ -201,10 +184,8 @@ public function postUpdate(StatementInterface $statement, ResultInterface $resul * * Triggers the "preDelete" event mapping the following parameters: * - $delete as "delete" - * - * @return void */ - public function preDelete(Delete $delete) + public function preDelete(Delete $delete): void { $this->event->setName(static::EVENT_PRE_DELETE); $this->event->setParams(['delete' => $delete]); @@ -217,10 +198,8 @@ public function preDelete(Delete $delete) * Triggers the "postDelete" event mapping the following parameters: * - $statement as "statement" * - $result as "result" - * - * @return void */ - public function postDelete(StatementInterface $statement, ResultInterface $result) + public function postDelete(StatementInterface $statement, ResultInterface $result): void { $this->event->setName(static::EVENT_POST_DELETE); $this->event->setParams([ diff --git a/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php b/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php index 56d6ac960..6449e66ed 100644 --- a/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php +++ b/src/TableGateway/Feature/EventFeature/TableGatewayEvent.php @@ -9,13 +9,11 @@ class TableGatewayEvent implements EventInterface { - /** @var AbstractTableGateway */ - protected $target; + protected ?AbstractTableGateway $target = null; protected ?string $name = null; - /** @var array|object */ - protected $params = []; + protected array|object $params = []; public function getName(): ?string { @@ -24,10 +22,8 @@ public function getName(): ?string /** * Get target/context from which event was triggered - * - * @return AbstractTableGateway */ - public function getTarget() + public function getTarget(): ?AbstractTableGateway { return $this->target; } @@ -43,7 +39,7 @@ public function getParams(): array|object /** * Get a single parameter by name * - * @param string $name + * @param string|int $name * @param mixed $default Default value to return if parameter does not exist */ public function getParam($name, $default = null): mixed @@ -54,7 +50,7 @@ public function getParam($name, $default = null): mixed /** * Set the event name * - * @param string|null $name + * @param string $name */ public function setName($name): void { @@ -64,7 +60,7 @@ public function setName($name): void /** * Set the event target/context * - * @param null|string|object $target + * @param object|string|null $target * @phpstan-ignore selfOut.type */ public function setTarget($target): void @@ -73,6 +69,8 @@ public function setTarget($target): void } /** + * Set event parameters + * * @param array|object $params * @phpstan-ignore selfOut.type */ @@ -84,7 +82,7 @@ public function setParams($params): void /** * Set a single parameter by key * - * @param string $name + * @param string|int $name * @param mixed $value */ public function setParam($name, $value): void diff --git a/src/TableGateway/Feature/EventFeatureEventsInterface.php b/src/TableGateway/Feature/EventFeatureEventsInterface.php index 227f28e60..166f09503 100644 --- a/src/TableGateway/Feature/EventFeatureEventsInterface.php +++ b/src/TableGateway/Feature/EventFeatureEventsInterface.php @@ -1,5 +1,7 @@ tableGateway = $tableGateway; foreach ($this->features as $feature) { @@ -40,11 +36,7 @@ public function setTableGateway(AbstractTableGateway $tableGateway) return $this; } - /** - * @param string $featureClassName - * @return null|AbstractFeature - */ - public function getFeatureByClassName($featureClassName) + public function getFeatureByClassName(string $featureClassName): ?FeatureInterface { $feature = null; foreach ($this->features as $potentialFeature) { @@ -56,10 +48,7 @@ public function getFeatureByClassName($featureClassName) return $feature; } - /** - * @return $this Provides a fluent interface - */ - public function addFeatures(array $features) + public function addFeatures(array $features): static { foreach ($features as $feature) { $this->addFeature($feature); @@ -67,10 +56,7 @@ public function addFeatures(array $features) return $this; } - /** - * @return $this Provides a fluent interface - */ - public function addFeature(AbstractFeature $feature) + public function addFeature(FeatureInterface $feature): static { if ($this->tableGateway instanceof TableGatewayInterface) { $feature->setTableGateway($this->tableGateway); @@ -79,16 +65,11 @@ public function addFeature(AbstractFeature $feature) return $this; } - /** - * @param string $method - * @param array $args - * @return void - */ - public function apply($method, $args) + public function apply(string $method, array $args): void { foreach ($this->features as $feature) { if (method_exists($feature, $method)) { - $return = call_user_func_array([$feature, $method], $args); + $return = $feature->$method(...$args); if ($return === self::APPLY_HALT) { break; } @@ -96,49 +77,30 @@ public function apply($method, $args) } } - /** - * @param string $property - * @return bool - */ - public function canCallMagicGet($property) + public function canCallMagicGet(string $property): bool { return false; } - /** - * @param string $property - * @return mixed - */ - public function callMagicGet($property) + public function callMagicGet(string $property): mixed { return null; } - /** - * @param string $property - * @return bool - */ - public function canCallMagicSet($property) + public function canCallMagicSet(string $property): bool { return false; } - /** - * @param string $property - * @param mixed $value - * @return mixed - */ - public function callMagicSet($property, $value) + public function callMagicSet(string $property, mixed $value): mixed { return null; } /** * Is the method requested available in one of the added features - * - * @param string $method */ - public function canCallMagicCall($method): bool + public function canCallMagicCall(string $method): bool { if ($this->features !== []) { foreach ($this->features as $feature) { @@ -152,12 +114,8 @@ public function canCallMagicCall($method): bool /** * Call method of on added feature as though it were a local method - * - * @param string $method - * @param array $arguments - * @return mixed */ - public function callMagicCall($method, $arguments) + public function callMagicCall(string $method, array $arguments): mixed { foreach ($this->features as $feature) { if (method_exists($feature, $method)) { diff --git a/src/TableGateway/Feature/GlobalAdapterFeature.php b/src/TableGateway/Feature/GlobalAdapterFeature.php index 7dff6af39..cb1f5a4b2 100644 --- a/src/TableGateway/Feature/GlobalAdapterFeature.php +++ b/src/TableGateway/Feature/GlobalAdapterFeature.php @@ -1,5 +1,7 @@ slaveAdapter = $slaveAdapter; @@ -27,16 +23,12 @@ public function __construct(AdapterInterface $slaveAdapter, ?Sql $slaveSql = nul } } - /** @return AdapterInterface */ - public function getSlaveAdapter() + public function getSlaveAdapter(): AdapterInterface { return $this->slaveAdapter; } - /** - * @return Sql - */ - public function getSlaveSql() + public function getSlaveSql(): ?Sql { return $this->slaveSql; } diff --git a/src/TableGateway/Feature/MetadataFeature.php b/src/TableGateway/Feature/MetadataFeature.php index 63322a16b..1178b3eaa 100644 --- a/src/TableGateway/Feature/MetadataFeature.php +++ b/src/TableGateway/Feature/MetadataFeature.php @@ -1,5 +1,7 @@ tableGateway; diff --git a/src/TableGateway/Feature/RowGatewayFeature.php b/src/TableGateway/Feature/RowGatewayFeature.php index c8c587e9a..5442ca3bf 100644 --- a/src/TableGateway/Feature/RowGatewayFeature.php +++ b/src/TableGateway/Feature/RowGatewayFeature.php @@ -1,5 +1,7 @@ constructorArguments = func_get_args(); + $this->constructorArguments = $constructorArguments; } public function postInitialize(): void @@ -52,7 +52,7 @@ public function postInitialize(): void $metadata = $this->tableGateway->featureSet->getFeatureByClassName( MetadataFeature::class ); - if ($metadata === false || ! isset($metadata->sharedData['metadata'])) { + if ($metadata === null || ! isset($metadata->sharedData['metadata'])) { throw new Exception\RuntimeException( 'No information was provided to the RowGatewayFeature and/or no MetadataFeature could be consulted ' . 'to find the primary key necessary for RowGateway object creation.' diff --git a/src/TableGateway/Feature/SequenceFeature.php b/src/TableGateway/Feature/SequenceFeature.php index 1398bf942..e6bdce876 100644 --- a/src/TableGateway/Feature/SequenceFeature.php +++ b/src/TableGateway/Feature/SequenceFeature.php @@ -13,14 +13,11 @@ class SequenceFeature extends AbstractFeature { - /** @var string */ - protected $primaryKeyField; + protected string $primaryKeyField; - /** @var string */ - protected $sequenceName; + protected string $sequenceName; - /** @var int */ - protected $sequenceValue; + protected ?int $sequenceValue = null; public function __construct(string $primaryKeyField, string $sequenceName) { @@ -28,10 +25,7 @@ public function __construct(string $primaryKeyField, string $sequenceName) $this->sequenceName = $sequenceName; } - /** - * @return Insert - */ - public function preInsert(Insert $insert) + public function preInsert(Insert $insert): Insert { $columns = $insert->getRawState('columns'); $values = $insert->getRawState('values'); @@ -62,7 +56,7 @@ public function postInsert(StatementInterface $statement, ResultInterface $resul * * @throws RuntimeException */ - public function nextSequenceId(): int + public function nextSequenceId(): ?int { $platform = $this->tableGateway->adapter->getPlatform(); $platformName = $platform->getName(); diff --git a/src/TableGateway/TableGateway.php b/src/TableGateway/TableGateway.php index c429b5a51..7969492a9 100644 --- a/src/TableGateway/TableGateway.php +++ b/src/TableGateway/TableGateway.php @@ -20,28 +20,24 @@ class TableGateway extends AbstractTableGateway public function __construct( TableIdentifier|array|string $table, AdapterInterface $adapter, - Feature\FeatureSet|Feature\AbstractFeature|array $features = new Feature\FeatureSet(), - ResultSetInterface $resultSetPrototype = new ResultSet(), + Feature\FeatureSet|Feature\FeatureInterface|array|null $features = new Feature\FeatureSet(), + ResultSetInterface|null $resultSetPrototype = new ResultSet(), ?Sql $sql = null ) { $this->table = $table; - // adapter $this->adapter = $adapter; $this->featureSet = match (true) { - $features instanceof Feature\AbstractFeature => new Feature\FeatureSet([$features]), - is_array($features) => new Feature\FeatureSet($features), - default => $features, + $features instanceof Feature\FeatureInterface => new Feature\FeatureSet([$features]), + is_array($features) => new Feature\FeatureSet($features), + default => $features, }; - // result prototype $this->resultSetPrototype = $resultSetPrototype; - // Sql object (factory for select, insert, update, delete) $this->sql = $sql ?: new Sql($this->adapter, $this->table); - // check sql object bound to same table if ($this->sql->getTable() !== $this->table) { throw new Exception\InvalidArgumentException( 'The table inside the provided Sql object must match the table of this TableGateway' diff --git a/src/TableGateway/TableGatewayInterface.php b/src/TableGateway/TableGatewayInterface.php index b7166db13..6c9268766 100644 --- a/src/TableGateway/TableGatewayInterface.php +++ b/src/TableGateway/TableGatewayInterface.php @@ -6,14 +6,14 @@ use Closure; use PhpDb\ResultSet\ResultSetInterface; +use PhpDb\Sql\TableIdentifier; use PhpDb\Sql\Where; interface TableGatewayInterface { - /** @return string */ - public function getTable(); + public function getTable(): TableIdentifier|string|array; - public function select(Where|Closure|string|array $where): ResultSetInterface; + public function select(Where|Closure|string|array|null $where = null): ResultSetInterface; /** * @param array $set diff --git a/test/unit/Adapter/AdapterAwareTraitTest.php b/test/unit/Adapter/AdapterAwareTraitTest.php index 2c0795165..b133fcfc6 100644 --- a/test/unit/Adapter/AdapterAwareTraitTest.php +++ b/test/unit/Adapter/AdapterAwareTraitTest.php @@ -6,38 +6,51 @@ use PhpDb\Adapter\Adapter; use PhpDb\Adapter\AdapterAwareTrait; +use PhpDb\Adapter\AdapterInterface; use PhpDb\Adapter\Driver\DriverInterface; use PhpDb\Adapter\Platform\PlatformInterface; -use PhpDbTest\DeprecatedAssertionsTrait; -use PHPUnit\Framework\Attributes\IgnoreDeprecations; -use PHPUnit\Framework\Attributes\RequiresPhp; -use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; -use ReflectionException; +use ReflectionProperty; -#[IgnoreDeprecations] -#[RequiresPhp('<= 8.6')] class AdapterAwareTraitTest extends TestCase { - use DeprecatedAssertionsTrait; - - /** - * @throws ReflectionException - * @throws Exception - */ public function testSetDbAdapter(): void { - $object = $this->getObjectForTrait(AdapterAwareTrait::class); + $object = new class { + use AdapterAwareTrait; + + public function getAdapter(): ?AdapterInterface + { + return $this->adapter ?? null; + } + }; + + self::assertNull($object->getAdapter()); + + $driver = $this->createMock(DriverInterface::class); + $platform = $this->createMock(PlatformInterface::class); + + $adapter = new Adapter($driver, $platform); - self::assertAttributeEquals(null, 'adapter', $object); + $object->setDbAdapter($adapter); + + self::assertSame($adapter, $object->getAdapter()); + } + + public function testSetDbAdapterSetsProperty(): void + { + $object = new class { + use AdapterAwareTrait; + }; - $driver = $this->getMockBuilder(DriverInterface::class)->getMock(); - $platform = $this->getMockBuilder(PlatformInterface::class)->getMock(); + $driver = $this->createMock(DriverInterface::class); + $platform = $this->createMock(PlatformInterface::class); $adapter = new Adapter($driver, $platform); $object->setDbAdapter($adapter); - self::assertAttributeEquals($adapter, 'adapter', $object); + $reflection = new ReflectionProperty($object, 'adapter'); + self::assertSame($adapter, $reflection->getValue($object)); } } diff --git a/test/unit/DeprecatedAssertionsTrait.php b/test/unit/DeprecatedAssertionsTrait.php index 13263e2ec..fbec2b193 100644 --- a/test/unit/DeprecatedAssertionsTrait.php +++ b/test/unit/DeprecatedAssertionsTrait.php @@ -24,8 +24,6 @@ public static function assertAttributeEquals( string $message = '' ): void { $r = new ReflectionProperty($instance, $attribute); - /** @noinspection PhpExpressionResultUnusedInspection */ - $r->setAccessible(true); Assert::assertEquals($expected, $r->getValue($instance), $message); } @@ -35,8 +33,6 @@ public static function assertAttributeEquals( public function readAttribute(object $instance, string $attribute): mixed { $r = new ReflectionProperty($instance, $attribute); - /** @noinspection PhpExpressionResultUnusedInspection */ - $r->setAccessible(true); return $r->getValue($instance); } } diff --git a/test/unit/Metadata/Source/AbstractSourceTest.php b/test/unit/Metadata/Source/AbstractSourceTest.php index 4d7696ddd..2d4c9d1dc 100644 --- a/test/unit/Metadata/Source/AbstractSourceTest.php +++ b/test/unit/Metadata/Source/AbstractSourceTest.php @@ -80,8 +80,6 @@ protected function setUp(): void private function setMockData(array $data): void { $refProp = new ReflectionProperty($this->abstractSourceMock, 'data'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $refProp->setAccessible(true); $refProp->setValue($this->abstractSourceMock, $data); } @@ -91,8 +89,6 @@ private function setMockData(array $data): void private function getMockData(): array { $refProp = new ReflectionProperty($this->abstractSourceMock, 'data'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $refProp->setAccessible(true); return $refProp->getValue($this->abstractSourceMock); } @@ -110,8 +106,6 @@ public function testConstructorWithSchemaFromAdapter(): void ->getMock(); $refProp = new ReflectionProperty($source, 'defaultSchema'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $refProp->setAccessible(true); // Verify schema is retrieved from adapter self::assertSame('my_schema', $refProp->getValue($source)); @@ -131,8 +125,6 @@ public function testConstructorWithNullSchemaUsesDefaultConstant(): void ->getMock(); $refProp = new ReflectionProperty($source, 'defaultSchema'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $refProp->setAccessible(true); // Verify default constant is used when adapter returns false self::assertSame(AbstractSource::DEFAULT_SCHEMA, $refProp->getValue($source)); @@ -164,8 +156,6 @@ public function testGetSchemasCallsLoadSchemaData(): void public function testGetTableNamesWithNullSchemaUsesDefault(): void { $refProp = new ReflectionProperty($this->abstractSourceMock, 'defaultSchema'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $refProp->setAccessible(true); $refProp->setValue($this->abstractSourceMock, 'default_schema'); $this->setMockData([ @@ -1029,13 +1019,9 @@ public function testPrepareDataHierarchyWithSingleKey(): void ->getMock(); $method = new ReflectionMethod($source, 'prepareDataHierarchy'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $method->invoke($source, 'test_key'); $refProp = new ReflectionProperty($source, 'data'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $refProp->setAccessible(true); $data = $refProp->getValue($source); @@ -1054,13 +1040,9 @@ public function testPrepareDataHierarchyWithMultipleKeys(): void ->getMock(); $method = new ReflectionMethod($source, 'prepareDataHierarchy'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $method->invoke($source, 'level1', 'level2', 'level3'); $refProp = new ReflectionProperty($source, 'data'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $refProp->setAccessible(true); $data = $refProp->getValue($source); @@ -1082,8 +1064,6 @@ public function testLoadTableNameDataEarlyReturnWhenDataExists(): void ]); $method = new ReflectionMethod($this->abstractSourceMock, 'loadTableNameData'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $method->invoke($this->abstractSourceMock, 'public'); $data = $this->getMockData(); @@ -1105,8 +1085,6 @@ public function testLoadColumnDataEarlyReturnWhenDataExists(): void ]); $method = new ReflectionMethod($this->abstractSourceMock, 'loadColumnData'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $method->invoke($this->abstractSourceMock, 'users', 'public'); $data = $this->getMockData(); @@ -1126,8 +1104,6 @@ public function testLoadConstraintDataEarlyReturnWhenDataExists(): void ]); $method = new ReflectionMethod($this->abstractSourceMock, 'loadConstraintData'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $method->invoke($this->abstractSourceMock, 'table', 'public'); $data = $this->getMockData(); @@ -1147,8 +1123,6 @@ public function testLoadConstraintDataKeysEarlyReturnWhenDataExists(): void ]); $method = new ReflectionMethod($this->abstractSourceMock, 'loadConstraintDataKeys'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $method->invoke($this->abstractSourceMock, 'public'); $data = $this->getMockData(); @@ -1168,8 +1142,6 @@ public function testLoadConstraintReferencesEarlyReturnWhenDataExists(): void ]); $method = new ReflectionMethod($this->abstractSourceMock, 'loadConstraintReferences'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $method->invoke($this->abstractSourceMock, 'table', 'public'); $data = $this->getMockData(); @@ -1189,8 +1161,6 @@ public function testLoadTriggerDataEarlyReturnWhenDataExists(): void ]); $method = new ReflectionMethod($this->abstractSourceMock, 'loadTriggerData'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $method->invoke($this->abstractSourceMock, 'public'); $data = $this->getMockData(); diff --git a/test/unit/RowGateway/AbstractRowGatewayTest.php b/test/unit/RowGateway/AbstractRowGatewayTest.php index 72ce60e0c..af6f12439 100644 --- a/test/unit/RowGateway/AbstractRowGatewayTest.php +++ b/test/unit/RowGateway/AbstractRowGatewayTest.php @@ -12,7 +12,9 @@ use PhpDb\Adapter\Driver\StatementInterface; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\RowGateway\AbstractRowGateway; +use PhpDb\RowGateway\Exception\InvalidArgumentException; use PhpDb\RowGateway\Exception\RuntimeException; +use PhpDb\RowGateway\Feature\FeatureSet; use PhpDb\RowGateway\RowGateway; use PhpDb\Sql\Select; use PhpDb\Sql\Sql; @@ -41,6 +43,7 @@ #[CoversMethod(RowGateway::class, 'processPrimaryKeyData')] #[CoversMethod(RowGateway::class, 'count')] #[CoversMethod(RowGateway::class, 'toArray')] +#[CoversMethod(RowGateway::class, 'exchangeArray')] final class AbstractRowGatewayTest extends TestCase { /** @var Adapter&MockObject */ @@ -59,7 +62,6 @@ final class AbstractRowGatewayTest extends TestCase #[Override] protected function setUp(): void { - // mock the adapter, driver, and parts $mockResult = $this->getMockBuilder(ResultInterface::class)->getMock(); $mockResult->expects($this->any())->method('getAffectedRows')->willReturn(1); $this->mockResult = $mockResult; @@ -70,7 +72,6 @@ protected function setUp(): void $mockDriver->expects($this->any())->method('createStatement')->willReturn($mockStatement); $mockDriver->expects($this->any())->method('getConnection')->willReturn($mockConnection); - // setup mock adapter $this->mockAdapter = $this->getMockBuilder(Adapter::class) ->onlyMethods([]) ->setConstructorArgs( @@ -83,7 +84,7 @@ protected function setUp(): void $this->rowGateway = $this->getMockBuilder(AbstractRowGateway::class)->onlyMethods([])->getMock(); $rgPropertyValues = [ - 'primaryKeyColumn' => 'id', + 'primaryKeyColumn' => ['id'], 'table' => 'foo', 'sql' => new Sql($this->mockAdapter), ]; @@ -92,7 +93,6 @@ protected function setUp(): void public function testOffsetSet(): void { - // If we set with an index, both getters should retrieve the same value: $this->rowGateway['testColumn'] = 'test'; self::assertEquals('test', $this->rowGateway->testColumn); self::assertEquals('test', $this->rowGateway['testColumn']); @@ -102,7 +102,6 @@ public function testOffsetSet(): void public function test__set(): void { // @codingStandardsIgnoreEnd - // If we set with a property, both getters should retrieve the same value: $this->rowGateway->testColumn = 'test'; self::assertEquals('test', $this->rowGateway->testColumn); self::assertEquals('test', $this->rowGateway['testColumn']); @@ -112,7 +111,6 @@ public function test__set(): void public function test__isset(): void { // @codingStandardsIgnoreEnd - // Test isset before and after assigning to a property: self::assertFalse(isset($this->rowGateway->foo)); $this->rowGateway->foo = 'bar'; self::assertTrue(isset($this->rowGateway->foo)); @@ -120,7 +118,6 @@ public function test__isset(): void public function testOffsetExists(): void { - // Test isset before and after assigning to an index: self::assertFalse(isset($this->rowGateway['foo'])); $this->rowGateway['foo'] = 'bar'; self::assertTrue(isset($this->rowGateway['foo'])); @@ -148,7 +145,6 @@ public function testOffsetUnset(): void public function testOffsetGet(): void { - // If we set with an index, both getters should retrieve the same value: $this->rowGateway['testColumn'] = 'test'; self::assertEquals('test', $this->rowGateway->testColumn); self::assertEquals('test', $this->rowGateway['testColumn']); @@ -158,7 +154,6 @@ public function testOffsetGet(): void public function test__get(): void { // @codingStandardsIgnoreEnd - // If we set with a property, both getters should retrieve the same value: $this->rowGateway->testColumn = 'test'; self::assertEquals('test', $this->rowGateway->testColumn); self::assertEquals('test', $this->rowGateway['testColumn']); @@ -166,7 +161,6 @@ public function test__get(): void public function testSaveInsert(): void { - // test insert $this->mockResult->expects($this->any())->method('current') ->willReturn(['id' => 5, 'name' => 'foo']); $this->mockResult->expects($this->any())->method('getGeneratedValue')->willReturn(5); @@ -197,7 +191,6 @@ public function testSaveInsertMultiKey(): void ]; $this->setRowGatewayState($rgPropertyValues); - // test insert $this->mockResult->expects($this->any())->method('current') ->willReturn(['one' => 'foo', 'two' => 'bar']); @@ -205,14 +198,11 @@ public function testSaveInsertMultiKey(): void $refRowGateway = new ReflectionObject($this->rowGateway); $refRowGatewayProp = $refRowGateway->getProperty('primaryKeyData'); - /** @psalm-suppress UnusedMethodCall */ - $refRowGatewayProp->setAccessible(true); $this->rowGateway->populate(['one' => 'foo', 'two' => 'bar']); self::assertNull($refRowGatewayProp->getValue($this->rowGateway)); - // save should setup the primaryKeyData $this->rowGateway->save(); self::assertEquals(['one' => 'foo', 'two' => 'bar'], $refRowGatewayProp->getValue($this->rowGateway)); @@ -220,7 +210,6 @@ public function testSaveInsertMultiKey(): void public function testSaveUpdate(): void { - // test update $this->mockResult->expects($this->any())->method('current') ->willReturn(['id' => 6, 'name' => 'foo']); $this->rowGateway->populate(['id' => 6, 'name' => 'foo'], true); @@ -230,7 +219,6 @@ public function testSaveUpdate(): void public function testSaveUpdateChangingPrimaryKey(): void { - // this mock is the select to be used to re-fresh the rowobject's data $selectMock = $this->getMockBuilder(Select::class) ->onlyMethods(['where']) ->getMock(); @@ -249,12 +237,10 @@ public function testSaveUpdateChangingPrimaryKey(): void $this->setRowGatewayState(['sql' => $sqlMock]); - // original mock returning updated data $this->mockResult->expects($this->any()) ->method('current') ->willReturn(['id' => 7, 'name' => 'fooUpdated']); - // populate forces an update in save(), seeds with original data (from db) $this->rowGateway->populate(['id' => 6, 'name' => 'foo'], true); $this->rowGateway->id = 7; $this->rowGateway->save(); @@ -301,6 +287,151 @@ public function testToArray(): void self::assertEquals(['id' => 5, 'name' => 'foo'], $this->rowGateway->toArray()); } + public function testExchangeArray(): void + { + $this->rowGateway->populate(['id' => 5, 'name' => 'foo'], true); + self::assertEquals(['id' => 5, 'name' => 'foo'], $this->rowGateway->toArray()); + + $oldData = $this->rowGateway->exchangeArray(['id' => 10, 'name' => 'bar']); + + /** @phpstan-ignore staticMethod.alreadyNarrowedType */ + self::assertIsArray($oldData); + self::assertEquals(['id' => 5, 'name' => 'foo'], $oldData); + + self::assertEquals(10, $this->rowGateway['id']); + self::assertEquals('bar', $this->rowGateway['name']); + self::assertTrue($this->rowGateway->rowExistsInDatabase()); + } + + public function testRowExistsInDatabaseReturnsFalseWhenNew(): void + { + $this->rowGateway->populate(['name' => 'foo']); + self::assertFalse($this->rowGateway->rowExistsInDatabase()); + } + + public function testRowExistsInDatabaseReturnsTrueAfterPopulateWithTrue(): void + { + $this->rowGateway->populate(['id' => 5, 'name' => 'foo'], true); + self::assertTrue($this->rowGateway->rowExistsInDatabase()); + } + + // @codingStandardsIgnoreStart + public function test__getThrowsExceptionForInvalidColumn(): void + { + // @codingStandardsIgnoreEnd + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Not a valid column in this row'); + + /** @phpstan-ignore property.notFound, expr.resultUnused */ + $this->rowGateway->nonExistentColumn; + } + + public function testInitializeThrowsExceptionWhenTableIsNull(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This row object does not have a valid table set.'); + + $rowGateway = new RowGateway('id', 'temp_table', $this->mockAdapter); + + $refRowGateway = new ReflectionObject($rowGateway); + $tableProp = $refRowGateway->getProperty('table'); + $tableProp->setValue($rowGateway, null); + + $isInitializedProp = $refRowGateway->getProperty('isInitialized'); + $isInitializedProp->setValue($rowGateway, false); + + $rowGateway->populate(['name' => 'test']); + } + + public function testInitializeThrowsExceptionWhenPrimaryKeyColumnIsNull(): void + { + $rowGateway = $this->getMockBuilder(AbstractRowGateway::class)->onlyMethods([])->getMock(); + + $refRowGateway = new ReflectionObject($rowGateway); + + $tableProp = $refRowGateway->getProperty('table'); + $tableProp->setValue($rowGateway, 'foo'); + + $sqlProp = $refRowGateway->getProperty('sql'); + $sqlProp->setValue($rowGateway, new Sql($this->mockAdapter)); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This row object does not have a primary key column set.'); + + $rowGateway->populate(['name' => 'test']); + } + + public function testInitializeThrowsExceptionWhenSqlIsNull(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This row object does not have a Sql object set.'); + + $rowGateway = new RowGateway('id', 'temp_table', $this->mockAdapter); + + $refRowGateway = new ReflectionObject($rowGateway); + + $sqlProp = $refRowGateway->getProperty('sql'); + $sqlProp->setValue($rowGateway, null); + + $isInitializedProp = $refRowGateway->getProperty('isInitialized'); + $isInitializedProp->setValue($rowGateway, false); + + $rowGateway->populate(['name' => 'test']); + } + + public function testInitializeOnlyRunsOnce(): void + { + $this->rowGateway->populate(['id' => 1, 'name' => 'foo'], true); + + $refRowGateway = new ReflectionObject($this->rowGateway); + $isInitializedProp = $refRowGateway->getProperty('isInitialized'); + self::assertTrue($isInitializedProp->getValue($this->rowGateway)); + + $this->rowGateway->populate(['id' => 2, 'name' => 'bar'], true); + + self::assertEquals(2, $this->rowGateway['id']); + self::assertEquals('bar', $this->rowGateway['name']); + } + + public function testInitializeEarlyReturnWhenAlreadyInitialized(): void + { + $rowGateway = new RowGateway('id', 'test_table', $this->mockAdapter); + + $refRowGateway = new ReflectionObject($rowGateway); + $featureSetProp = $refRowGateway->getProperty('featureSet'); + $originalFeatureSet = $featureSetProp->getValue($rowGateway); + + $isInitializedProp = $refRowGateway->getProperty('isInitialized'); + self::assertTrue($isInitializedProp->getValue($rowGateway)); + + $rowGateway->populate(['id' => 2, 'name' => 'bar'], true); + + self::assertSame($originalFeatureSet, $featureSetProp->getValue($rowGateway)); + } + + public function testInitializeCreatesFeatureSetIfNotSet(): void + { + $rowGateway = $this->getMockBuilder(AbstractRowGateway::class)->onlyMethods([])->getMock(); + + $refRowGateway = new ReflectionObject($rowGateway); + + $tableProp = $refRowGateway->getProperty('table'); + $tableProp->setValue($rowGateway, 'foo'); + + $pkProp = $refRowGateway->getProperty('primaryKeyColumn'); + $pkProp->setValue($rowGateway, ['id']); + + $sqlProp = $refRowGateway->getProperty('sql'); + $sqlProp->setValue($rowGateway, new Sql($this->mockAdapter)); + + $featureSetProp = $refRowGateway->getProperty('featureSet'); + self::assertNull($featureSetProp->getValue($rowGateway)); + + $rowGateway->populate(['id' => 1, 'name' => 'test'], true); + + self::assertInstanceOf(FeatureSet::class, $featureSetProp->getValue($rowGateway)); + } + /** * @throws ReflectionException */ @@ -310,8 +441,6 @@ protected function setRowGatewayState(array $properties): void $refRowGateway = new ReflectionObject($this->rowGateway); foreach ($properties as $rgPropertyName => $rgPropertyValue) { $refRowGatewayProp = $refRowGateway->getProperty($rgPropertyName); - /** @psalm-suppress UnusedMethodCall */ - $refRowGatewayProp->setAccessible(true); $refRowGatewayProp->setValue($this->rowGateway, $rgPropertyValue); } } diff --git a/test/unit/RowGateway/Feature/AbstractFeatureTest.php b/test/unit/RowGateway/Feature/AbstractFeatureTest.php new file mode 100644 index 000000000..16c3345a8 --- /dev/null +++ b/test/unit/RowGateway/Feature/AbstractFeatureTest.php @@ -0,0 +1,68 @@ +feature = $this->getMockBuilder(AbstractFeature::class) + ->disableOriginalConstructor() + ->onlyMethods([]) + ->getMock(); + } + + public function testGetNameReturnsClassName(): void + { + $name = $this->feature->getName(); + + // The mock class name will contain the class name + /** @phpstan-ignore staticMethod.alreadyNarrowedType */ + self::assertIsString($name); + self::assertNotEmpty($name); + } + + public function testSetRowGateway(): void + { + /** @var AbstractRowGateway&MockObject $rowGateway */ + $rowGateway = $this->getMockBuilder(AbstractRowGateway::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->feature->setRowGateway($rowGateway); + + // Use reflection to verify the rowGateway was set + $reflection = new ReflectionProperty(AbstractFeature::class, 'rowGateway'); + $value = $reflection->getValue($this->feature); + + self::assertSame($rowGateway, $value); + } + + public function testInitializeThrowsRuntimeException(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This method is not intended to be called on this object.'); + + $this->feature->initialize(); + } + + public function testGetMagicMethodSpecificationsReturnsEmptyArray(): void + { + $result = $this->feature->getMagicMethodSpecifications(); + + /** @phpstan-ignore staticMethod.alreadyNarrowedType */ + self::assertIsArray($result); + self::assertEmpty($result); + } +} diff --git a/test/unit/RowGateway/Feature/FeatureSetTest.php b/test/unit/RowGateway/Feature/FeatureSetTest.php new file mode 100644 index 000000000..b59907b4f --- /dev/null +++ b/test/unit/RowGateway/Feature/FeatureSetTest.php @@ -0,0 +1,227 @@ +createMock(AbstractFeature::class); + $featureSet = new FeatureSet([$feature]); + self::assertInstanceOf(FeatureSet::class, $featureSet); + } + + public function testSetRowGateway(): void + { + /** @var AbstractRowGateway&MockObject $rowGateway */ + $rowGateway = $this->getMockBuilder(AbstractRowGateway::class) + ->disableOriginalConstructor() + ->getMock(); + + $feature = $this->createMock(AbstractFeature::class); + $feature->expects($this->once()) + ->method('setRowGateway') + ->with($rowGateway); + + $featureSet = new FeatureSet([$feature]); + $result = $featureSet->setRowGateway($rowGateway); + + self::assertSame($featureSet, $result); + } + + public function testGetFeatureByClassNameReturnsFeature(): void + { + $feature = $this->createMock(AbstractFeature::class); + $featureSet = new FeatureSet([$feature]); + + $result = $featureSet->getFeatureByClassName(AbstractFeature::class); + + self::assertSame($feature, $result); + } + + public function testGetFeatureByClassNameReturnsNullWhenNotFound(): void + { + $featureSet = new FeatureSet(); + + $result = $featureSet->getFeatureByClassName(AbstractFeature::class); + + self::assertNull($result); + } + + public function testAddFeatures(): void + { + $feature1 = $this->createMock(AbstractFeature::class); + $feature2 = $this->createMock(AbstractFeature::class); + + $featureSet = new FeatureSet(); + $result = $featureSet->addFeatures([$feature1, $feature2]); + + self::assertSame($featureSet, $result); + self::assertSame($feature1, $featureSet->getFeatureByClassName(AbstractFeature::class)); + } + + public function testAddFeature(): void + { + $feature = $this->createMock(AbstractFeature::class); + + $featureSet = new FeatureSet(); + $result = $featureSet->addFeature($feature); + + self::assertSame($featureSet, $result); + self::assertSame($feature, $featureSet->getFeatureByClassName(AbstractFeature::class)); + } + + public function testAddFeatureCallsSetRowGatewayWhenRowGatewayIsSet(): void + { + /** @var AbstractRowGateway&MockObject $rowGateway */ + $rowGateway = $this->getMockBuilder(AbstractRowGateway::class) + ->disableOriginalConstructor() + ->getMock(); + + $feature = $this->createMock(AbstractFeature::class); + $feature->expects($this->once()) + ->method('setRowGateway') + ->with($rowGateway); + + $featureSet = new FeatureSet(); + $featureSet->setRowGateway($rowGateway); + $featureSet->addFeature($feature); + } + + public function testApplyCallsMethodOnFeatures(): void + { + $called = false; + $receivedArgs = []; + + $feature = new class ($called, $receivedArgs) extends AbstractFeature { + /** @var bool @phpstan-ignore property.onlyWritten */ + private $called; + /** @var array @phpstan-ignore property.onlyWritten */ + private $receivedArgs; + + public function __construct(bool &$called, array &$receivedArgs) + { + $this->called = &$called; + $this->receivedArgs = &$receivedArgs; + } + + public function preInitialize(string $arg1, string $arg2): void + { + $this->called = true; + $this->receivedArgs = [$arg1, $arg2]; + } + }; + + $featureSet = new FeatureSet([$feature]); + $featureSet->apply('preInitialize', ['arg1', 'arg2']); + + self::assertTrue($called); + self::assertEquals(['arg1', 'arg2'], $receivedArgs); + } + + public function testApplyHaltsWhenFeatureReturnsHalt(): void + { + $feature1Called = false; + $feature2Called = false; + + $feature1 = new class ($feature1Called) extends AbstractFeature { + /** @var bool @phpstan-ignore property.onlyWritten */ + private $called; + + public function __construct(bool &$called) + { + $this->called = &$called; + } + + public function preInitialize(): string + { + $this->called = true; + return FeatureSet::APPLY_HALT; + } + }; + + $feature2 = new class ($feature2Called) extends AbstractFeature { + /** @var bool @phpstan-ignore property.onlyWritten */ + private $called; + + public function __construct(bool &$called) + { + $this->called = &$called; + } + + public function preInitialize(): void + { + $this->called = true; + } + }; + + $featureSet = new FeatureSet([$feature1, $feature2]); + $featureSet->apply('preInitialize', []); + + self::assertTrue($feature1Called); + self::assertFalse($feature2Called); + } + + public function testApplySkipsFeatureWithoutMethod(): void + { + $feature = $this->createMock(AbstractFeature::class); + + $featureSet = new FeatureSet([$feature]); + $featureSet->apply('nonExistentMethod', []); + + /** @phpstan-ignore staticMethod.alreadyNarrowedType */ + self::assertTrue(true); + } + + public function testCanCallMagicGetReturnsFalse(): void + { + $featureSet = new FeatureSet(); + /** @phpstan-ignore staticMethod.impossibleType */ + self::assertFalse($featureSet->canCallMagicGet('property')); + } + + public function testCallMagicGetReturnsNull(): void + { + $featureSet = new FeatureSet(); + self::assertNull($featureSet->callMagicGet('property')); + } + + public function testCanCallMagicSetReturnsFalse(): void + { + $featureSet = new FeatureSet(); + /** @phpstan-ignore staticMethod.impossibleType */ + self::assertFalse($featureSet->canCallMagicSet('property')); + } + + public function testCallMagicSetReturnsNull(): void + { + $featureSet = new FeatureSet(); + self::assertNull($featureSet->callMagicSet('property', 'value')); + } + + public function testCanCallMagicCallReturnsFalse(): void + { + $featureSet = new FeatureSet(); + self::assertFalse($featureSet->canCallMagicCall('method')); + } + + public function testCallMagicCallReturnsNull(): void + { + $featureSet = new FeatureSet(); + self::assertNull($featureSet->callMagicCall('method', [])); + } +} diff --git a/test/unit/RowGateway/RowGatewayTest.php b/test/unit/RowGateway/RowGatewayTest.php index 71773c6fd..f0fd2ffb0 100644 --- a/test/unit/RowGateway/RowGatewayTest.php +++ b/test/unit/RowGateway/RowGatewayTest.php @@ -11,10 +11,14 @@ use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\Driver\StatementInterface; use PhpDb\Adapter\Platform\PlatformInterface; +use PhpDb\RowGateway\Exception\InvalidArgumentException; use PhpDb\RowGateway\Exception\RuntimeException; use PhpDb\RowGateway\RowGateway; +use PhpDb\Sql\Sql; +use PhpDb\Sql\TableIdentifier; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use ReflectionProperty; final class RowGatewayTest extends TestCase { @@ -25,10 +29,10 @@ final class RowGatewayTest extends TestCase /** @var ResultInterface&MockObject */ protected ResultInterface|MockObject $mockResult; + #[Override] protected function setUp(): void { - // mock the adapter, driver, and parts $mockResult = $this->getMockBuilder(ResultInterface::class)->getMock(); $mockResult->expects($this->any())->method('getAffectedRows')->willReturn(1); $this->mockResult = $mockResult; @@ -42,7 +46,6 @@ protected function setUp(): void $mockDriver->expects($this->any())->method('createStatement')->willReturn($mockStatement); $mockDriver->expects($this->any())->method('getConnection')->willReturn($mockConnection); - // setup mock adapter $this->mockAdapter = $this->getMockBuilder(Adapter::class) ->onlyMethods([]) ->setConstructorArgs( @@ -59,4 +62,109 @@ public function testEmptyPrimaryKey(): void $this->expectExceptionMessage('This row object does not have a primary key column set.'); $this->rowGateway = new RowGateway('', 'foo', $this->mockAdapter); } + + public function testConstructorWithStringPrimaryKey(): void + { + $rowGateway = new RowGateway('id', 'foo', $this->mockAdapter); + + $tableProp = new ReflectionProperty(RowGateway::class, 'table'); + $sqlProp = new ReflectionProperty(RowGateway::class, 'sql'); + + self::assertEquals('foo', $tableProp->getValue($rowGateway)); + self::assertInstanceOf(Sql::class, $sqlProp->getValue($rowGateway)); + } + + public function testConstructorWithArrayPrimaryKey(): void + { + $rowGateway = new RowGateway(['id', 'name'], 'foo', $this->mockAdapter); + + $tableProp = new ReflectionProperty(RowGateway::class, 'table'); + self::assertEquals('foo', $tableProp->getValue($rowGateway)); + + $pkProp = new ReflectionProperty(RowGateway::class, 'primaryKeyColumn'); + self::assertEquals(['id', 'name'], $pkProp->getValue($rowGateway)); + } + + public function testConstructorWithNullPrimaryKey(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This row object does not have a primary key column set.'); + + new RowGateway(null, 'foo', $this->mockAdapter); + } + + public function testConstructorWithTableIdentifier(): void + { + $tableIdentifier = new TableIdentifier('foo', 'schema'); + $rowGateway = new RowGateway('id', $tableIdentifier, $this->mockAdapter); + + $tableProp = new ReflectionProperty(RowGateway::class, 'table'); + self::assertSame($tableIdentifier, $tableProp->getValue($rowGateway)); + } + + public function testConstructorWithSqlObject(): void + { + $sql = new Sql($this->mockAdapter, 'foo'); + $rowGateway = new RowGateway('id', 'foo', $sql); + + $sqlProp = new ReflectionProperty(RowGateway::class, 'sql'); + $tableProp = new ReflectionProperty(RowGateway::class, 'table'); + + self::assertSame($sql, $sqlProp->getValue($rowGateway)); + self::assertEquals('foo', $tableProp->getValue($rowGateway)); + } + + public function testConstructorThrowsExceptionWhenSqlTableDoesNotMatch(): void + { + $sql = new Sql($this->mockAdapter, 'bar'); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The Sql object provided does not have a table that matches this row object'); + + new RowGateway('id', 'foo', $sql); + } + + public function testInitializeReturnsEarlyWhenAlreadyInitialized(): void + { + $rowGateway = new RowGateway('id', 'foo', $this->mockAdapter); + + $isInitializedProp = new ReflectionProperty(RowGateway::class, 'isInitialized'); + self::assertTrue($isInitializedProp->getValue($rowGateway)); + + $rowGateway->initialize(); + + self::assertTrue($isInitializedProp->getValue($rowGateway)); + } + + public function testInitializeThrowsWhenTableIsNull(): void + { + $rowGateway = new RowGateway('id', 'foo', $this->mockAdapter); + + $isInitializedProp = new ReflectionProperty(RowGateway::class, 'isInitialized'); + $isInitializedProp->setValue($rowGateway, false); + + $tableProp = new ReflectionProperty(RowGateway::class, 'table'); + $tableProp->setValue($rowGateway, null); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This row object does not have a valid table set.'); + + $rowGateway->initialize(); + } + + public function testInitializeThrowsWhenSqlIsNull(): void + { + $rowGateway = new RowGateway('id', 'foo', $this->mockAdapter); + + $isInitializedProp = new ReflectionProperty(RowGateway::class, 'isInitialized'); + $isInitializedProp->setValue($rowGateway, false); + + $sqlProp = new ReflectionProperty(RowGateway::class, 'sql'); + $sqlProp->setValue($rowGateway, null); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This row object does not have a Sql object set.'); + + $rowGateway->initialize(); + } } diff --git a/test/unit/Sql/AbstractSqlTest.php b/test/unit/Sql/AbstractSqlTest.php index d14ebcce8..005df76f4 100644 --- a/test/unit/Sql/AbstractSqlTest.php +++ b/test/unit/Sql/AbstractSqlTest.php @@ -208,8 +208,6 @@ public function testProcessExpressionWorksWithNamedParameterPrefixContainingWhit public function testResolveColumnValueWithNull(): void { $method = new ReflectionMethod($this->abstractSql, 'resolveColumnValue'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $result = $method->invoke( $this->abstractSql, @@ -230,8 +228,6 @@ public function testResolveColumnValueWithSelect(): void { $select = new Select('foo'); $method = new ReflectionMethod($this->abstractSql, 'resolveColumnValue'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $result = $method->invoke( $this->abstractSql, @@ -253,8 +249,6 @@ public function testResolveColumnValueWithSelect(): void public function testResolveColumnValueWithArrayAndFromTable(): void { $method = new ReflectionMethod($this->abstractSql, 'resolveColumnValue'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $result = $method->invoke( $this->abstractSql, @@ -280,8 +274,6 @@ public function testResolveTableWithTableIdentifierAndSchema(): void { $table = new TableIdentifier('users', 'public'); $method = new ReflectionMethod($this->abstractSql, 'resolveTable'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $result = $method->invoke( $this->abstractSql, @@ -302,8 +294,6 @@ public function testResolveTableWithSelect(): void { $select = new Select('foo'); $method = new ReflectionMethod($this->abstractSql, 'resolveTable'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $result = $method->invoke( $this->abstractSql, @@ -327,8 +317,6 @@ public function testProcessSubSelectWithParameterContainer(): void $select->where(['id' => 5]); $method = new ReflectionMethod($this->abstractSql, 'processSubSelect'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $parameterContainer = new ParameterContainer(); $result = $method->invoke( @@ -351,8 +339,6 @@ public function testProcessSubSelectWithoutParameterContainer(): void $select = new Select('foo'); $method = new ReflectionMethod($this->abstractSql, 'processSubSelect'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); $result = $method->invoke( $this->abstractSql, @@ -374,8 +360,6 @@ protected function invokeProcessExpressionMethod( string|null $namedParameterPrefix = null ): string|StatementContainer { $method = new ReflectionMethod($this->abstractSql, 'processExpression'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); return $method->invoke( $this->abstractSql, $expression, diff --git a/test/unit/Sql/Platform/PlatformTest.php b/test/unit/Sql/Platform/PlatformTest.php index 5889c36a6..eb3be9382 100644 --- a/test/unit/Sql/Platform/PlatformTest.php +++ b/test/unit/Sql/Platform/PlatformTest.php @@ -32,9 +32,6 @@ public function testResolveDefaultPlatform(): void $reflectionMethod = new ReflectionMethod($platform, 'resolvePlatform'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $reflectionMethod->setAccessible(true); - self::assertEquals($adapter->getPlatform(), $reflectionMethod->invoke($platform, null)); } @@ -47,9 +44,6 @@ public function testResolvePlatformName(): void $reflectionMethod = new ReflectionMethod($platform, 'resolvePlatformName'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $reflectionMethod->setAccessible(true); - self::assertEquals('mysql', $reflectionMethod->invoke($platform, new TestAsset\TrustingMysqlPlatform())); self::assertEquals('sqlserver', $reflectionMethod->invoke( $platform, diff --git a/test/unit/Sql/SelectTest.php b/test/unit/Sql/SelectTest.php index 658192eb6..0f8ea959b 100644 --- a/test/unit/Sql/SelectTest.php +++ b/test/unit/Sql/SelectTest.php @@ -226,8 +226,6 @@ public function testBadJoinName(): void $sr = new ReflectionObject($select); $mr = $sr->getMethod('processJoins'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $mr->setAccessible(true); $this->expectException(InvalidArgumentException::class); $mr->invokeArgs($select, [new Sql92(), $mockDriver, $parameterContainer]); @@ -434,8 +432,6 @@ public function testOrder(): void $sr = new ReflectionObject($select); $method = $sr->getMethod('processOrder'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); self::assertEquals( [[['RAND()']]], $method->invokeArgs($select, [new TrustingSql92Platform()]) @@ -451,8 +447,6 @@ public function testOrder(): void ); $sr = new ReflectionObject($select); $method = $sr->getMethod('processOrder'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $method->setAccessible(true); self::assertEquals( [[['"rating" < \'10\'']]], $method->invokeArgs($select, [new TrustingSql92Platform()]) @@ -834,8 +828,6 @@ public function testProcessMethods( */ foreach ($internalTests as $method => $expected) { $mr = $sr->getMethod($method); - /** @noinspection PhpExpressionResultUnusedInspection */ - $mr->setAccessible(true); /** @psalm-suppress MixedAssignment */ $return = $mr->invokeArgs($select, [new Sql92(), $mockDriver, $parameterContainer]); self::assertEquals($expected, $return); diff --git a/test/unit/TableGateway/AbstractTableGatewayTest.php b/test/unit/TableGateway/AbstractTableGatewayTest.php index 69c3d76b1..b6c14f791 100644 --- a/test/unit/TableGateway/AbstractTableGatewayTest.php +++ b/test/unit/TableGateway/AbstractTableGatewayTest.php @@ -19,6 +19,9 @@ use PhpDb\Sql\Select; use PhpDb\Sql\Update; use PhpDb\TableGateway\AbstractTableGateway; +use PhpDb\TableGateway\Exception\InvalidArgumentException; +use PhpDb\TableGateway\Exception\RuntimeException; +use PhpDb\TableGateway\Feature\AbstractFeature; use PhpDb\TableGateway\Feature\FeatureSet; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\IgnoreDeprecations; @@ -47,7 +50,13 @@ #[CoversMethod(AbstractTableGateway::class, 'executeDelete')] #[CoversMethod(AbstractTableGateway::class, 'getLastInsertValue')] #[CoversMethod(AbstractTableGateway::class, '__get')] +#[CoversMethod(AbstractTableGateway::class, '__set')] +#[CoversMethod(AbstractTableGateway::class, '__call')] #[CoversMethod(AbstractTableGateway::class, '__clone')] +#[CoversMethod(AbstractTableGateway::class, 'isInitialized')] +#[CoversMethod(AbstractTableGateway::class, 'getColumns')] +#[CoversMethod(AbstractTableGateway::class, 'getFeatureSet')] +#[CoversMethod(AbstractTableGateway::class, 'initialize')] final class AbstractTableGatewayTest extends TestCase { protected MockObject&Adapter $mockAdapter; @@ -63,14 +72,9 @@ final class AbstractTableGatewayTest extends TestCase protected MockObject&Update $mockUpdate; protected MockObject&Delete $mockDelete; - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ #[Override] protected function setUp(): void { - // mock the adapter, driver, and parts $mockResult = $this->getMockBuilder(ResultInterface::class)->getMock(); $mockResult->expects($this->any())->method('getAffectedRows')->willReturn(5); @@ -133,8 +137,6 @@ protected function setUp(): void $tgReflection = new ReflectionClass(AbstractTableGateway::class); foreach ($tgReflection->getProperties() as $tgPropReflection) { - /** @noinspection PhpExpressionResultUnusedInspection */ - $tgPropReflection->setAccessible(true); switch ($tgPropReflection->getName()) { case 'table': $tgPropReflection->setValue($this->table, 'foo'); @@ -152,8 +154,6 @@ protected function setUp(): void $tgPropReflection->setValue($this->table, $this->mockFeatureSet); break; } - /** @noinspection PhpExpressionResultUnusedInspection */ - $tgPropReflection->setAccessible(false); } } @@ -337,8 +337,6 @@ public function testGetLastInsertValue(): void public function testInitializeBuildsAResultSet(): void { - $this->markTestSkipped('This needs refactored due to setAccessible has been deprecated in PHP 8.1'); - /** @phpstan-ignore deadCode.unreachable */ $stub = $this ->getMockBuilder(AbstractTableGateway::class) ->onlyMethods([]) @@ -346,8 +344,6 @@ public function testInitializeBuildsAResultSet(): void $tgReflection = new ReflectionClass(AbstractTableGateway::class); foreach ($tgReflection->getProperties() as $tgPropReflection) { - /** @noinspection PhpExpressionResultUnusedInspection */ - $tgPropReflection->setAccessible(true); switch ($tgPropReflection->getName()) { case 'table': $tgPropReflection->setValue($stub, 'foo'); @@ -383,4 +379,436 @@ public function test__clone(): void $cTable = clone $this->table; self::assertSame($this->mockAdapter, $cTable->getAdapter()); } + + public function testIsInitialized(): void + { + // Create a fresh mock without initialization + $stub = $this->getMockBuilder(AbstractTableGateway::class) + ->onlyMethods([]) + ->getMock(); + + self::assertFalse($stub->isInitialized()); + + // Set required properties for initialization + $tgReflection = new ReflectionClass(AbstractTableGateway::class); + + $tableProp = $tgReflection->getProperty('table'); + $tableProp->setValue($stub, 'foo'); + + $adapterProp = $tgReflection->getProperty('adapter'); + $adapterProp->setValue($stub, $this->mockAdapter); + + $stub->initialize(); + + self::assertTrue($stub->isInitialized()); + } + + public function testInitializeEarlyReturnWhenAlreadyInitialized(): void + { + // Create a fresh mock without initialization + $stub = $this->getMockBuilder(AbstractTableGateway::class) + ->onlyMethods([]) + ->getMock(); + + // Set required properties for initialization + $tgReflection = new ReflectionClass(AbstractTableGateway::class); + + $tableProp = $tgReflection->getProperty('table'); + $tableProp->setValue($stub, 'foo'); + + $adapterProp = $tgReflection->getProperty('adapter'); + $adapterProp->setValue($stub, $this->mockAdapter); + + // First initialization + $stub->initialize(); + self::assertTrue($stub->isInitialized()); + + // Get the featureSet that was created during first init + $featureSetProp = $tgReflection->getProperty('featureSet'); + $originalFeatureSet = $featureSetProp->getValue($stub); + + // Second initialization should early return (line 69) + $stub->initialize(); + + // Verify featureSet is still the same object (proving early return was taken) + self::assertSame($originalFeatureSet, $featureSetProp->getValue($stub)); + } + + public function testInitializeThrowsExceptionWithoutAdapter(): void + { + $stub = $this->getMockBuilder(AbstractTableGateway::class) + ->onlyMethods([]) + ->getMock(); + + $tgReflection = new ReflectionClass(AbstractTableGateway::class); + $tableProp = $tgReflection->getProperty('table'); + $tableProp->setValue($stub, 'foo'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This table does not have an Adapter setup'); + + $stub->initialize(); + } + + public function testInitializeThrowsExceptionWithoutTable(): void + { + $stub = $this->getMockBuilder(AbstractTableGateway::class) + ->onlyMethods([]) + ->getMock(); + + $tgReflection = new ReflectionClass(AbstractTableGateway::class); + $adapterProp = $tgReflection->getProperty('adapter'); + $adapterProp->setValue($stub, $this->mockAdapter); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('This table object does not have a valid table set.'); + + $stub->initialize(); + } + + public function testGetColumns(): void + { + $tgReflection = new ReflectionClass(AbstractTableGateway::class); + $columnsProp = $tgReflection->getProperty('columns'); + $columnsProp->setValue($this->table, ['id', 'name', 'email']); + + self::assertEquals(['id', 'name', 'email'], $this->table->getColumns()); + } + + public function testGetFeatureSet(): void + { + self::assertSame($this->mockFeatureSet, $this->table->getFeatureSet()); + } + + public function testSelectWithClosure(): void + { + $mockSelect = $this->mockSelect; + $mockSelect->expects($this->any()) + ->method('getRawState') + ->willReturn([ + 'table' => $this->table->getTable(), + 'columns' => [], + ]); + + $closureCalled = false; + $result = $this->table->select(function ($select) use (&$closureCalled) { + $closureCalled = true; + self::assertInstanceOf(Select::class, $select); + }); + + self::assertTrue($closureCalled); + self::assertInstanceOf(ResultSet::class, $result); + } + + public function testInsertWith(): void + { + $insert = new Insert('foo'); + $insert->values(['column' => 'value']); + + $affectedRows = $this->table->insertWith($insert); + self::assertEquals(5, $affectedRows); + } + + public function testUpdateWith(): void + { + $update = $this->getMockBuilder(Update::class) + ->onlyMethods(['getRawState']) + ->setConstructorArgs(['foo']) + ->getMock(); + + $update->expects($this->any()) + ->method('getRawState') + ->willReturn(['table' => 'foo']); + + $affectedRows = $this->table->updateWith($update); + self::assertEquals(5, $affectedRows); + } + + public function testDeleteWith(): void + { + $delete = $this->getMockBuilder(Delete::class) + ->onlyMethods(['getRawState']) + ->setConstructorArgs(['foo']) + ->getMock(); + + $delete->expects($this->any()) + ->method('getRawState') + ->willReturn(['table' => 'foo']); + + $affectedRows = $this->table->deleteWith($delete); + self::assertEquals(5, $affectedRows); + } + + public function testDeleteWithClosure(): void + { + // The closure receives the Delete object created by $this->sql->delete() + // We verify that the closure is called with a Delete instance + $closureCalled = false; + $affectedRows = $this->table->delete(function ($delete) use (&$closureCalled) { + $closureCalled = true; + self::assertInstanceOf(Delete::class, $delete); + }); + + self::assertTrue($closureCalled); + self::assertEquals(5, $affectedRows); + } + + // @codingStandardsIgnoreStart + public function test__getTable(): void + { + // @codingStandardsIgnoreEnd + self::assertEquals('foo', $this->table->table); + } + + // @codingStandardsIgnoreStart + public function test__getThrowsExceptionForInvalidProperty(): void + { + // @codingStandardsIgnoreEnd + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid magic property access'); + + /** @phpstan-ignore expr.resultUnused, property.notFound */ + $this->table->invalidProperty; + } + + // @codingStandardsIgnoreStart + public function test__setThrowsExceptionForInvalidProperty(): void + { + // @codingStandardsIgnoreEnd + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid magic property access'); + + /** @phpstan-ignore property.notFound */ + $this->table->invalidProperty = 'value'; + } + + // @codingStandardsIgnoreStart + public function test__callThrowsExceptionForInvalidMethod(): void + { + // @codingStandardsIgnoreEnd + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid method (invalidMethod) called'); + + /** @phpstan-ignore method.notFound */ + $this->table->invalidMethod(); + } + + // @codingStandardsIgnoreStart + public function test__cloneWithTableIdentifier(): void + { + // @codingStandardsIgnoreEnd + $tableIdentifier = new Sql\TableIdentifier('bar', 'schema'); + + $tgReflection = new ReflectionClass(AbstractTableGateway::class); + $tableProp = $tgReflection->getProperty('table'); + $tableProp->setValue($this->table, $tableIdentifier); + + $cloned = clone $this->table; + + // The table should be cloned, not the same instance + self::assertNotSame($tableIdentifier, $cloned->getTable()); + self::assertEquals($tableIdentifier->getTable(), $cloned->getTable()->getTable()); + } + + // @codingStandardsIgnoreStart + public function test__cloneWithAliasedTableIdentifier(): void + { + // @codingStandardsIgnoreEnd + $tableIdentifier = new Sql\TableIdentifier('bar', 'schema'); + $aliasedTable = ['alias' => $tableIdentifier]; + + $tgReflection = new ReflectionClass(AbstractTableGateway::class); + $tableProp = $tgReflection->getProperty('table'); + $tableProp->setValue($this->table, $aliasedTable); + + $cloned = clone $this->table; + + $clonedTable = $cloned->getTable(); + self::assertIsArray($clonedTable); + // The TableIdentifier inside the array should be cloned + self::assertNotSame($tableIdentifier, $clonedTable['alias']); + } + + public function testExecuteSelectThrowsExceptionWhenArrayTableDoesNotMatch(): void + { + $select = $this->getMockBuilder(Select::class) + ->onlyMethods(['getRawState']) + ->setConstructorArgs(['bar']) + ->getMock(); + + // With an array table that doesn't end with 'foo', exception should be thrown + $select->expects($this->any()) + ->method('getRawState') + ->willReturn([ + 'table' => ['alias' => 'bar'], + 'columns' => [Select::SQL_STAR], + ]); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The table name of the provided Select object must match that of the table'); + + $this->table->selectWith($select); + } + + public function testExecuteInsertThrowsExceptionWhenTableDoesNotMatch(): void + { + $insert = new Insert('bar'); + $insert->values(['name' => 'test']); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The table name of the provided Insert object must match that of the table'); + + $this->table->insertWith($insert); + } + + public function testExecuteUpdateThrowsExceptionWhenTableDoesNotMatch(): void + { + $update = new Update('bar'); + $update->set(['name' => 'test']); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The table name of the provided Update object must match that of the table'); + + $this->table->updateWith($update); + } + + public function testExecuteDeleteThrowsExceptionWhenTableDoesNotMatch(): void + { + $delete = new Delete('bar'); + $delete->where(['id' => 1]); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The table name of the provided Delete object must match that of the table'); + + $this->table->deleteWith($delete); + } + + public function testSelectAppliesColumnsWhenStarSelected(): void + { + // Set up columns on the table + $tgReflection = new ReflectionClass(AbstractTableGateway::class); + $columnsProp = $tgReflection->getProperty('columns'); + $columnsProp->setValue($this->table, ['id', 'name', 'email']); + + $select = $this->getMockBuilder(Select::class) + ->onlyMethods(['getRawState', 'columns']) + ->setConstructorArgs(['foo']) + ->getMock(); + + $select->expects($this->any()) + ->method('getRawState') + ->willReturn([ + 'table' => 'foo', + 'columns' => [Select::SQL_STAR], + ]); + + $select->expects($this->once()) + ->method('columns') + ->with(['id', 'name', 'email']); + + $this->table->selectWith($select); + } + + // @codingStandardsIgnoreStart + public function test__getLastInsertValue(): void + { + // @codingStandardsIgnoreEnd + self::assertNull($this->table->lastInsertValue); + } + + // @codingStandardsIgnoreStart + public function test__getAdapter(): void + { + // @codingStandardsIgnoreEnd + self::assertSame($this->mockAdapter, $this->table->adapter); + } + + // @codingStandardsIgnoreStart + public function test__getWithFeatureSetMagicGet(): void + { + // @codingStandardsIgnoreEnd + // Create a custom feature that can handle magic get + $feature = new class extends AbstractFeature { + /** + * @return array> + */ + public function getMagicMethodSpecifications(): array + { + return ['get' => ['customProperty']]; + } + }; + + // Create a FeatureSet mock that returns true for canCallMagicGet + $featureSet = $this->getMockBuilder(FeatureSet::class) + ->onlyMethods(['canCallMagicGet', 'callMagicGet']) + ->getMock(); + $featureSet->expects($this->once()) + ->method('canCallMagicGet') + ->with('customProperty') + ->willReturn(true); + $featureSet->expects($this->once()) + ->method('callMagicGet') + ->with('customProperty') + ->willReturn('customValue'); + + $tgReflection = new ReflectionClass(AbstractTableGateway::class); + $featureSetProp = $tgReflection->getProperty('featureSet'); + $featureSetProp->setValue($this->table, $featureSet); + + /** @phpstan-ignore property.notFound */ + $result = $this->table->customProperty; + + self::assertEquals('customValue', $result); + } + + // @codingStandardsIgnoreStart + public function test__setWithFeatureSetMagicSet(): void + { + // @codingStandardsIgnoreEnd + // Create a FeatureSet mock that returns true for canCallMagicSet + $featureSet = $this->getMockBuilder(FeatureSet::class) + ->onlyMethods(['canCallMagicSet', 'callMagicSet']) + ->getMock(); + $featureSet->expects($this->once()) + ->method('canCallMagicSet') + ->with('customProperty') + ->willReturn(true); + $featureSet->expects($this->once()) + ->method('callMagicSet') + ->with('customProperty', 'customValue'); + + $tgReflection = new ReflectionClass(AbstractTableGateway::class); + $featureSetProp = $tgReflection->getProperty('featureSet'); + $featureSetProp->setValue($this->table, $featureSet); + + /** @phpstan-ignore property.notFound */ + $this->table->customProperty = 'customValue'; + } + + // @codingStandardsIgnoreStart + public function test__callWithFeatureSetMagicCall(): void + { + // @codingStandardsIgnoreEnd + // Create a FeatureSet mock that returns true for canCallMagicCall + $featureSet = $this->getMockBuilder(FeatureSet::class) + ->onlyMethods(['canCallMagicCall', 'callMagicCall']) + ->getMock(); + $featureSet->expects($this->once()) + ->method('canCallMagicCall') + ->with('customMethod') + ->willReturn(true); + $featureSet->expects($this->once()) + ->method('callMagicCall') + ->with('customMethod', ['arg1', 'arg2']) + ->willReturn('customResult'); + + $tgReflection = new ReflectionClass(AbstractTableGateway::class); + $featureSetProp = $tgReflection->getProperty('featureSet'); + $featureSetProp->setValue($this->table, $featureSet); + + /** @phpstan-ignore method.notFound */ + $result = $this->table->customMethod('arg1', 'arg2'); + + self::assertEquals('customResult', $result); + } } diff --git a/test/unit/TableGateway/Feature/AbstractFeatureTest.php b/test/unit/TableGateway/Feature/AbstractFeatureTest.php new file mode 100644 index 000000000..cd604fd13 --- /dev/null +++ b/test/unit/TableGateway/Feature/AbstractFeatureTest.php @@ -0,0 +1,62 @@ +feature = $this->getMockBuilder(AbstractFeature::class) + ->disableOriginalConstructor() + ->onlyMethods([]) + ->getMock(); + } + + public function testGetNameReturnsClassName(): void + { + $name = $this->feature->getName(); + + self::assertNotEmpty($name); + } + + public function testSetTableGateway(): void + { + /** @var AbstractTableGateway&MockObject $tableGateway */ + $tableGateway = $this->getMockBuilder(AbstractTableGateway::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->feature->setTableGateway($tableGateway); + + $reflection = new ReflectionProperty(AbstractFeature::class, 'tableGateway'); + $value = $reflection->getValue($this->feature); + + self::assertSame($tableGateway, $value); + } + + public function testInitializeDoesNothing(): void + { + // initialize() is a no-op, just verify it doesn't throw + $this->feature->initialize(); + + /** @phpstan-ignore staticMethod.alreadyNarrowedType */ + self::assertTrue(true); + } + + public function testGetMagicMethodSpecificationsReturnsEmptyArray(): void + { + $result = $this->feature->getMagicMethodSpecifications(); + + self::assertEmpty($result); + } +} diff --git a/test/unit/TableGateway/Feature/EventFeature/TableGatewayEventTest.php b/test/unit/TableGateway/Feature/EventFeature/TableGatewayEventTest.php new file mode 100644 index 000000000..0c0fd556d --- /dev/null +++ b/test/unit/TableGateway/Feature/EventFeature/TableGatewayEventTest.php @@ -0,0 +1,101 @@ +event = new TableGatewayEvent(); + } + + public function testSetNameAndGetName(): void + { + self::assertNull($this->event->getName()); + + $this->event->setName('test.event'); + + self::assertEquals('test.event', $this->event->getName()); + } + + public function testSetTargetAndGetTarget(): void + { + /** @var AbstractTableGateway&MockObject $tableGateway */ + $tableGateway = $this->getMockBuilder(AbstractTableGateway::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->event->setTarget($tableGateway); + + self::assertSame($tableGateway, $this->event->getTarget()); + } + + public function testSetParamsAndGetParams(): void + { + self::assertEquals([], $this->event->getParams()); + + $params = ['key1' => 'value1', 'key2' => 'value2']; + $this->event->setParams($params); + + self::assertEquals($params, $this->event->getParams()); + } + + public function testSetParamsWithObject(): void + { + $params = new stdClass(); + $params->key = 'value'; + + $this->event->setParams($params); + + self::assertSame($params, $this->event->getParams()); + } + + public function testSetParamAndGetParam(): void + { + self::assertNull($this->event->getParam('unknown')); + self::assertEquals('default', $this->event->getParam('unknown', 'default')); + + $this->event->setParam('myParam', 'myValue'); + + self::assertEquals('myValue', $this->event->getParam('myParam')); + } + + public function testGetParamWithDefault(): void + { + $result = $this->event->getParam('nonExistent', 'defaultValue'); + + self::assertEquals('defaultValue', $result); + } + + public function testStopPropagation(): void + { + // stopPropagation should do nothing, just ensure it doesn't throw + $this->event->stopPropagation(true); + $this->event->stopPropagation(false); + + /** @phpstan-ignore staticMethod.impossibleType */ + self::assertFalse($this->event->propagationIsStopped()); + } + + public function testPropagationIsStoppedAlwaysReturnsFalse(): void + { + /** @phpstan-ignore staticMethod.impossibleType */ + self::assertFalse($this->event->propagationIsStopped()); + + $this->event->stopPropagation(true); + + // Still returns false as per implementation + /** @phpstan-ignore staticMethod.impossibleType */ + self::assertFalse($this->event->propagationIsStopped()); + } +} diff --git a/test/unit/TableGateway/Feature/EventFeatureTest.php b/test/unit/TableGateway/Feature/EventFeatureTest.php index 7e42b86d0..4766ad5f2 100644 --- a/test/unit/TableGateway/Feature/EventFeatureTest.php +++ b/test/unit/TableGateway/Feature/EventFeatureTest.php @@ -5,6 +5,7 @@ namespace PhpDbTest\TableGateway\Feature; use Laminas\EventManager\EventManager; +use Laminas\EventManager\EventManagerInterface; use Override; use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\Driver\StatementInterface; @@ -284,4 +285,40 @@ function (EventFeature\TableGatewayEvent $e) use (&$closureHasRun, &$event): voi self::assertSame($stmt, $event->getParam('statement')); self::assertSame($result, $event->getParam('result')); } + + public function testConstructorWithDefaults(): void + { + $feature = new EventFeature(); + + self::assertInstanceOf(EventManagerInterface::class, $feature->getEventManager()); + self::assertInstanceOf(EventFeature\TableGatewayEvent::class, $feature->getEvent()); + } + + /** + * @throws Exception + */ + public function testPreInitializeAddsIdentifiersForCustomTableGatewayClass(): void + { + // Create a custom subclass of TableGateway (using anonymous class) + $customTableGateway = new class extends TableGateway { + public function __construct() + { + // Skip parent constructor + } + }; + + $eventManager = new EventManager(); + $feature = new EventFeature($eventManager); + $feature->setTableGateway($customTableGateway); + + // The custom class name should be added as an identifier + $feature->preInitialize(); + + // Get the identifiers from the event manager + $identifiers = $eventManager->getIdentifiers(); + + // Should contain both TableGateway::class and the anonymous class name + self::assertContains(TableGateway::class, $identifiers); + self::assertContains($customTableGateway::class, $identifiers); + } } diff --git a/test/unit/TableGateway/Feature/FeatureSetTest.php b/test/unit/TableGateway/Feature/FeatureSetTest.php index 7a5bcbaa5..934320611 100644 --- a/test/unit/TableGateway/Feature/FeatureSetTest.php +++ b/test/unit/TableGateway/Feature/FeatureSetTest.php @@ -4,16 +4,14 @@ namespace PhpDbTest\TableGateway\Feature; -use PhpDb\Adapter\Adapter; use PhpDb\Adapter\AdapterInterface; use PhpDb\Adapter\Driver\DriverInterface; -use PhpDb\Adapter\Driver\Pgsql\Result; use PhpDb\Adapter\Driver\StatementInterface; -use PhpDb\Adapter\Platform\Postgresql; use PhpDb\Adapter\Platform\Sql92; use PhpDb\Metadata\MetadataInterface; use PhpDb\Metadata\Object\ConstraintObject; use PhpDb\TableGateway\AbstractTableGateway; +use PhpDb\TableGateway\Feature\AbstractFeature; use PhpDb\TableGateway\Feature\FeatureSet; use PhpDb\TableGateway\Feature\MasterSlaveFeature; use PhpDb\TableGateway\Feature\MetadataFeature; @@ -24,10 +22,19 @@ use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; -use ReflectionClass; #[IgnoreDeprecations] #[RequiresPhp('<= 8.6')] +#[CoversMethod(FeatureSet::class, '__construct')] +#[CoversMethod(FeatureSet::class, 'setTableGateway')] +#[CoversMethod(FeatureSet::class, 'getFeatureByClassName')] +#[CoversMethod(FeatureSet::class, 'addFeatures')] +#[CoversMethod(FeatureSet::class, 'addFeature')] +#[CoversMethod(FeatureSet::class, 'apply')] +#[CoversMethod(FeatureSet::class, 'canCallMagicGet')] +#[CoversMethod(FeatureSet::class, 'callMagicGet')] +#[CoversMethod(FeatureSet::class, 'canCallMagicSet')] +#[CoversMethod(FeatureSet::class, 'callMagicSet')] #[CoversMethod(FeatureSet::class, 'canCallMagicCall')] #[CoversMethod(FeatureSet::class, 'callMagicCall')] class FeatureSetTest extends TestCase @@ -57,7 +64,6 @@ public function testAddFeatureThatFeatureDoesNotHaveTableGatewayButFeatureSetHas $tableGatewayMock = $this->getMockBuilder(AbstractTableGateway::class)->onlyMethods([])->getMock(); - //feature doesn't have tableGateway, but FeatureSet has $feature = new MasterSlaveFeature($mockSlaveAdapter); $featureSet = new FeatureSet(); @@ -84,7 +90,6 @@ public function testAddFeatureThatFeatureHasTableGatewayButFeatureSetDoesNotHave $metadataMock->expects($this->any())->method('getConstraints')->willReturn([$constraintObject]); - //feature have tableGateway, but FeatureSet doesn't has $feature = new MetadataFeature($metadataMock); $feature->setTableGateway($tableGatewayMock); @@ -126,49 +131,192 @@ public function testCanCallMagicCallReturnsFalseWhenNoFeaturesHaveBeenAdded(): v public function testCallMagicCallSucceedsForValidMethodOfAddedFeature(): void { - $this->markTestSkipped('This needs refactored to use a custom TestFeature and Sql92'); - /** @phpstan-ignore deadCode.unreachable */ - $sequenceName = 'table_sequence'; + $feature = new class extends AbstractFeature { + public function customMethod(array $args): string + { + return 'result: ' . ($args[0] ?? 'default'); + } + }; - $platformMock = $this->getMockBuilder(Postgresql::class)->getMock(); - $platformMock->expects($this->any()) - ->method('getName')->willReturn('PostgreSQL'); + $featureSet = new FeatureSet(); + $featureSet->addFeature($feature); + + $result = $featureSet->callMagicCall('customMethod', ['test_value']); - $resultMock = $this->getMockBuilder(Result::class)->getMock(); - $resultMock->expects($this->any()) - ->method('current') - ->willReturn(['currval' => 1]); + self::assertEquals('result: test_value', $result); + } - $statementMock = $this->getMockBuilder(StatementInterface::class)->getMock(); - $statementMock->expects($this->any()) - ->method('prepare') - ->with('SELECT CURRVAL(\'' . $sequenceName . '\')'); - $statementMock->expects($this->any()) - ->method('execute') - ->willReturn($resultMock); + public function testConstructorWithFeatures(): void + { + $feature = new SequenceFeature('id', 'table_sequence'); + $featureSet = new FeatureSet([$feature]); - $adapterMock = $this->getMockBuilder(Adapter::class) + self::assertSame($feature, $featureSet->getFeatureByClassName(SequenceFeature::class)); + } + + public function testSetTableGateway(): void + { + $tableGatewayMock = $this->getMockBuilder(AbstractTableGateway::class) ->disableOriginalConstructor() ->getMock(); - $adapterMock->expects($this->any()) - ->method('getPlatform')->willReturn($platformMock); - $adapterMock->expects($this->any()) - ->method('createStatement')->willReturn($statementMock); + $feature = new SequenceFeature('id', 'table_sequence'); + $featureSet = new FeatureSet([$feature]); + + $result = $featureSet->setTableGateway($tableGatewayMock); + + self::assertSame($featureSet, $result); + } + + public function testGetFeatureByClassNameReturnsNullWhenNotFound(): void + { + $featureSet = new FeatureSet(); + + $result = $featureSet->getFeatureByClassName(SequenceFeature::class); + + self::assertNull($result); + } + + public function testAddFeaturesReturnsFluentInterface(): void + { + $feature1 = new SequenceFeature('id', 'seq1'); + $feature2 = new SequenceFeature('id', 'seq2'); + + $featureSet = new FeatureSet(); + $result = $featureSet->addFeatures([$feature1, $feature2]); + + self::assertSame($featureSet, $result); + } + + public function testApplyCallsMethodOnFeatures(): void + { $tableGatewayMock = $this->getMockBuilder(AbstractTableGateway::class) ->disableOriginalConstructor() ->getMock(); - $reflectionClass = new ReflectionClass(AbstractTableGateway::class); - $reflectionProperty = $reflectionClass->getProperty('adapter'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($tableGatewayMock, $adapterMock); + $feature = new MasterSlaveFeature( + $this->getMockBuilder(AdapterInterface::class)->getMock() + ); - $feature = new SequenceFeature('id', 'table_sequence'); - $feature->setTableGateway($tableGatewayMock); + $featureSet = new FeatureSet([$feature]); + $featureSet->setTableGateway($tableGatewayMock); + + $featureSet->apply('preSelect', []); + + /** @phpstan-ignore staticMethod.alreadyNarrowedType */ + self::assertTrue(true); + } + + public function testApplySkipsFeatureWithoutMethod(): void + { + $feature = new SequenceFeature('id', 'table_sequence'); + $featureSet = new FeatureSet([$feature]); + + $featureSet->apply('nonExistentMethod', []); + + /** @phpstan-ignore staticMethod.alreadyNarrowedType */ + self::assertTrue(true); + } + + public function testCanCallMagicGetReturnsFalse(): void + { $featureSet = new FeatureSet(); - $featureSet->addFeature($feature); - self::assertEquals(1, $featureSet->callMagicCall('lastSequenceId', [])); + + self::assertFalse($featureSet->canCallMagicGet('property')); + } + + public function testCallMagicGetReturnsNull(): void + { + $featureSet = new FeatureSet(); + + self::assertNull($featureSet->callMagicGet('property')); + } + + public function testCanCallMagicSetReturnsFalse(): void + { + $featureSet = new FeatureSet(); + + self::assertFalse($featureSet->canCallMagicSet('property')); + } + + public function testCallMagicSetReturnsNull(): void + { + $featureSet = new FeatureSet(); + + self::assertNull($featureSet->callMagicSet('property', 'value')); + } + + public function testCallMagicCallReturnsNullWhenNoFeatureHasMethod(): void + { + $featureSet = new FeatureSet(); + + self::assertNull($featureSet->callMagicCall('nonExistentMethod', [])); + } + + public function testApplyHaltsWhenFeatureReturnsHalt(): void + { + $feature1 = new class extends AbstractFeature { + public bool $called = false; + public function testMethod(): string + { + $this->called = true; + return FeatureSet::APPLY_HALT; + } + }; + + $feature2 = new class extends AbstractFeature { + public bool $called = false; + public function testMethod(): void + { + $this->called = true; + } + }; + + $featureSet = new FeatureSet([$feature1, $feature2]); + $featureSet->apply('testMethod', []); + + self::assertTrue($feature1->called); + self::assertFalse($feature2->called); + } + + public function testApplyCallsAllFeaturesWhenNoHalt(): void + { + $feature1 = new class extends AbstractFeature { + public bool $called = false; + public function testMethod(): void + { + $this->called = true; + } + }; + + $feature2 = new class extends AbstractFeature { + public bool $called = false; + public function testMethod(): void + { + $this->called = true; + } + }; + + $featureSet = new FeatureSet([$feature1, $feature2]); + $featureSet->apply('testMethod', []); + + self::assertTrue($feature1->called); + self::assertTrue($feature2->called); + } + + public function testApplyPassesArgumentsToFeatures(): void + { + $feature = new class extends AbstractFeature { + public mixed $receivedArg; + public function testMethod(string $arg): void + { + $this->receivedArg = $arg; + } + }; + + $featureSet = new FeatureSet([$feature]); + $featureSet->apply('testMethod', ['test value']); + + self::assertEquals('test value', $feature->receivedArg); } } diff --git a/test/unit/TableGateway/Feature/GlobalAdapterFeatureTest.php b/test/unit/TableGateway/Feature/GlobalAdapterFeatureTest.php new file mode 100644 index 000000000..fb576cb82 --- /dev/null +++ b/test/unit/TableGateway/Feature/GlobalAdapterFeatureTest.php @@ -0,0 +1,123 @@ +setValue(null, []); + } + + protected function tearDown(): void + { + // Clean up static adapters after each test + $reflection = new ReflectionProperty(GlobalAdapterFeature::class, 'staticAdapters'); + $reflection->setValue(null, []); + } + + public function testSetStaticAdapter(): void + { + $adapter = $this->createMock(AdapterInterface::class); + + GlobalAdapterFeature::setStaticAdapter($adapter); + + $result = GlobalAdapterFeature::getStaticAdapter(); + self::assertSame($adapter, $result); + } + + public function testGetStaticAdapterThrowsExceptionWhenNoAdapterSet(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('No database adapter was found in the static registry.'); + + GlobalAdapterFeature::getStaticAdapter(); + } + + public function testPreInitializeSetsAdapterOnTableGateway(): void + { + $adapter = $this->createMock(AdapterInterface::class); + GlobalAdapterFeature::setStaticAdapter($adapter); + + /** @var AbstractTableGateway&MockObject $tableGatewayMock */ + $tableGatewayMock = $this->getMockBuilder(AbstractTableGateway::class) + ->disableOriginalConstructor() + ->getMock(); + + $feature = new GlobalAdapterFeature(); + $feature->setTableGateway($tableGatewayMock); + + $feature->preInitialize(); + + // Verify adapter was set on table gateway + $reflection = new ReflectionProperty(AbstractTableGateway::class, 'adapter'); + $result = $reflection->getValue($tableGatewayMock); + + self::assertSame($adapter, $result); + } + + public function testGetStaticAdapterReturnsDefaultAdapterWhenClassSpecificNotSet(): void + { + $adapter = $this->createMock(AdapterInterface::class); + + // Set adapter on the base class + GlobalAdapterFeature::setStaticAdapter($adapter); + + // Get adapter should return the default adapter + $result = GlobalAdapterFeature::getStaticAdapter(); + + self::assertSame($adapter, $result); + } + + public function testSubclassCanSetAndGetOwnAdapter(): void + { + $baseAdapter = $this->createMock(AdapterInterface::class); + $subclassAdapter = $this->createMock(AdapterInterface::class); + + // Set default adapter on base class + GlobalAdapterFeature::setStaticAdapter($baseAdapter); + + // Set a different adapter on the subclass + TestGlobalAdapterFeatureSubclass::setStaticAdapter($subclassAdapter); + + // Base class should return base adapter + self::assertSame($baseAdapter, GlobalAdapterFeature::getStaticAdapter()); + + // Subclass should return its own adapter + self::assertSame($subclassAdapter, TestGlobalAdapterFeatureSubclass::getStaticAdapter()); + } + + public function testSubclassFallsBackToDefaultAdapterWhenNoSpecificAdapterSet(): void + { + $defaultAdapter = $this->createMock(AdapterInterface::class); + + // Only set adapter on base class + GlobalAdapterFeature::setStaticAdapter($defaultAdapter); + + // Subclass should fall back to default adapter + $result = TestGlobalAdapterFeatureSubclass::getStaticAdapter(); + + self::assertSame($defaultAdapter, $result); + } + + public function testSubclassThrowsExceptionWhenNoAdaptersSet(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('No database adapter was found in the static registry.'); + + TestGlobalAdapterFeatureSubclass::getStaticAdapter(); + } +} diff --git a/test/unit/TableGateway/Feature/MasterSlaveFeatureTest.php b/test/unit/TableGateway/Feature/MasterSlaveFeatureTest.php index 7a73e9d64..7fed1e33a 100644 --- a/test/unit/TableGateway/Feature/MasterSlaveFeatureTest.php +++ b/test/unit/TableGateway/Feature/MasterSlaveFeatureTest.php @@ -10,6 +10,7 @@ use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\Driver\StatementInterface; use PhpDb\Adapter\Platform\Sql92; +use PhpDb\Sql\Sql; use PhpDb\TableGateway\Feature\MasterSlaveFeature; use PhpDb\TableGateway\TableGateway; use PHPUnit\Framework\MockObject\Exception; @@ -113,4 +114,37 @@ public function testPostSelect(): void // test that the sql object is restored self::assertSame($masterSql, $table->getSql()); } + + public function testGetSlaveAdapter(): void + { + self::assertSame($this->mockSlaveAdapter, $this->feature->getSlaveAdapter()); + } + + /** + * @throws Exception + */ + public function testConstructorWithSlaveSql(): void + { + $slaveSql = new Sql($this->mockSlaveAdapter, 'foo'); + $feature = new MasterSlaveFeature($this->mockSlaveAdapter, $slaveSql); + + self::assertSame($slaveSql, $feature->getSlaveSql()); + } + + /** + * @throws Exception + */ + public function testPostInitializeWithProvidedSlaveSql(): void + { + $slaveSql = new Sql($this->mockSlaveAdapter, 'foo'); + $feature = new MasterSlaveFeature($this->mockSlaveAdapter, $slaveSql); + + $this->getMockBuilder(TableGateway::class) + ->setConstructorArgs(['foo', $this->mockMasterAdapter, $feature]) + ->onlyMethods([]) + ->getMock(); + + // The provided slaveSql should be used instead of creating a new one + self::assertSame($slaveSql, $feature->getSlaveSql()); + } } diff --git a/test/unit/TableGateway/Feature/MetadataFeatureTest.php b/test/unit/TableGateway/Feature/MetadataFeatureTest.php index 79385a1fe..646a3097d 100644 --- a/test/unit/TableGateway/Feature/MetadataFeatureTest.php +++ b/test/unit/TableGateway/Feature/MetadataFeatureTest.php @@ -8,7 +8,9 @@ use PhpDb\Metadata\Object\ConstraintObject; use PhpDb\Metadata\Object\TableObject; use PhpDb\Metadata\Object\ViewObject; +use PhpDb\Sql\TableIdentifier; use PhpDb\TableGateway\AbstractTableGateway; +use PhpDb\TableGateway\Exception\RuntimeException; use PhpDb\TableGateway\Feature\MetadataFeature; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\IgnoreDeprecations; @@ -56,12 +58,14 @@ public function testPostInitialize(): void */ public function testPostInitializeRecordsPrimaryKeyColumnToSharedMetadata(): void { - $this->markTestSkipped('This should be an integration test'); - /** @var AbstractTableGateway&MockObject $tableGatewayMock */ - /** @phpstan-ignore deadCode.unreachable */ $tableGatewayMock = $this->getMockBuilder(AbstractTableGateway::class)->onlyMethods([])->getMock(); - $metadataMock = $this->getMockBuilder(MetadataInterface::class)->getMock(); + + // Set the table property on the mock using reflection + $tableProperty = new ReflectionProperty(AbstractTableGateway::class, 'table'); + $tableProperty->setValue($tableGatewayMock, 'foo'); + + $metadataMock = $this->getMockBuilder(MetadataInterface::class)->getMock(); $metadataMock->expects($this->any())->method('getColumnNames')->willReturn(['id', 'name']); $metadataMock->expects($this->any()) ->method('getTable') @@ -77,9 +81,7 @@ public function testPostInitializeRecordsPrimaryKeyColumnToSharedMetadata(): voi $feature->setTableGateway($tableGatewayMock); $feature->postInitialize(); - $r = new ReflectionProperty(MetadataFeature::class, 'sharedData'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $r->setAccessible(true); + $r = new ReflectionProperty(MetadataFeature::class, 'sharedData'); $sharedData = $r->getValue($feature); self::assertIsArray($sharedData); @@ -96,12 +98,14 @@ public function testPostInitializeRecordsPrimaryKeyColumnToSharedMetadata(): voi */ public function testPostInitializeRecordsListOfColumnsInPrimaryKeyToSharedMetadata(): void { - $this->markTestSkipped('This should be an integration test'); - /** @var AbstractTableGateway&MockObject $tableGatewayMock */ - /** @phpstan-ignore deadCode.unreachable */ $tableGatewayMock = $this->getMockBuilder(AbstractTableGateway::class)->onlyMethods([])->getMock(); - $metadataMock = $this->getMockBuilder(MetadataInterface::class)->getMock(); + + // Set the table property on the mock using reflection + $tableProperty = new ReflectionProperty(AbstractTableGateway::class, 'table'); + $tableProperty->setValue($tableGatewayMock, 'foo'); + + $metadataMock = $this->getMockBuilder(MetadataInterface::class)->getMock(); $metadataMock->expects($this->any())->method('getColumnNames')->willReturn(['id', 'name']); $metadataMock->expects($this->any()) ->method('getTable') @@ -117,9 +121,7 @@ public function testPostInitializeRecordsListOfColumnsInPrimaryKeyToSharedMetada $feature->setTableGateway($tableGatewayMock); $feature->postInitialize(); - $r = new ReflectionProperty(MetadataFeature::class, 'sharedData'); - /** @noinspection PhpExpressionResultUnusedInspection */ - $r->setAccessible(true); + $r = new ReflectionProperty(MetadataFeature::class, 'sharedData'); $sharedData = $r->getValue($feature); self::assertIsArray($sharedData); @@ -138,7 +140,12 @@ public function testPostInitializeSkipsPrimaryKeyCheckIfNotTable(): void { /** @var AbstractTableGateway&MockObject $tableGatewayMock */ $tableGatewayMock = $this->getMockBuilder(AbstractTableGateway::class)->onlyMethods([])->getMock(); - $metadataMock = $this->getMockBuilder(MetadataInterface::class)->getMock(); + + // Set the table property on the mock using reflection + $tableProperty = new ReflectionProperty(AbstractTableGateway::class, 'table'); + $tableProperty->setValue($tableGatewayMock, 'foo'); + + $metadataMock = $this->getMockBuilder(MetadataInterface::class)->getMock(); $metadataMock->expects($this->any())->method('getColumnNames')->willReturn(['id', 'name']); $metadataMock->expects($this->any()) ->method('getTable') @@ -150,4 +157,130 @@ public function testPostInitializeSkipsPrimaryKeyCheckIfNotTable(): void $feature->setTableGateway($tableGatewayMock); $feature->postInitialize(); } + + /** + * @throws Exception + * @throws \Exception + */ + public function testPostInitializeThrowsExceptionWhenNoPrimaryKeyFound(): void + { + /** @var AbstractTableGateway&MockObject $tableGatewayMock */ + $tableGatewayMock = $this->getMockBuilder(AbstractTableGateway::class)->onlyMethods([])->getMock(); + + // Set the table property on the mock using reflection + $tableProperty = new ReflectionProperty(AbstractTableGateway::class, 'table'); + $tableProperty->setValue($tableGatewayMock, 'foo'); + + $metadataMock = $this->getMockBuilder(MetadataInterface::class)->getMock(); + $metadataMock->expects($this->any())->method('getColumnNames')->willReturn(['id', 'name']); + $metadataMock->expects($this->any()) + ->method('getTable') + ->willReturn(new TableObject('foo')); + + // Return empty constraints - no PRIMARY KEY + $metadataMock->expects($this->any())->method('getConstraints')->willReturn([]); + + $feature = new MetadataFeature($metadataMock); + $feature->setTableGateway($tableGatewayMock); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('A primary key for this column could not be found in the metadata.'); + + $feature->postInitialize(); + } + + /** + * @throws Exception + * @throws \Exception + */ + public function testPostInitializeWithTableIdentifier(): void + { + /** @var AbstractTableGateway&MockObject $tableGatewayMock */ + $tableGatewayMock = $this->getMockBuilder(AbstractTableGateway::class)->onlyMethods([])->getMock(); + + // Set the table property as a TableIdentifier + $tableIdentifier = new TableIdentifier('foo', 'myschema'); + $tableProperty = new ReflectionProperty(AbstractTableGateway::class, 'table'); + $tableProperty->setValue($tableGatewayMock, $tableIdentifier); + + $metadataMock = $this->getMockBuilder(MetadataInterface::class)->getMock(); + $metadataMock->expects($this->any()) + ->method('getColumnNames') + ->with('foo', 'myschema') + ->willReturn(['id', 'name']); + $metadataMock->expects($this->any()) + ->method('getTable') + ->with('foo', 'myschema') + ->willReturn(new TableObject('foo')); + + $constraintObject = new ConstraintObject('id_pk', 'foo'); + $constraintObject->setColumns(['id']); + $constraintObject->setType('PRIMARY KEY'); + + $metadataMock->expects($this->any()) + ->method('getConstraints') + ->with('foo', 'myschema') + ->willReturn([$constraintObject]); + + $feature = new MetadataFeature($metadataMock); + $feature->setTableGateway($tableGatewayMock); + $feature->postInitialize(); + + $r = new ReflectionProperty(MetadataFeature::class, 'sharedData'); + $sharedData = $r->getValue($feature); + + self::assertSame('id', $sharedData['metadata']['primaryKey']); + } + + /** + * @throws Exception + * @throws \Exception + */ + public function testPostInitializeWithArrayTable(): void + { + /** @var AbstractTableGateway&MockObject $tableGatewayMock */ + $tableGatewayMock = $this->getMockBuilder(AbstractTableGateway::class)->onlyMethods([])->getMock(); + + // Set the table property as an array (aliased table) + $tableProperty = new ReflectionProperty(AbstractTableGateway::class, 'table'); + $tableProperty->setValue($tableGatewayMock, ['t' => 'foo']); + + $metadataMock = $this->getMockBuilder(MetadataInterface::class)->getMock(); + $metadataMock->expects($this->any()) + ->method('getColumnNames') + ->with('foo', null) + ->willReturn(['id', 'name']); + $metadataMock->expects($this->any()) + ->method('getTable') + ->willReturn(new TableObject('foo')); + + $constraintObject = new ConstraintObject('id_pk', 'foo'); + $constraintObject->setColumns(['id']); + $constraintObject->setType('PRIMARY KEY'); + + $metadataMock->expects($this->any())->method('getConstraints')->willReturn([$constraintObject]); + + $feature = new MetadataFeature($metadataMock); + $feature->setTableGateway($tableGatewayMock); + $feature->postInitialize(); + + $r = new ReflectionProperty(MetadataFeature::class, 'sharedData'); + $sharedData = $r->getValue($feature); + + self::assertSame('id', $sharedData['metadata']['primaryKey']); + } + + public function testConstructorSetsInitialSharedData(): void + { + $metadataMock = $this->getMockBuilder(MetadataInterface::class)->getMock(); + $feature = new MetadataFeature($metadataMock); + + $r = new ReflectionProperty(MetadataFeature::class, 'sharedData'); + $sharedData = $r->getValue($feature); + + self::assertIsArray($sharedData); + self::assertArrayHasKey('metadata', $sharedData); + self::assertNull($sharedData['metadata']['primaryKey']); + self::assertEquals([], $sharedData['metadata']['columns']); + } } diff --git a/test/unit/TableGateway/Feature/RowGatewayFeatureTest.php b/test/unit/TableGateway/Feature/RowGatewayFeatureTest.php new file mode 100644 index 000000000..97ae43253 --- /dev/null +++ b/test/unit/TableGateway/Feature/RowGatewayFeatureTest.php @@ -0,0 +1,213 @@ +getMockBuilder(AbstractTableGateway::class) + ->disableOriginalConstructor() + ->getMock(); + + $adapter = $this->createMock(AdapterInterface::class); + + $tableProperty = new ReflectionProperty(AbstractTableGateway::class, 'table'); + $tableProperty->setValue($tableGateway, 'test_table'); + + $adapterProperty = new ReflectionProperty(AbstractTableGateway::class, 'adapter'); + $adapterProperty->setValue($tableGateway, $adapter); + + $resultSetProperty = new ReflectionProperty(AbstractTableGateway::class, 'resultSetPrototype'); + $resultSetProperty->setValue($tableGateway, $resultSetPrototype); + + if ($featureSet !== null) { + $featureSetProperty = new ReflectionProperty(AbstractTableGateway::class, 'featureSet'); + $featureSetProperty->setValue($tableGateway, $featureSet); + } + + return $tableGateway; + } + + public function testPostInitializeWithStringPrimaryKey(): void + { + $resultSet = new ResultSet(); + $tableGateway = $this->createTableGatewayMock($resultSet); + + $feature = new RowGatewayFeature('id'); + $feature->setTableGateway($tableGateway); + + $feature->postInitialize(); + + $prototype = $resultSet->getRowPrototype(); + self::assertInstanceOf(RowGatewayInterface::class, $prototype); + } + + public function testPostInitializeWithRowGatewayInstance(): void + { + $resultSet = new ResultSet(); + + /** @var RowGatewayInterface&MockObject $rowGateway */ + $rowGateway = $this->createMock(RowGatewayInterface::class); + + $tableGateway = $this->createTableGatewayMock($resultSet); + + $feature = new RowGatewayFeature($rowGateway); + $feature->setTableGateway($tableGateway); + + $feature->postInitialize(); + + self::assertSame($rowGateway, $resultSet->getRowPrototype()); + } + + public function testPostInitializeThrowsExceptionForNonResultSet(): void + { + $resultSet = $this->createMock(ResultSetInterface::class); + $tableGateway = $this->createTableGatewayMock($resultSet); + + $feature = new RowGatewayFeature('id'); + $feature->setTableGateway($tableGateway); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('expects the ResultSet to be an instance of'); + + $feature->postInitialize(); + } + + public function testPostInitializeWithMetadataFeature(): void + { + $resultSet = new ResultSet(); + + // Create a MetadataFeature mock with primary key in sharedData + $metadataFeature = $this->getMockBuilder(MetadataFeature::class) + ->disableOriginalConstructor() + ->getMock(); + + // Set sharedData with metadata containing primaryKey + $sharedDataProperty = new ReflectionProperty(MetadataFeature::class, 'sharedData'); + $sharedDataProperty->setValue($metadataFeature, [ + 'metadata' => ['primaryKey' => 'id'], + ]); + + $featureSet = $this->createMock(FeatureSet::class); + $featureSet->expects($this->once()) + ->method('getFeatureByClassName') + ->with(MetadataFeature::class) + ->willReturn($metadataFeature); + + $tableGateway = $this->createTableGatewayMock($resultSet, $featureSet); + + $feature = new RowGatewayFeature(); + $feature->setTableGateway($tableGateway); + + $feature->postInitialize(); + + $prototype = $resultSet->getRowPrototype(); + self::assertInstanceOf(RowGatewayInterface::class, $prototype); + } + + public function testPostInitializeThrowsExceptionWhenNoMetadataAndNoPrimaryKey(): void + { + $resultSet = new ResultSet(); + + $featureSet = $this->createMock(FeatureSet::class); + $featureSet->expects($this->once()) + ->method('getFeatureByClassName') + ->with(MetadataFeature::class) + ->willReturn(null); + + $tableGateway = $this->createTableGatewayMock($resultSet, $featureSet); + + $feature = new RowGatewayFeature(); + $feature->setTableGateway($tableGateway); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('No information was provided to the RowGatewayFeature'); + + $feature->postInitialize(); + } + + public function testPostInitializeThrowsExceptionWhenMetadataHasNoMetadataKey(): void + { + $resultSet = new ResultSet(); + + // Create a MetadataFeature mock without the metadata key in sharedData + $metadataFeature = $this->getMockBuilder(MetadataFeature::class) + ->disableOriginalConstructor() + ->getMock(); + + // Set empty sharedData on the metadata feature + $sharedDataProperty = new ReflectionProperty(MetadataFeature::class, 'sharedData'); + $sharedDataProperty->setValue($metadataFeature, []); + + $featureSet = $this->createMock(FeatureSet::class); + $featureSet->expects($this->once()) + ->method('getFeatureByClassName') + ->with(MetadataFeature::class) + ->willReturn($metadataFeature); + + $tableGateway = $this->createTableGatewayMock($resultSet, $featureSet); + + $feature = new RowGatewayFeature(); + $feature->setTableGateway($tableGateway); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('No information was provided to the RowGatewayFeature'); + + $feature->postInitialize(); + } + + public function testConstructorStoresArguments(): void + { + $feature = new RowGatewayFeature('id'); + + // Use reflection to check the constructorArguments property + $property = new ReflectionProperty(RowGatewayFeature::class, 'constructorArguments'); + $args = $property->getValue($feature); + + self::assertEquals(['id'], $args); + } + + public function testConstructorStoresRowGatewayInstance(): void + { + /** @var RowGatewayInterface&MockObject $rowGateway */ + $rowGateway = $this->createMock(RowGatewayInterface::class); + + $feature = new RowGatewayFeature($rowGateway); + + // Use reflection to check the constructorArguments property + $property = new ReflectionProperty(RowGatewayFeature::class, 'constructorArguments'); + $args = $property->getValue($feature); + + self::assertSame($rowGateway, $args[0]); + } + + public function testConstructorWithNoArguments(): void + { + $feature = new RowGatewayFeature(); + + // Use reflection to check the constructorArguments property + $property = new ReflectionProperty(RowGatewayFeature::class, 'constructorArguments'); + $args = $property->getValue($feature); + + self::assertEquals([], $args); + } +} diff --git a/test/unit/TableGateway/Feature/SequenceFeatureTest.php b/test/unit/TableGateway/Feature/SequenceFeatureTest.php index 27e637e56..f8eaaadac 100644 --- a/test/unit/TableGateway/Feature/SequenceFeatureTest.php +++ b/test/unit/TableGateway/Feature/SequenceFeatureTest.php @@ -9,11 +9,16 @@ use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\Driver\StatementInterface; use PhpDb\Adapter\Platform\PlatformInterface; +use PhpDb\Exception\RuntimeException; +use PhpDb\Sql\Insert; +use PhpDb\TableGateway\AbstractTableGateway; use PhpDb\TableGateway\Feature\SequenceFeature; use PhpDb\TableGateway\TableGateway; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use ReflectionProperty; final class SequenceFeatureTest extends TestCase { @@ -33,6 +38,48 @@ protected function setUp(): void $this->feature = new SequenceFeature($this->primaryKeyField, self::$sequenceName); } + private function createTableGatewayWithPlatform( + string $platformName, + int $sequenceValue = 2 + ): AbstractTableGateway&MockObject { + $platform = $this->createMock(PlatformInterface::class); + $platform->expects($this->any()) + ->method('getName') + ->willReturn($platformName); + $platform->expects($this->any()) + ->method('quoteIdentifier') + ->willReturnCallback(fn($name) => $name); + + $result = $this->createMock(ResultInterface::class); + $result->expects($this->any()) + ->method('current') + ->willReturn(['nextval' => $sequenceValue, 'currval' => $sequenceValue]); + + $statement = $this->createMock(StatementInterface::class); + $statement->expects($this->any()) + ->method('execute') + ->willReturn($result); + + $adapter = $this->getMockBuilder(Adapter::class) + ->onlyMethods(['getPlatform', 'createStatement']) + ->disableOriginalConstructor() + ->getMock(); + $adapter->expects($this->any()) + ->method('getPlatform') + ->willReturn($platform); + $adapter->expects($this->any()) + ->method('createStatement') + ->willReturn($statement); + + /** @var AbstractTableGateway&MockObject $tableGateway */ + $tableGateway = $this->getMockBuilder(TableGateway::class) + ->setConstructorArgs(['table', $adapter]) + ->onlyMethods([]) + ->getMock(); + + return $tableGateway; + } + /** * @throws Exception */ @@ -84,4 +131,164 @@ public static function nextSequenceIdProvider(): array ['Oracle', 'SELECT ' . self::$sequenceName . '.NEXTVAL as "nextval" FROM dual'], ]; } + + public function testPreInsertWhenPrimaryKeyAlreadyInValues(): void + { + $tableGateway = $this->createTableGatewayWithPlatform('PostgreSQL'); + $this->feature->setTableGateway($tableGateway); + + $insert = new Insert('table'); + $insert->columns(['id', 'name']); + $insert->values([42, 'test']); + + $result = $this->feature->preInsert($insert); + + self::assertSame($insert, $result); + + $sequenceValueProp = new ReflectionProperty(SequenceFeature::class, 'sequenceValue'); + self::assertEquals(42, $sequenceValueProp->getValue($this->feature)); + } + + public function testPreInsertGeneratesSequenceWhenPrimaryKeyNotInValues(): void + { + $tableGateway = $this->createTableGatewayWithPlatform('PostgreSQL', 99); + $this->feature->setTableGateway($tableGateway); + + $insert = new Insert('table'); + $insert->columns(['name']); + $insert->values(['test']); + + $result = $this->feature->preInsert($insert); + + self::assertSame($insert, $result); + + $sequenceValueProp = new ReflectionProperty(SequenceFeature::class, 'sequenceValue'); + self::assertEquals(99, $sequenceValueProp->getValue($this->feature)); + + $rawState = $insert->getRawState(); + self::assertContains('id', $rawState['columns']); + } + + public function testPostInsertSetsLastInsertValue(): void + { + $tableGateway = $this->createTableGatewayWithPlatform('PostgreSQL', 123); + $this->feature->setTableGateway($tableGateway); + + $insert = new Insert('table'); + $insert->columns(['name']); + $insert->values(['test']); + $this->feature->preInsert($insert); + + $statement = $this->createMock(StatementInterface::class); + $result = $this->createMock(ResultInterface::class); + + $this->feature->postInsert($statement, $result); + + self::assertEquals(123, $tableGateway->lastInsertValue); + } + + #[DataProvider('lastSequenceIdProvider')] + public function testLastSequenceId(string $platformName): void + { + $tableGateway = $this->createTableGatewayWithPlatform($platformName, 55); + $this->feature->setTableGateway($tableGateway); + + $result = $this->feature->lastSequenceId(); + + self::assertEquals(55, $result); + } + + /** @psalm-return array */ + public static function lastSequenceIdProvider(): array + { + return [ + ['PostgreSQL'], + ['Oracle'], + ]; + } + + public function testNextSequenceIdThrowsExceptionForUnsupportedPlatform(): void + { + $tableGateway = $this->createTableGatewayWithPlatform('MySQL'); + $this->feature->setTableGateway($tableGateway); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Unsupported platform for retrieving next sequence id'); + + $this->feature->nextSequenceId(); + } + + public function testLastSequenceIdThrowsExceptionForUnsupportedPlatform(): void + { + $tableGateway = $this->createTableGatewayWithPlatform('MySQL'); + $this->feature->setTableGateway($tableGateway); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Unsupported platform for retrieving last sequence id'); + + $this->feature->lastSequenceId(); + } + + public function testPostInsertDoesNotSetLastInsertValueWhenSequenceValueIsNull(): void + { + $tableGateway = $this->createTableGatewayWithPlatform('PostgreSQL'); + $this->feature->setTableGateway($tableGateway); + + $lastInsertValueProp = new ReflectionProperty(AbstractTableGateway::class, 'lastInsertValue'); + $lastInsertValueProp->setValue($tableGateway, 999); + + $statement = $this->createMock(StatementInterface::class); + $result = $this->createMock(ResultInterface::class); + + $this->feature->postInsert($statement, $result); + + self::assertEquals(999, $lastInsertValueProp->getValue($tableGateway)); + } + + public function testPreInsertWithPrimaryKeyColumnButNullValue(): void + { + $tableGateway = $this->createTableGatewayWithPlatform('PostgreSQL'); + $this->feature->setTableGateway($tableGateway); + + $insert = new Insert('table'); + $insert->columns(['id', 'name']); + $insert->values([null, 'test']); + + $result = $this->feature->preInsert($insert); + + self::assertSame($insert, $result); + + $sequenceValueProp = new ReflectionProperty(SequenceFeature::class, 'sequenceValue'); + self::assertNull($sequenceValueProp->getValue($this->feature)); + } + + public function testPreInsertReturnsEarlyWhenNextSequenceIdReturnsNull(): void + { + $tableGateway = $this->createTableGatewayWithPlatform('PostgreSQL'); + + $feature = $this->getMockBuilder(SequenceFeature::class) + ->setConstructorArgs([$this->primaryKeyField, self::$sequenceName]) + ->onlyMethods(['nextSequenceId']) + ->getMock(); + + $feature->expects($this->once()) + ->method('nextSequenceId') + ->willReturn(null); + + $feature->setTableGateway($tableGateway); + + $insert = new Insert('table'); + $insert->columns(['name']); + $insert->values(['test']); + + $result = $feature->preInsert($insert); + + self::assertSame($insert, $result); + + $sequenceValueProp = new ReflectionProperty(SequenceFeature::class, 'sequenceValue'); + self::assertNull($sequenceValueProp->getValue($feature)); + + $rawState = $insert->getRawState(); + self::assertNotContains('id', $rawState['columns']); + } } diff --git a/test/unit/TableGateway/Feature/TestAsset/TestGlobalAdapterFeatureSubclass.php b/test/unit/TableGateway/Feature/TestAsset/TestGlobalAdapterFeatureSubclass.php new file mode 100644 index 000000000..4a35144c7 --- /dev/null +++ b/test/unit/TableGateway/Feature/TestAsset/TestGlobalAdapterFeatureSubclass.php @@ -0,0 +1,14 @@ +getMockBuilder(DriverInterface::class)->getMock(); $mockDriver->expects($this->any())->method('createStatement')->willReturn($mockStatement); $mockDriver->expects($this->any())->method('getConnection')->willReturn($mockConnection); + $mockPlatform = $this->getMockBuilder(PlatformInterface::class)->getMock(); // setup mock adapter $this->mockAdapter = $this->getMockBuilder(Adapter::class) ->onlyMethods([]) - ->setConstructorArgs([$mockDriver]) + ->setConstructorArgs([$mockDriver, $mockPlatform]) ->getMock(); } @@ -83,9 +87,8 @@ public function testConstructor(): void self::assertSame($resultSet, $table->getResultSetPrototype()); self::assertSame($sql, $table->getSql()); - // constructor expects exception - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Table name must be a string or an instance of PhpDb\Sql\TableIdentifier'); + // constructor expects exception - native type declaration throws TypeError for null table + $this->expectException(TypeError::class); /** @psalm-suppress NullArgument - Testing incorrect constructor */ new TableGateway( null, @@ -350,4 +353,66 @@ public function testDeleteShouldResetTableToUnaliasedTable( $state['table'] ); } + + public function testConstructorThrowsExceptionWhenSqlTableDoesNotMatch(): void + { + $sql = new Sql($this->mockAdapter, 'bar'); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'The table inside the provided Sql object must match the table of this TableGateway' + ); + + new TableGateway('foo', $this->mockAdapter, null, null, $sql); + } + + public function testConstructorWithSingleFeature(): void + { + $feature = new Feature\SequenceFeature('id', 'foo_seq'); + + $table = new TableGateway('foo', $this->mockAdapter, $feature); + + $featureSet = $table->getFeatureSet(); + self::assertInstanceOf(FeatureSet::class, $featureSet); + self::assertSame($feature, $featureSet->getFeatureByClassName(Feature\SequenceFeature::class)); + } + + public function testConstructorWithArrayOfFeatures(): void + { + $feature1 = new Feature\SequenceFeature('id', 'foo_seq'); + $feature2 = new Feature\GlobalAdapterFeature(); + + // Set up global adapter for GlobalAdapterFeature + Feature\GlobalAdapterFeature::setStaticAdapter($this->mockAdapter); + + $table = new TableGateway('foo', $this->mockAdapter, [$feature1, $feature2]); + + $featureSet = $table->getFeatureSet(); + self::assertInstanceOf(FeatureSet::class, $featureSet); + self::assertSame($feature1, $featureSet->getFeatureByClassName(Feature\SequenceFeature::class)); + self::assertSame($feature2, $featureSet->getFeatureByClassName(Feature\GlobalAdapterFeature::class)); + + // Clean up static adapter + $reflection = new ReflectionProperty(Feature\GlobalAdapterFeature::class, 'staticAdapters'); + $reflection->setValue(null, []); + } + + public function testConstructorWithFeatureSet(): void + { + $feature = new Feature\SequenceFeature('id', 'foo_seq'); + $featureSet = new FeatureSet([$feature]); + + $table = new TableGateway('foo', $this->mockAdapter, $featureSet); + + self::assertSame($featureSet, $table->getFeatureSet()); + } + + public function testConstructorWithCustomResultSetPrototype(): void + { + $resultSet = new ResultSet(); + + $table = new TableGateway('foo', $this->mockAdapter, null, $resultSet); + + self::assertSame($resultSet, $table->getResultSetPrototype()); + } }