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());
+ }
}